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

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.

David Demaree

October 05, 2011
Tweet

More Decks by David Demaree

Other Decks in Programming

Transcript

  1. 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
  2. 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
  3. 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
  4. M E R B C O M M U N

    I T Y c a . 2 0 1 1
  5. 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
  6. 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 ...
  7. # 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
  8. 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
  9. 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
  10. # 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"
  11. 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
  12. class FlipperConstraint def initialize(flag) @flag = flag end def matches?(request)

    request.env['flipper.manager'].flag_enabled?(@flag) end end
  13. # 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
  14. # 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
  15. 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
  16. 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
  17. # In a middleware session['user.id'] = 1234 #=> 1234 #

    In a Rails controller session['user.id'] #=> nil
  18. # 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
  19. 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
  20. 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
  21. 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
  22. 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
  23. 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
  24. 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
  25. # 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
  26. 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
  27. 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