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