Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Rails Engines from the bottom up

Akira Matsuda
September 28, 2013

Rails Engines from the bottom up

Slides for RubyShift 2013 talk "Rails Engines from the bottom up" http://rubyshift.org/

Akira Matsuda

September 28, 2013
Tweet

More Decks by Akira Matsuda

Other Decks in Programming

Transcript

  1. self.inspect GitHub: amatsuda Twitter: @a_matsuda a committer of: Ruby, Rails,

    Haml the founder of: Asakusa.rb an organizer of: RubyKaigi
  2. Unknown Ruby 2.1 "new feature" # before 2.1: ActiveRecord::Base.send :include,

    MyPagination # since 2.1: ActiveRecord::Base.include MyPagination
  3. My least favorite Ruby 2.1 new feature # freezing String

    'Kiev'.frozen # 'frozen String' literal (the f suffix) 'Kiev'f # "f" is not for "Float" '1.0'f # "to_f" to get a "Float" '1.0'f.to_f
  4. f to every String!ɹ # Then this is how our

    code would look like... source 'https://rubygems.org'f gem 'rails'f, '4.0.0'f gem 'sqlite3'f gem 'sass-rails'f, '~> 4.0.0'f gem 'uglifier'f, '>= 1.3.0'f gem 'coffee-rails'f, '~> 4.0.0'f gem 'jquery-rails'f gem 'turbolinks'f gem 'jbuilder'f, '~> 1.2'f group :doc do gem 'sdoc'f, require: false end
  5. What are Rails Engines? "Engines can be considered miniature applications

    that provide functionality to their host applications" (Rails Guides)
  6. What happens when bundling hello gem? RubyGems adds the lib

    directory into $LOAD_PATH Then someone calls hello/lib/ hello.rb le somehow
  7. the callers were require require require require... .../gems/bundler-1.3.5/lib/bundler/runtime.rb:72:in `require' .../gems/bundler-1.3.5/lib/bundler/runtime.rb:72:in

    `block (2 levels) in require' .../gems/bundler-1.3.5/lib/bundler/runtime.rb:70:in `each' .../gems/bundler-1.3.5/lib/bundler/runtime.rb:70:in `block in require' .../gems/bundler-1.3.5/lib/bundler/runtime.rb:59:in `each' .../gems/bundler-1.3.5/lib/bundler/runtime.rb:59:in `require' .../gems/bundler-1.3.5/lib/bundler.rb:132:in `require' ./config/application.rb:7:in `<top (required)>' .../gems/railties-4.0.0/lib/rails/commands/runner.rb:42:in `require' .../gems/railties-4.0.0/lib/rails/commands/runner.rb:42:in `<top (required)>' .../gems/railties-4.0.0/lib/rails/commands.rb:86:in `require' .../gems/railties-4.0.0/lib/rails/commands.rb:86:in `<top (required)>' ./bin/rails:4:in `require' ./bin/rails:4:in `<main>'
  8. How Bundler requires a gem # gems/bundler-1.3.5/lib/bundler/runtime.rb module Bundler class

    Runtime < Environment def require(*groups) ... @definition.dependencies.each do |dep| ... Array(dep.autorequire || dep.name).each do |file| required_file = file Kernel.require file end ... ennnnnd
  9. Kernel.require(autorequire || name) I didn't set anything to "autorequire" attribute,

    so it just requires the "name" equivalent to `require 'hello'` hello/lib/ is included in $LOAD_PATH, so nally ruby nds hello/lib/hello.rb and loads it
  10. Creating a Rails plugin gem Tie your gem with the

    main app Just declare a class inheriting ::Rails::Railties class
  11. Adding Railtie class and an initializer # hello/lib/hello.rb module Hello

    class Railtle < ::Rails::Railtie initializer 'hello.init'f do puts 'initializing hello...'f end end end
  12. What happens when Raitie.inherited? # railties/lib/rails/railtie.rb module Rails class Railtie

    class << self def inherited(base) unless base.abstract_railtie? subclasses << base end end ... ennnd
  13. Railtie.subclasses # railties/lib/rails/engine/railties.rb module Rails class Engine < Railtie class

    Railties include Enumerable attr_reader :_all def initialize @_all || = ::Rails::Railtie.subclasses.map(&:instance) + ::Rails::Engine.subclasses.map(&:instance) end ... ennnd
  14. The points so far You can bundle a directory as

    a gem if it has a .gemspec le bundler automatically calls "gem/lib/#{gemname}.rb" You can create a Rails hook by inheriting ::Rails::Railtie class inside the plugin
  15. Rails Engine A Rails plugin is a gem A Rails

    plugin is a Railtie A Rails Engine is also a Railtie
  16. Anatomy of Rails Engine my_engine !"" app # !"" assets

    # # !"" images # # !"" javascripts # # $"" stylesheets # !"" controllers # !"" helpers # !"" mailers # !"" models # $"" views !"" bin !"" config !"" lib # !"" my_engine # # !"" engine.rb # # $"" version.rb # !"" my_engine.rb # $"" tasks $"" test
  17. Difference between Engines and non-Engine plugins An Engine can contain

    models, controllers, views, routes, etc An Engine should contain a subclass of ::Rails::Engine
  18. If a class.is_a? Engine, it should be a Railtie as

    well % rails r "p Rails::Engine.ancestors" [ Rails::Engine, Rails::Railtie, Rails::Initializable, Object, ActiveSupport::Dependencies::Loadable, JSON::Ext::Generator::GeneratorMethods::Object, Kernel, BasicObject ]
  19. What happens when subclassed? # railties/lib/rails/engine.rb class Engine < Railtie

    class << self attr_accessor :called_from, :isolated ... def inherited(base) ... base.called_from = begin call_stack = caller.map { |p| p.sub(/:\d+.*/, '') } File.dirname(call_stack.detect { |p| p !~ %r[railties[\w.-]*/lib/rails|rack[\w.-]*/lib/rack] }) end ...
  20. Doesn't have to be wrapped inside the Hello module #

    hello/lib/hello.rb Class.new ::Rails::Engine
  21. How could Rails nd Foo? hello/app/models was added in $LOAD_PATH

    hello/app/models was added in the Engine's eager_load_paths
  22. Make it a webapp # hello/config/routes.rb Rails.application.routes.draw do resources :foos

    end # hello/app/controllers/foos_controller.rb class FoosController < ApplicationController def index render text: Foo.new.foo end end
  23. Rails Engine is_a plugin is_a Gem is_a tiny application that

    can contain models, controllers, routes, etc.
  24. Why Engine? Rails Engine can contain app/ {controllers,models,views,helpers,etc} A pagination

    library consists of model extension, helpers, and views It was quite natural to implement as an Engine (Just wanted to try the new feature of Rails 3.0)
  25. Anatomy of Kaminari kaminari !"" app # !"" helpers #

    $"" views # $"" kaminari !"" config # $"" locales !"" lib # !"" generators # # $"" kaminari # # $"" templates # $"" kaminari # !"" helpers # $"" models $"" spec
  26. Initial version https://github.com/amatsuda/ kaminari/tree/v0.1.0 There exist some bugs, but the

    code is pretty straightforward Only 4 les in kaminari/lib/ directory!
  27. v0.1.0 (railtie.rb for the Raitie class) (engine.rb for the Engine

    class) active_record.rb for AR extension (helper.rb for the helpers) (views are in app/views)
  28. active_record.rb (edited) # kaminari/lib/kaminari/active_record.rb (edited) module Kaminari::ActiveRecord extend ActiveSupport::Concern included

    do def self.inherited(kls) kls.class_eval do scope :page, lambda {|num| offset(PER_PAGE * ([num.to_i, 1].max - 1)).limit(10) } do def per(num) offset(offset_value / limit_value * num).limit(num) end ... ennnnnd
  29. How this code works? How does Kaminari paginate? (Pat Shaughnessy)

    http://patshaughnessy.net/2011/9/10/ how-does-kaminari-paginate Active Record scopes vs class methods (Carlos Antônio) http://blog.plataformatec.com.br/ 2013/02/active-record-scopes-vs- class-methods/
  30. "Kaminari" Naming a gem is so hard There was no

    available /.*page.*/ name @hsbt named it "kaminari" probably inspired by "nokogiri"
  31. "Kaminari" ཕ == thunder I know it's hard to memorize,

    hard to spell, and hard to pronounce for you, non-Japanese In fact, everybody pronounces it incorrectly (I don't mind though)
  32. The con ict main_app !"" app # $"" controllers #

    $"" users_controller.rb $"" my_engine $"" app $"" controllers $"" users_controller.rb
  33. Anatomy of a Moutable Engine MyEngine !"" app # !""

    assets # # !"" images # # # $"" my_engine # # !"" javascripts # # # $"" my_engine # # # $"" application.js # # $"" stylesheets # # $"" my_engine # # $"" application.css # !"" controllers # # $"" my_engine # # $"" application_controller.rb # !"" helpers # # $"" my_engine # # $"" application_helper.rb # !"" mailers # # $"" my_engine # !"" models # # $"" my_engine # $"" views # $"" layouts # $"" my_engine # $"" application.html.erb !"" config # $"" routes.rb !"" lib # !"" my_engine # # !"" engine.rb # # $"" version.rb # $"" my_engine.rb !"" my_engine.gemspec $"" test
  34. What can a Mountable Engine do? It can provide safely

    namespaced models, controllers, views, etc. It can be "mounted" onto the parent Rails application's certain URL It can refer to the parent application from controllers / views via `main_app` method
  35. ERD A mountable Engine that loads the main_app's models and

    draws the ER diagram on the browser You can create new model, add / rename / alter column, etc.
  36. A tiny tip erd mounts itself onto the main_app's routes

    on the :after_initialize hook, so users don't have to con gure anything
  37. Self-mounting Engine # erd/lib/erd/railtie.rb module Erd class Railtie < ::Rails::Railtie

    #:nodoc: initializer 'erd' do |app| ActiveSupport.on_load(:after_initialize) do if Rails.env.development? Rails.application.routes.append do mount Erd::Engine, :at => '/erd' ennnnnnd
  38. I'll be the Roundabout Visualizes page (action) transitions No need

    for any extra programming or document writing. Just run `rake spec` Records all page transitions in `rake spec`
  39. (DEMO) % rails g scaffold user name age:integer % rake

    db:migrate require 'roundabout/rspec'f add spec/feature/???_spec.rb
  40. What roundabout does Generates documents Tells us missing `visit`s Shows

    us "page visit coverage" Motivates users to complete feature specs
  41. HocusPocus Records your browser action You can copy & paste

    the generated text into your spec/ features. No need to handwrite anymore!
  42. HocusPocus (reprise) A Wiki-wiki way Rails app development platform Users

    can edit current page like a Wiki page Catches URL missing in the browser for example, just visit http://localhost:3000/books Catches link_to missing in the browser for example, add `link_to 'authors', authors_path`
  43. Anatomy of HocusPocus hocus_pocus !"" engines # !"" command_line #

    # !"" app # # # !"" controllers # # # $"" views # # !"" config # # $"" lib # !"" editor # # !"" app # # # !"" assets # # # !"" controllers # # # $"" views # # !"" config # # $"" lib # !"" generator # # !"" app # # # !"" assets # # # !"" controllers # # # !"" helpers # # # $"" views # # !"" config # # $"" lib # $"" recorder # !"" app # # !"" assets # # !"" controllers # # $"" views # !"" config # $"" lib !"" lib # !"" generators # # $"" hocus_pocus # $"" hocus_pocus $"" spec
  44. Engines in an Engine! AFAIK the only Rails Engine that

    contains other Rails Engines inside Adds each Engine's lib, app/* into $LOAD_PATH require_paths does the trick
  45. Gem::Speci cation #require_path # hocus_pocus/hocus_pocus.gemspec Gem::Specification.new do |s| s.name =

    'hocus_pocus' s.version = HocusPocus::VERSION s.authors = ['Akira Matsuda'] s.homepage = 'https://github.com/amatsuda/hocus_pocus' ... s.require_paths = ['lib', 'engines/generator/lib', 'engines/ editor/lib', 'engines/recorder/lib', 'engines/command_line/lib'] ... end
  46. ljax_rails "Lazy-load" partial views in Ajax Created last week Not

    an "isolated" engine but loads a JS le from app/assets
  47. Anatomy of ljax_rails ljax_rails !"" app # $"" assets #

    $"" javascripts # $"" ljax_rails.js.coffee !"" lib # !"" ljax_rails # # !"" action_controller_monkey.rb # # !"" action_dispatch_monkey.rb # # $"" action_view_monkey.rb # $"" ljax_rails.rb !"" ljax_rails.gemspec $"" spec
  48. What if the query is very very slow? class UsersController

    < ApplicationController ... def index if request.ljax? @users = User.all sleep 3 end ennd