Download - Rails Engine | Modular application
Problem
monolithic application
no reusability
slow tests
bigger application, more mess
Possible solutions
extract common functionality to modules
create gems
tie gem with rails application with railtie
rails engine
What is a Gem ?
packaged application or library code tests documentation gemspec
Creating gem
bundle gem simplify
structure code (lib/) tests (test/ or spec/) documentation (doc/ README) executable files (bin/)
information about gem simplify.gemspec
├── Gemfile├── LICENSE.txt├── README.md├── Rakefile├── lib│ ├── simplify│ │ └── version.rb│ └── simplify.rb└── simplify.gemspec
Releasing Gem
change version
run `bundle`
commit changes
run `rake release`
$ rake releasestupid_spam_protection 0.0.2 built to pkg/stupid_spam_protection-0.0.2.gemTagged v0.0.2Pushed git commits and tagsPushed stupid_spam_protection 0.0.2 to rubygems.org
in-memory testing with sqlite
require "active_record"require "sqlite3”
ActiveRecord::Base.establish_connection { :adapter => "sqlite3”, :database => ":memory:”}
load "db/schema.rb"
RSpec.configure do |config| config.around do |example| ActiveRecord::Base.transaction do example.run raise ActiveRecord::Rollback end endend
ActiveRecord::Schema.define do create_table "items", :force => true do |t| t.string "name” t.datetime "created_at", :null => false t.datetime "updated_at", :null => false end
spec/spec_helper.rb
db/schema.rb
testing with mysql database
begin ActiveRecord::Base.establish_connection(config)rescue ActiveRecord::Base.establish_connection(config.merge('database' => nil)) ActiveRecord::Base.connection.create_database(config['database'], { :charset => 'utf8’, :collation => 'utf8_unicode_ci’})end
load "db/schema.rb"
RSpec.configure do |config| # the same from previous slideend
config = HashWithIndifferentAccess.new({ :adapter => "mysql2", :host => "127.0.0.1", :username => "root", :password => "", :database => ”database_name”})
spec/spec_helper.rb
Tie gem to Rails app Rails::Railtie
extend Rails framework load all needed dependencies
several hooks methods for all needs
module WnmSupport class Railtie < Rails::Railtie initializer "wnm_support.view_helpers" do ActionView::Base.send :include, BoolToHuman end endend
require "wnm_support/railtie" if defined?(Rails) lib/wnm_support.rb
lib/wnm_support/railtie.rb
Railtie hooks 1
Plugin hooks initializer generators rake_tasks console
initializer "wnm_support.active_record_ext" do ActiveSupport.on_load(:active_record) do ActiveRecord::Base.send :include, WnmSupport::ActiveRecordExt::DestroyValidation endend
Railtie hooks 2
Rails configuration hooks to_prepare (once in production, every request in
development)
before_initialize after_initialize app_middleware before_configuration before_eager_load generatorsconfig.to_prepare do AppController.send(:include, Controller::Authentication)end
What is Rails Engine ?
every rails app is an engine
Rails::Engine < Rails::Railtie
all together gem skeleton railtie structure for app dummy app for testing assets
Engine walkthrough
generate new engine
basic commands
testing
mounting to host app
overriding defaults
Generating Engine
rails plugin new blorgh --mountable –full
everything (assets, controller, models, views) are inside Blorgh namespacemodule Blorgh class Engine < ::Rails::Engine isolate_namespace Blorgh endend
Basics commands
in root directory of the engine bundle rake rspec rails generate # generate code for engine
in spec/dummy or test/dummy location rails console rails server rails generate # generate code for dummy app
Testing
generally as in standard rails application
engine is tested inside dummy app
small problems with factory girl explicite model class in factory girl include routes for request tests
FactoryGirl.define do factory :user, :class => Blorgh ::User do sequence(:login) { |n| "admin_#{n}" } password "too_secret" password_confirmation { password } endend
RSpec.configure do |config| config.include Blorgh ::Engine.routes.url_helpersend
Mounting Engine to host application
Gemfile gem “blorgh”, :path => “~/Sites/blorgh”
install migration `rake blorgh:install:migrations`
load seeds data Blorgh::Engine.load_seed # in rails console
require assets if needed *= require blorgh/application #
assets/stylesheets/application.css
mount to routes.rb mount Blorgh::Engine => "/blorgh"
Get to engine and get back
routes to engine from host app prefix with engine name example engine “blorgh”
<%= link_to “Users”, blorgh.users_path %>
routes from engine to host app prefix with main_app example engine “blorgh”
<%= link_to “Home page”,main_app.root_path %>
Controller, Model, etc. reopening classes
class_eval, module_eval
Overriding engine classes
Blorgh::Article.class_eval do has_many :users, :class_name => ‘User'end
app/decorators/models/blorgh/article.rb
module MySite class Railtie < ::Rails::Railtie config.to_prepare do Dir["app/decorators/**/*.rb"].each do |path| load path end end endend
Overriding engine views
simple copy and paste view file that you want to override to the same location in host app
Application Modules with Rails Engine
every new engine means new namespace
application modules means for us more engines with same namespace
inspiration from refinerycms
The same namespacedifferent engines
module Wnm module Core class Engine < ::Rails::Engine isolate_namespace Wnm engine_name :wnm_core endend
module Wnm module Pages class Engine < ::Rails::Engine isolate_namespace Wnm engine_name :wnm_pages endend
rake wnm_core:install:migrations
Wnm::Core::Engine.load_seed
rake wnm_pages:install:migrations
Wnm::Pages::Engine.load_seed
both are nested in `Wnm` namespace
Developing from localProduction from git
bundler does not allow to have same package from different sources in Gemfile
if ENV["LOAD_GEMS_FROM_LOCAL"] == "1" gem "wnm_core", :path => "~/Sites/wnm_core" gem "wnm_pages", :path => "~/Sites/wnm_pages" gem "wnm_customers", :path => "~/Sites/wnm_customers"else gem "wnm_core", :git => "git@…/wnm_core.git", :tag => "v1.0.0" gem "wnm_pages", :git => "git@…/wnm_pages.git", :tag => "v1.0.0" gem "wnm_customers", :git => "git@.../wnm_customers.git", :tag => "v1.0.0"end
export LOAD_GEMS_FROM_LOCAL=1 .rvmrc
Gemfile
Deploying
change LOAD_GEMS_FROM_LOCAL to 0 and run `bundle`
commit and push
`cap deploy`
Tips
if you are overriding views heavily always prefix routes path with engine module name
use class methods instead of constants
always write full path to class/module if it is in namespace ::ApplicationController instead of ApplicationController
testing is essential
<%= link_to page.name, wnm_page.page_path(page), :title => page.name %>
Why use Rails::Engine ?
reusable code code hole application assets
modular thinking
gem with MVC
faster test
testing gem in dummy app infrastructure
some well known engines devise, kaminari, refinerycms
Problems
if you are bending engines weird thinks can happen
a lot of weird errors application does not run in development mode from git
sources tests passed, but in browser it is not running in browser it is running but tests are throwing exceptions
overriding classes in host app
helper methods from host app does not work in overridden views
some things from documentation does not work at all
Why we did this like that ?
a lot of our project are in general the same
build core for site quickly “scaffold” for application
rails engine is infrastructure for modular application
we can have backend and frontend in the same application module
Sources
http://guides.rubygems.org/what-is-a-gem/ http://www.engineyard.com/blog/2010/extending-rails-3-with-
railties/ http://www.slideshare.net/AndyMaleh/rails-engine-patterns http://edgeguides.rubyonrails.org/engines.html http://edgeapi.rubyonrails.org/classes/Rails/Railtie.html http://edgeapi.rubyonrails.org/classes/Rails/Engine.html http://pivotallabs.com/users/shagemann/blog/articles/1994-
migrating-from-a-single-rails-app-to-a-suite-of-rails-engines http://railscasts.com/episodes/301-extracting-a-ruby-gem http://railscasts.com/episodes/303-publishing-a-gem http://railscasts.com/episodes/277-mountable-engines