Slide 1

Slide 1 text

Rails Engines from the bottom up Akira Matsuda

Slide 2

Slide 2 text

self.name Akira Matsuda (দా ໌)

Slide 3

Slide 3 text

From Japan

Slide 4

Slide 4 text

self.inspect GitHub: amatsuda Twitter: @a_matsuda a committer of: Ruby, Rails, Haml the founder of: Asakusa.rb an organizer of: RubyKaigi

Slide 5

Slide 5 text

Unknown Ruby 2.1 "new feature" # before 2.1: ActiveRecord::Base.send :include, MyPagination # since 2.1: ActiveRecord::Base.include MyPagination

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

amatsuda creator of: kaminari, active_decorator, action_args, gem-src, html5_validators, everywhere, kawaii_validation, i18n_generators, ... recently created: database_rewinder, ljax_rails

Slide 9

Slide 9 text

Rails Engineer?

Slide 10

Slide 10 text

https://twitter.com/kosaki55tea/ status/147525384546689024 Boeing 787 engine engineer strongly objects to software engineers calling them selves engineers.

Slide 11

Slide 11 text

Ruby on Rails has Engines!

Slide 12

Slide 12 text

What are Rails Engines? "Engines can be considered miniature applications that provide functionality to their host applications" (Rails Guides)

Slide 13

Slide 13 text

Engine.is_a? Engine.is_a Rails application Engine.is_a Rails plugin Engine.is_a Gem

Slide 14

Slide 14 text

From the bottom up % cd RAILS_ROOT % mkdir hello hello/hello.gemspec

Slide 15

Slide 15 text

Creating a gem

Slide 16

Slide 16 text

We need a gemspec # hello/hello.gemspec Gem::Specification.new do |g| g.name = 'hello'f g.version = '0'f end

Slide 17

Slide 17 text

Bundling via bundler # RAILS_ROOT/Gemfile gem 'hello'f, path: 'hello'f

Slide 18

Slide 18 text

Bundler (RubyGems) adds PLUGIN_ROOT/lib into $LOAD_PATH % rails r 'puts $:'

Slide 19

Slide 19 text

Now let's implement some features

Slide 20

Slide 20 text

Say hello from our plugin # hello/lib/hello.rb puts 'Hello, Kiev!'f

Slide 21

Slide 21 text

What happens when bundling hello gem? RubyGems adds the lib directory into $LOAD_PATH Then someone calls hello/lib/ hello.rb le somehow

Slide 22

Slide 22 text

Let's see who called hello.rb # hello/lib/hello.rb puts caller puts 'Hello, Kiev!'f

Slide 23

Slide 23 text

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 `' .../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 `' .../gems/railties-4.0.0/lib/rails/commands.rb:86:in `require' .../gems/railties-4.0.0/lib/rails/commands.rb:86:in `' ./bin/rails:4:in `require' ./bin/rails:4:in `'

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

Creating a Rails plugin gem Tie your gem with the main app Just declare a class inheriting ::Rails::Railties class

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

YOUR_APP::Application. initialize! YOUR_APP/con g/environment.rb => railties/lib/rails/application.rb: initialize! => railties/lib/rails/initalizable.rb: run_initializers => raities/lib/rails/application.rb: initializer ... => Railties.new

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

Rails Engine A Rails plugin is a gem A Rails plugin is a Railtie A Rails Engine is also a Railtie

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

Enginize hello gem by subclassing Rails::Engine # hello/lib/hello.rb module Hello class Engine < ::Rails::Engine end end

Slide 36

Slide 36 text

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 ]

Slide 37

Slide 37 text

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 ...

Slide 38

Slide 38 text

Doesn't have to be a named class # hello/lib/hello.rb module Hello Class.new ::Rails::Engine end

Slide 39

Slide 39 text

Doesn't have to be wrapped inside the Hello module # hello/lib/hello.rb Class.new ::Rails::Engine

Slide 40

Slide 40 text

