The Great Migration: From Merb to Rails 3 at Typekit

The Great Migration: From Merb to Rails 3 at Typekit

Late in 2008 the Rails and Merb development teams merged to create Rails 3, the most robust, most extensible, best release of everyone's favorite Ruby web framework. But while those competing teams merged, their frameworks didn't. Though Rails 3 shows plenty of influence from Merb and its creators, today Merb itself is a legacy framework, with no clear, supported path for Merb apps to move forward to Rails 3 without significant effort.

C157b16234f1e75e8eac3698c1d4414a?s=128

David Demaree

October 05, 2011
Tweet

Transcript

  1. THE GREAT MIGRATION David Demaree david@typekit.com • @ddemaree ChicagoRuby October

    4, 2011
  2. None
  3. http://j.mp/q2jl54

  4. Merb to Rails 3

  5. None
  6. None
  7. None
  8. Also, the Merb guys aren’t just abandoning the existing Merb

    user base and their applications. They’ll still be doing bug fixes, security fixes, and work on easing the upgrade path to Rails 3. This will all progress in a nice, orderly fashion. David Heinemeier Hansson DECEMBER 23, 2008
  9. Merb folks will not be left out in the cold.

    We will continue to support … the merb 1.0.x line. And we will provide a clear upgrade path to Rails 3.0 for merb apps. Ezra Zygmuntowicz DECEMBER 23, 2008
  10. We will also release versions of Merb specifically designed to

    help ease the transition to Rails 3 … with deprecation notices and other transitional mechanisms to assist developers in tracking down the changes that will come between Merb 1.x and Rails 3. Yehuda Katz DECEMBER 23, 2008
  11. M E R B C O M M U N

    I T Y c a . 2 0 1 1
  12. None
  13. Fork and maintain Merb Move to Rails 3

  14. Move to Rails 3 Fork and maintain Merb Move to

    Rails 3
  15. D A V I D V S . M E

    R B
  16. S E T U P

  17. http://cl.ly/AebP

  18. MERB config/init.rb config/router.rb RAILS config/application.rb config/routes.rb Shared Config Directory

  19. module Typekit class Application < Rails::Application ... config.before_initialize do Merb.push_path(:view,

    "#{config.root}/app/views") end config.after_initialize do Merb.start_environment(:environment => Rails.env.to_s) end ... end end
  20. app/ controllers/ antiques.rb application.rb application_controller.rb exceptions.rb novelties_controller.rb MERB & RAILS

    CONTROLLERS, TOGETHER AT LAST
  21. class Application < Merb::Controller ... include Typekit::MerbExtensions::RailsHelper include Typekit::MerbExtensions::CsrfSupport ...

    end Bridging Merb to Rails
  22. module Typekit module MerbExtensions module RailsHelper extend ActiveSupport::Concern included do

    |base| base.class_eval do # Include Rails routes into Merbland include Rails.application.routes.url_helpers # Alias resource/url routes as merb_resource/merb_url, # for API consistency with the Rails app alias merb_resource resource alias merb_url url end end ...
  23. # app/helpers/typekit/merb_transition_helper.rb module Typekit module MerbTransitionHelper include AssetBundles def merb_url(named_url,

    *args) Merb::Router.url(named_url, *args) end ... # app/controllers/application_controller.rb class ApplicationController < ActionController::Base include Typekit::MerbTransitionHelper ... Bridging Rails to Merb
  24. R O U T I N G

  25. module RackApplication def self.new Rack::Builder.new do use Middleware::ExceptionCatcher map "#{host}/api/v1"

    do # Sinatra-based API app run Api::V1::Application.new end map "#{host}/" do # Merb app run Merb::Rack::Application.new end end end end
  26. module RackApplication def self.new Rack::Builder.new do use Middleware::ExceptionCatcher map "#{host}/"

    do # Rails app run Typekit::Application end end end end
  27. Typekit::Application.routes.draw do # Sinatra app mount Api::V1::Application => "/api/v1" #

    .. Rails routes here .. # # Merb app match "*wildcard" => Merb::Rack::Application.new # Home page (Rails controller) root :to => "static_pages#home" end config/routes.rb
  28. # Passes through entire URI get "/api/v1" => Api::V1::Application #

    Only passes through the segment after # the given prefix, e.g. /api/v1/foo is # handled by the Sinatra app as /foo mount Api::V1::Application => "/api/v1"
  29. Next-Level Routing Tricks

  30. constraints(FlipperConstraint.new(:new_browse_ui)) do get "/fonts" => "browse/families#index", :as => :browse_families end

    # If the :new_browse_ui flag is false, requests # for /fonts are served by the Merb app CONSTRAINTS
  31. class FlipperConstraint def initialize(flag) @flag = flag end def matches?(request)

    request.env['flipper.manager'].flag_enabled?(@flag) end end
  32. # config/routes.rb # /backend is normally served by the Merb

    app get "/backend" => "backend/home#index" # app/controllers/backend/home_controller.rb class Backend::HomeController < ApplicationController def index # The X-Cascade header tells Rack to skip this # endpoint and look for the next suitable one, # which in this case would be the Merb app head :ok, "X-Cascade" => "pass" end end X-CASCADE: PASS
  33. M I D D L E W A R E

  34. # send all exceptions to hoptoad use Rack::Hoptoad, HOPTOAD_API_KEY do

    |notifier| notifier.environment_filters << %w(warden rack.session) end # run offline jobs after each request when a # separate process isn't used. use Middleware::OfflineJobRunner # reload the application after each request if # rails is configured to do so. use Middleware::RailsAutoload app/racks/rack_application.rb
  35. config/initializers/middleware.rb Rails.application.config.middleware.tap do |m| # send all exceptions to hoptoad

    m.use Rack::Hoptoad, HOPTOAD_API_KEY do |notifier| notifier.environment_filters << %w(warden rack.session) end # run offline jobs after each request when a # separate process isn't used. m.use Middleware::OfflineJobRunner # reload the application after each request if # rails is configured to do so. m.use Middleware::RailsAutoload end
  36. Cookies, Sessions, & the Rails Flash

  37. module RackApplication def self.new Rack::Builder.new do use Middleware::ExceptionCatcher use Rack::Session::Cookie,

    :key => ::Merb::Config[:session_cookie_name], :secret => ::Merb::Config[:session_secret_key], :domain => typekit_cookie_domain, :httponly => true ... end end end app/racks/rack_application.rb Stage 1: Rack sessions
  38. # In a middleware session['user.id'] = 1234 #=> 1234 #

    In a Rails controller session['user.id'] #=> nil
  39. # Initialize Rails cookie store use Middleware::SecretToken, "sekrit token here"

    use ActionDispatch::Cookies # Enable Rails session storage for all endpoints use ActionDispatch::Session::CookieStore, :key => Merb::Config[:session_cookie_name], :domain => typekit_cookie_domain, :httponly => true # Enable Rails flash use ActionDispatch::Flash Stage 2: Port Rails session middlewares to RackApplication
  40. module Middleware class SecretToken TOKEN_KEY = "action_dispatch.secret_token".freeze def initialize(app, secret)

    @app = app; @secret = secret end def call(env) env[TOKEN_KEY] ||= @secret return @app.call(env) end end end
  41. Stage 3: Switch to standard Rails sessions # config/initializers/session_store.rb Typekit::Application.config.session_store

    :cookie_store, :key => Typekit.config.session_cookie_name, :domain => typekit_cookie_domain, :httponly => true
  42. Cross-Site Request Forgery (CSRF) Protection

  43. module Typekit module MerbExtensions module CsrfSupport extend ActiveSupport::Concern def verified_request?

    request.method == :get || form_authenticity_token == params[csrf_param_name] || form_authenticity_token == request.env['HTTP_X_CSRF_TOKEN'] end def verify_csrf_token unless verified_request? raise Merb::Controller::PreconditionFailed end end end end end
  44. module Middleware class CsrfToken def initialize(app) @app = app end

    def call(env) env['rack.session'][:_csrf_token] ||= SecureRandom.base64(32) @app.call(env) end end end
  45. T E S T I N G

  46. None
  47. A test suite should be a trusted system.

  48. None
  49. Custom Example Groups Our solution:

  50. module Typekit module LegacyRequestExampleGroup extend ActiveSupport::Concern # Include Merb request

    stuff include Merb::Test::RouteHelper include Merb::Test::RequestHelper include Merb::Test::MultipartRequestHelper ... end end Defining a custom example group
  51. RSpec.configure do |config| config.include Typekit::LegacyRequestExampleGroup, :type => :legacy_request, :example_group =>

    { :file_path => config.escaped_path(%w[spec requests]) } end Adding a custom example group to RSpec
  52. # BTW, specs in spec/requests are automatically tagged thusly describe

    "GET /login over non-SSL", :type => :legacy_request do it "should redirect to SSL" do # Returns a Merb::Test::MakeRequest struct object @response = request("/login") @response.should redirect_to("https://secure.example.org/login") end end
  53. describe "GET /login over non-SSL", :type => :legacy_request do it

    "should redirect to SSL" do # Returns a Merb::Test::MakeRequest struct object @response = request("/login") @response.should redirect_to("https://secure.example.org/login") end end describe "GET /login over SSL", :type => :legacy_request do it "should be successful" do # Returns an ActionDispatch::Request object @response = request(path_over_ssl("/login")).body @response.status.should == 200 end end
  54. Cucumber Merb legacy request specs Rails integration specs Capybara-driven acceptance

    specs Choose your tools wisely
  55. Q&A to make up for your speaker’s woeful lack of

    preparation
  56. S T R AT E G Y

  57. Merb and Rails co-exist for the foreseeable future

  58. New work should be done in Rails

  59. Favor small iterations over big rewrites

  60. Try to change just one thing at a time Trustworthy

    test coverage, passing before and after Code review Merge and deploy once tests are passing Big, complex features can be migrated in stages
  61. Build bridges between Rails & Merb when necessary

  62. Use Rack

  63. Use integration tests

  64. When in doubt, read the source code

  65. None
  66. None
  67. None
  68. None
  69. None
  70. None
  71. None
  72. None