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 full-size slide

  2. http://j.mp/q2jl54

    View full-size slide

  3. Merb to Rails 3

    View full-size slide

  4. 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 full-size slide

  5. 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 full-size slide

  6. 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 full-size slide

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

    View full-size slide

  8. Fork and maintain Merb
    Move to Rails 3

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  11. http://cl.ly/AebP

    View full-size slide

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

    View full-size slide

  13. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  16. 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 full-size slide

  17. # 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 full-size slide

  18. R O U T I N G

    View full-size slide

  19. 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 full-size slide

  20. 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 full-size slide

  21. 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 full-size slide

  22. # 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 full-size slide

  23. Next-Level Routing Tricks

    View full-size slide

  24. 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 full-size slide

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

    View full-size slide

  26. # 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 full-size slide

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

    View full-size slide

  28. # 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 full-size slide

  29. 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 full-size slide

  30. Cookies, Sessions, & the Rails Flash

    View full-size slide

  31. 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 full-size slide

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

    View full-size slide

  33. # 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 full-size slide

  34. 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 full-size slide

  35. 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 full-size slide

  36. Cross-Site Request Forgery (CSRF)
    Protection

    View full-size slide

  37. 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 full-size slide

  38. 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 full-size slide

  39. T E S T I N G

    View full-size slide

  40. A test suite should
    be a trusted system.

    View full-size slide

  41. Custom Example Groups
    Our solution:

    View full-size slide

  42. 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 full-size slide

  43. 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 full-size slide

  44. # 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 full-size slide

  45. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  48. S T R AT E G Y

    View full-size slide

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

    View full-size slide

  50. New work should
    be done in Rails

    View full-size slide

  51. Favor small iterations
    over big rewrites

    View full-size slide

  52. 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 full-size slide

  53. Build bridges between
    Rails & Merb when necessary

    View full-size slide

  54. Use integration tests

    View full-size slide

  55. When in doubt,
    read the source code

    View full-size slide