Engine can contain models # hello/app/models/foo.rb class Foo def foo p 'foooo'f end end

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

$LOAD_PATH % rails r 'puts $:'

Slide 43

Slide 43 text

eager_load_paths? # hello/lib/hello.rb p Class.new(::Rails::Engine).config.eager_load_paths

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

Rails Engine is_a plugin is_a Gem is_a tiny application that can contain models, controllers, routes, etc.

Slide 46

Slide 46 text

What can Engines do?

Slide 47

Slide 47 text

My First Rails Engine

Slide 48

Slide 48 text

amatsuda/kaminari A pagination plugin Created for Rails 3.0.0 AFAIK the 2nd Rails Engine in the world

Slide 49

Slide 49 text

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)

Slide 50

Slide 50 text

Anatomy of Kaminari kaminari !"" app # !"" helpers # $"" views # $"" kaminari !"" config # $"" locales !"" lib # !"" generators # # $"" kaminari # # $"" templates # $"" kaminari # !"" helpers # $"" models $"" spec

Slide 51

Slide 51 text

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!

Slide 52

Slide 52 text

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)

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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/

Slide 55

Slide 55 text

"Kaminari" Naming a gem is so hard There was no available /.*page.*/ name @hsbt named it "kaminari" probably inspired by "nokogiri"

Slide 56

Slide 56 text

"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)

Slide 57

Slide 57 text

New version? The next version will be 0.15.0 We're working on that...

Slide 58

Slide 58 text

Mountable Engines

Slide 59

Slide 59 text

The Problem constants, routes, helpers often con ict between main_app and Engines

Slide 60

Slide 60 text

The con ict main_app !"" app # $"" controllers # $"" users_controller.rb $"" my_engine $"" app $"" controllers $"" users_controller.rb

Slide 61

Slide 61 text

The Solution Since Rails 3.1 "Isolated Engine" or "Mountable Engine"

Slide 62

Slide 62 text

Mountable Engine % rails plugin new my_engine -- mountable

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

Mountable Engine's Engine # my_engine/lib/my_engine/engine.rb module MyEngine class Engine < ::Rails::Engine isolate_namespace MyEngine end end

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

Real world examples Documentations in Rails ER diagram Page transition diagram

Slide 67

Slide 67 text

ER Diagram

Slide 68

Slide 68 text

amatsuda/erd

Slide 69

Slide 69 text

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.

Slide 70

Slide 70 text

(DEMO) rails g some model

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

amatsuda/roundabout

Slide 74

Slide 74 text

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`

Slide 75

Slide 75 text

(DEMO) % rails g scaffold user name age:integer % rake db:migrate require 'roundabout/rspec'f add spec/feature/???_spec.rb

Slide 76

Slide 76 text

What roundabout does Generates documents Tells us missing `visit`s Shows us "page visit coverage" Motivates users to complete feature specs

Slide 77

Slide 77 text

amatsuda/hocus_pocus

Slide 78

Slide 78 text

hocus_pocus Released before 3.1.0 stable AFAIK this was the rst released mountable Engine in the world

Slide 79

Slide 79 text

HocusPocus Records your browser action You can copy & paste the generated text into your spec/ features. No need to handwrite anymore!

Slide 80

Slide 80 text

(DEMO)

Slide 81

Slide 81 text

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`

Slide 82

Slide 82 text

(DEMO)

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

amatsuda/ljax_rails

Slide 87

Slide 87 text

ljax_rails "Lazy-load" partial views in Ajax Created last week Not an "isolated" engine but loads a JS le from app/assets

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

render :partial <%# app/views/users/index.html.erb %> <%= render 'users'f %>

Slide 90

Slide 90 text

remote: true <%# app/views/users/index.html.erb %> <%= render 'users'f, remote: true %>

Slide 91

Slide 91 text

What if the query is very very slow? class UsersController < ApplicationController ... def index if request.ljax? @users = User.all sleep 3 end ennd

Slide 92

Slide 92 text

Have fun with Rails Engines, and be a great Rails Engineer!