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. THE GREAT
    MIGRATION
    David Demaree
    [email protected] • @ddemaree
    ChicagoRuby
    October 4, 2011

    View Slide

  2. View Slide

  3. http://j.mp/q2jl54

    View Slide

  4. Merb to Rails 3

    View Slide

  5. View Slide

  6. View Slide

  7. View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  11. M E R B C O M M U N I T Y c a . 2 0 1 1

    View Slide

  12. View Slide

  13. Fork and maintain Merb
    Move to Rails 3

    View Slide

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

    View Slide

  15. D A V I D V S . M E R B

    View Slide

  16. S E T U P

    View Slide

  17. http://cl.ly/AebP

    View Slide

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

    View Slide

  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

    View Slide

  20. app/
    controllers/
    antiques.rb
    application.rb
    application_controller.rb
    exceptions.rb
    novelties_controller.rb
    MERB & RAILS CONTROLLERS, TOGETHER AT LAST

    View Slide

  21. class Application < Merb::Controller
    ...
    include Typekit::MerbExtensions::RailsHelper
    include Typekit::MerbExtensions::CsrfSupport
    ...
    end
    Bridging Merb to Rails

    View Slide

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

    View Slide

  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

    View Slide

  24. R O U T I N G

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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"

    View Slide

  29. Next-Level Routing Tricks

    View Slide

  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

    View Slide

  31. class FlipperConstraint
    def initialize(flag)
    @flag = flag
    end
    def matches?(request)
    request.env['flipper.manager'].flag_enabled?(@flag)
    end
    end

    View Slide

  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

    View Slide

  33. M I D D L E W A R E

    View Slide

  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

    View Slide

  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

    View Slide

  36. Cookies, Sessions, & the Rails Flash

    View Slide

  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

    View Slide

  38. # In a middleware
    session['user.id'] = 1234
    #=> 1234
    # In a Rails controller
    session['user.id']
    #=> nil

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  42. Cross-Site Request Forgery (CSRF)
    Protection

    View Slide

  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

    View Slide

  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

    View Slide

  45. T E S T I N G

    View Slide

  46. View Slide

  47. A test suite should
    be a trusted system.

    View Slide

  48. View Slide

  49. Custom Example Groups
    Our solution:

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  54. Cucumber
    Merb legacy request specs
    Rails integration specs
    Capybara-driven acceptance specs
    Choose your tools wisely

    View Slide

  55. Q&A
    to make up for your speaker’s woeful lack of preparation

    View Slide

  56. S T R AT E G Y

    View Slide

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

    View Slide

  58. New work should
    be done in Rails

    View Slide

  59. Favor small iterations
    over big rewrites

    View Slide

  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

    View Slide

  61. Build bridges between
    Rails & Merb when necessary

    View Slide

  62. Use Rack

    View Slide

  63. Use integration tests

    View Slide

  64. When in doubt,
    read the source code

    View Slide

  65. View Slide

  66. View Slide

  67. View Slide

  68. View Slide

  69. View Slide

  70. View Slide

  71. View Slide

  72. View Slide