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

Build Your Own Framework to Understand Rails Magic

Build Your Own Framework to Understand Rails Magic

A quick "how to" with full code for every stage. Start with an empty directory, and build a Ruby MVC framework, plus a tiny test app to show it off.

Noah Gibbs

May 01, 2019
Tweet

More Decks by Noah Gibbs

Other Decks in Programming

Transcript

  1. BUILD YOUR OWN FRAMEWORK
    TO UNDERSTAND RAILS MAGIC
    NOAH GIBBS & RICHARD MACKLIN

    View Slide

  2. APPFOLIO PAYS US TO RUBY

    THANK YOU, APPFOLIO!
    I wrote a book about this
    called Rebuilding Rails.
    My performance blog
    posts are often in Ruby
    Weekly.
    We work for AppFolio.
    Github: noahgibbs & rmacklin blog: engineering.appfolio.com

    View Slide

  3. WHAT ARE WE DOING?
    Rails is "just Ruby" - the "magic" is metaprogramming.
    If you know the metaprogramming, the magic follows.
    The easiest way to see it is to build it.
    So: today we build it.

    View Slide

  4. WAIT - ALL OF IT?
    No time to build all of Rails. We build chunks of working web
    framework, structured like Rails.
    Rack-Compatible Controllers
    Automatic "require"
    ERB Views
    Rack Requests and Params
    Simple Models

    View Slide

  5. WHAT YOU GET
    We'll build a web framework and a sample app
    that looks like a Rails app
    You'll leave with a working framework
    This will be fast-paced and intense
    It's cool - intense is fun

    View Slide

  6. CLASS NOTES
    You need Ruby and Bundler. Git is good.
    We work in sections. Your code will do something
    useful at the end of a section.
    Can't get a section working? There's a git branch
    for each. You can move on.

    View Slide

  7. WAIT WAIT WAIT
    We're typing a web framework? Yup.
    It's easier than you think. 132 lines,
    all-in, with app and framework both.

    View Slide

  8. NON-CLASS NOTE
    Many names are cut short for the slides. Annoying.
    What if it weren't on slides? Here's a coupon to get my
    book free, just for today. It's the longer version.
    http://bit.ly/rebuilding2019free
    Stay, though. You'll get through more if you start now.

    View Slide

  9. SECTION ONE:

    A FRAMEWORK, A TEST APP
    Git link: none
    We start anew, like our primitive ancestors

    View Slide

  10. A DIRECTORY
    You need a place for two directories for your
    framework and your test app. They'll go side-by-
    side in the same dir (but don't make them yet.)

    View Slide

  11. A WHOLE NEW GEM
    You'll need a gem to be the framework.
    $ bundle gem r00lz
    This creates a new gem in a directory called
    "r00lz."
    Now you can edit r00lz.gemspec.

    View Slide

  12. DEPENDENCIES ARE BAD -

    ADD ONE NOW!
    # r00lz/r00lz.gemspec
    spec.authors = [ "Mimi" ]
    spec.email = "[email protected]"
    spec.homepage = "http://mimi.com/r00lz"
    spec.summary = %q{My Web Framework}
    spec.description = %q{A Web Framework, but
    with extra awesome.}
    # ...
    spec.add_runtime_dependency "rack", "~>2.0.7"

    View Slide

  13. AN APP CALLED "QUOTES"
    You'll also make a test app in your source
    directory. Just "mkdir quotes" for it. It's not a gem
    or a Rails app, just an empty directory.

    View Slide

  14. VERY QUICK INTRO TO RACK
    Rack is Ruby's interface to web servers.
    A Rack endpoint acts like a proc. It calls #call with
    request info and the value is sent back:
    # config.ru
    run proc {
    [ 200, {}, ["hello, web!"]]
    }

    View Slide

  15. HELLO, WEB
    Put this file in your quotes directory:
    # quotes/config.ru
    run proc {
    [ 200,
    { 'Content-Type' => 'text/html'},
    ["hello, web!"]
    ]
    }
    That's enough to say hello to the web... nearly.

    View Slide

  16. NEARLY?
    You still need Rack. Let's add a Gemfile:
    # quotes/Gemfile
    source "https://rubygems.org"
    gem "r00lz", path: "../r00lz"
    Run "bundle" from inside "quotes" to install gems
    and create Gemfile.lock. Now we're ready.

    View Slide

  17. LIKE SLOW, ANNOYING MAGIC!
    Still from "quotes", run "bundle exec rackup
    -p 3000". It runs Ruby's built-in web server,
    WEBrick, on port 3000, with your "hello web"
    app.
    Then go to "http://localhost:3000"

    View Slide

  18. $EDITOR R00LZ.RB
    That's great! Now we want your framework to do
    it. We'll make an App object which calls code in
    r00lz.
    Open up r00lz/lib/r00lz.rb...

    View Slide

  19. R00LZ/LIB/R00LZ.RB
    # r00lz/lib/r00lz.rb
    require "r00lz/version"
    module R00lz
    class App
    def call(env) # Like proc#call
    [200,
    {'Content-Type' => 'text/html'},
    ["Hello from R00lz!"]]
    end
    end
    end

    View Slide

  20. NOW FOR YOUR APP
    Mkdir quotes/config, then define an App class:
    # quotes/config/app.rb
    require "r00lz"
    module Quotes
    class App < R00lz::App
    end
    end

    View Slide

  21. NOW...
    Now use that app class. You'll replace the old
    proc-and-run code completely:
    # quotes/config.ru
    require_relative 'config/app'
    run Quotes::App.new

    View Slide

  22. DID IT WORK?
    Again, run "bundle exec rackup -p 3000"
    from inside "quotes".
    Now point your browser at "http://localhost:
    3000".
    Do you see "Hello from R00lz?"

    View Slide

  23. TESTS OR IT DIDN'T HAPPEN
    We wrote code. Yay!
    We didn't test it. Hm. Let's test it.

    View Slide

  24. FRAMEWORK TESTS
    There are lots of kinds of tests. We're going to do
    a simple framework test.
    The "bundle gem" command that created r00lz
    also set it up with minitest. We'll use that.

    View Slide

  25. FIRST THINGS FIRST
    Run "rake test" from inside R00lz. You should get
    one failure and zero errors.

    View Slide

  26. A FIRST TEST
    # test/r00lz_test.rb:
    class R00lzTest < Minitest::Test
    def test_app_returns_success
    env = { "PATH_INFO" => "/",
    "QUERY_STRING" => "" }
    assert_equal 200,
    ::R00lz::App.new.call(env)[0]
    end
    end

    View Slide

  27. END SECTION ONE
    How is everybody doing? We'll
    pause for a few moments before
    moving to section 2.

    View Slide

  28. SECTION TWO:

    A CONTROLLER
    Git link: https://github.com/noahgibbs/r00lz
    Branch: section_two
    git clone [email protected]:noahgibbs/r00lz.git -b section_two

    View Slide

  29. AN OLD-STYLE CONTROLLER
    Rails 2.0 would route "/posts/fnargl" to
    PostsController#fnargl even with only
    default routes. This was a terrible security idea,
    but convenient. Today, we'll do the same.

    View Slide

  30. CODE TO MAKE IT WORK
    # r00lz/lib/r00lz.rb
    class App
    def call(env) # Replace prev
    kl, act = cont_and_act(env)
    text = kl.new(env).send(act)
    [200, {'Content-Type' =>
    'text/html'}, [text]]
    end
    end

    View Slide

  31. MORE CODE
    # r00lz/lib/r00lz.rb
    class App
    def cont_and_act(env)
    _, con, act, after =
    env["PATH_INFO"].split('/')
    con = con.capitalize +
    "Controller"
    [Object.const_get(con), act]
    end
    end

    View Slide

  32. A CONTROLLER CLASS
    # r00lz/lib/roolz.rb
    module R00lz
    class Controller # New class, add it
    attr_reader :env
    def initialize(env)
    @env = env
    end
    end
    end

    View Slide

  33. ADD TO YOUR APP
    And now in "quotes", make an "app" directory:
    # quotes/app/q_controller.rb
    class QController <
    R00lz::Controller
    def a_quote
    "What's up, Doc?"
    end
    end

    View Slide

  34. AND FINALLY...
    In config.ru:
    # quotes/config.ru
    require_relative "config/app"
    require_relative "app/q_controller"
    run Quotes::App.new

    View Slide

  35. LET'S SEE IT GO!
    At this point, you should be able to type "bundle
    exec rackup -p 3000" to start the server...
    And aim your browser at "localhost:3000/q/
    a_quote" to see your new quote.

    View Slide

  36. AWESOME! BUT WAIT...
    This is great! But we broke our test. Now we must
    specify a controller and action. Let's fix the test...

    View Slide

  37. BACK TO TESTING
    # r00lz/test/r00lz_test.rb
    class TedController < R00lz::Controller
    def think; "Whoah, man..."; end
    end
    class R00lzTest < Minitest::Test
    def test_new_controller_action
    e = { "PATH_INFO" => "/ted/think",
    "QUERY_STRING" => "" }
    assert_equal 200,
    ::R00lz::App.new.call(e)[0]
    end
    end

    View Slide

  38. SECTION THREE:

    LOAD WITHOUT REQUIRE
    Git link: https://github.com/noahgibbs/r00lz
    Branch: section_three
    git clone [email protected]:noahgibbs/r00lz.git -b section_three

    View Slide

  39. CONST_MISSING, QUICK
    In Ruby, classes are constants. If you try to use a
    constant that doesn't exist, Ruby calls
    const_missing.
    So: you can use a class without loading it first if
    you do the right thing with const_missing.

    View Slide

  40. HOW TO CONST_MISSING
    What would using it look like? This, almost...
    class Object
    def self.const_missing(c)
    require "./bobo"
    Bobo
    end
    end
    Bobo.new.some_instance_method

    View Slide

  41. NEARLY THERE...
    Pretty soon, you'll add code (nearly) like this to R00lz:
    class Object
    def self.const_missing(c)
    require c
    Object.const_get(c)
    end
    end

    View Slide

  42. NEARLY THERE...
    Pretty soon, you'll add code (nearly) like this to R00lz:
    class Object
    def self.const_missing(c)
    require c
    Object.const_get(c)
    end
    end
    Wrong name!

    View Slide

  43. CAMEL CASE AND SNAKE CASE
    Mixed-caps is called CamelCase. Lower-and-
    underscores is called snake_case.
    But how do we convert between them? It's not
    built in... So we'll do it ourselves.

    View Slide

  44. SPECIAL-PURPOSE CODE
    # r00lz/lib/r00lz.rb
    module R00lz # Also: bit.ly/gibbs-regexp-cheat
    def self.to_underscore(s)
    s.gsub(
    /([A-Z]+)([A-Z][a-z])/,
    '\1_\2').gsub(
    /([a-z\d])([A-Z])/,
    '\1_\2').downcase
    end
    end

    View Slide

  45. SPECIAL-PURPOSE CODE
    # r00lz/lib/r00lz.rb
    module R00lz # Also: bit.ly/gibbs-regexp-cheat
    def self.to_underscore(s)
    s.gsub(
    /([A-Z]+)([A-Z][a-z])/,
    '\1_\2').gsub(
    /([a-z\d])([A-Z])/,
    '\1_\2').downcase
    end
    end

    View Slide

  46. SPECIAL-PURPOSE CODE
    # r00lz/lib/r00lz.rb
    module R00lz # Also: bit.ly/gibbs-regexp-cheat
    def self.to_underscore(s)
    s.gsub(
    /([A-Z]+)([A-Z][a-z])/,
    '\1_\2').gsub(
    /([a-z\d])([A-Z])/,
    '\1_\2').downcase
    end
    end

    View Slide

  47. ASSEMBLE THE PIECES
    Now we can add the code to R00lz to automatically require our
    classes.
    # r00lz/lib/r00lz.rb
    class Object
    def self.const_missing(c)
    require R00lz.to_underscore(c.to_s)
    Object.const_get(c)
    end
    end

    View Slide

  48. BUT HOW TO USE IT?
    Pop into Quotes...
    # quotes/config.ru
    require_relative "config/app"
    $LOAD_PATH << "#{__dir__}/app"
    run Quotes::App.new

    View Slide

  49. BUT HOW TO USE IT?
    Pop into Quotes...
    # quotes/config.ru
    require_relative "config/app"
    $LOAD_PATH << "#{__dir__}/app"
    run Quotes::App.new

    View Slide

  50. AND FINALLY...
    Now: "bundle exec rackup -p 3000" and
    load "http://localhost:3000/q/a_quote".
    For good measure, replace the previous quote
    with a new one. Maybe something from a TV
    show?

    View Slide

  51. EXTRA CREDIT
    Some folks may be having trouble - live coding is
    like that. If you have spare time, you could add a
    test. You'll need to add a controller file and
    maybe a $LOAD_PATH entry to test auto-
    requiring controllers.

    View Slide

  52. SECTION FOUR:

    ERB VIEWS
    Git link: https://github.com/noahgibbs/r00lz
    Branch: section_four
    git clone [email protected]:noahgibbs/r00lz.git -b section_four

    View Slide

  53. USING ERB
    ERB (Embedded Ruby) is the code in templates
    that <%= "looks" %> like

    <%= "t" + "his" %>.
    While there are several ERB implementations,
    we'll use the one that's built into Ruby.

    View Slide

  54. HOW DOES ERB WORK?
    You can make an ERB object from a template and
    evaluate it:
    require "erb"
    thing = "bunnies"
    e = ERB.new(
    "Yay, <%= thing %>!")
    puts e.result(binding)

    View Slide

  55. A CONTROLLER METHOD
    We'll add a controller method to R00lz:
    module R00lz
    require "erb" # Or do this up-top
    class Controller
    def render(name, b = binding())
    template = "app/views/#{name}.html.erb"
    e = ERB.new(File.read template)
    e.result(b)
    end
    end
    end

    View Slide

  56. IN QUOTES...
    In the "quotes" app, add an app/
    views subdirectory. You'll need it in a
    minute.

    View Slide

  57. AND AN APP ACTION
    # Add an action to app/q_controller.rb
    class QController <
    R00lz::Controller
    def shakes
    @noun = :winking
    render(:shakes)
    end
    end

    View Slide

  58. AND FINALLY, A VIEW
    # app/views/shakes.html.erb
    There is nothing either good
    or bad but <%= @noun %> makes
    it so.

    View Slide

  59. FIRE IT UP
    By now, you know the drill: "bundle exec
    rackup -p 3000" and point a browser... This
    time at "http://localhost:3000/q/shakes".

    View Slide

  60. TAKE A BREATH
    Okay, I'll give folks a second to
    debug. Having trouble? Have a
    question? Let us know!

    View Slide

  61. SECTION FIVE:

    PARAMS AND RACK
    Git link: https://github.com/noahgibbs/r00lz
    Branch: section_five
    git clone [email protected]:noahgibbs/r00lz.git -b section_five

    View Slide

  62. RACK REQUESTS
    How do Ruby frameworks handle
    parameter parsing, HTTP and so on?
    Mostly they just let Rack do it.
    You can too.

    View Slide

  63. RACK::REQUEST
    Rack::Request is an object that
    parses the environment hash and
    accesses useful bits such as params.

    View Slide

  64. SHOW ME
    In your controller class:
    # r00lz/lib/r00lz.rb
    class Controller
    #...
    def request
    @request ||= Rack::Request.new @env
    end
    def params
    request.params
    end

    View Slide

  65. AND A VERY QUICK ACTION
    # quotes/app/q_controller.rb
    class QController < R00lz::Controller
    def card_trick
    n = params["card"] || "Queen"
    "Your card: the #{n} of spades!"
    end
    end

    View Slide

  66. AND THEN CHECK
    Now "bundle exec rackup -p 3000"
    and check "http://localhost:3000/
    q/card_trick". It should say the Queen...
    Then add "?card=hedgehog" afterward
    and make sure it changes.

    View Slide

  67. GETTING TIRED?
    If you're feeling itchy, you can quickly
    add a test for params. If you need a
    breather, this is a good time for a
    minute or three of rest.

    View Slide

  68. SECTION SIX: MODELS
    Git link: https://github.com/noahgibbs/r00lz
    Branch: section_six
    git clone [email protected]:noahgibbs/r00lz.git -b section_six

    View Slide

  69. A TINY DIVERSION
    Models can involve databases, but they don't
    have to. We're going to use models that read
    files.
    For our quotes app, we'll use JSON files to
    contain quotes. Model objects will represent data
    files, not database rows.

    View Slide

  70. MODEL DATA
    Let's make .JSON files with quotes. I use short
    quotes so they fit on slides.
    You can use your favorite quotes.
    First, mkdir quotes/data to hold them.

    View Slide

  71. AN EXAMPLE QUOTE
    # quotes/data/1.json
    {
    "text": "Ack!",
    "speaker": "Cathy"
    }

    View Slide

  72. SO HOW DO WE USE IT?
    First, make sure we have the JSON gem. In r00lz.gemspec:
    # r00lz.gemspec
    spec.add_runtime_dependency "rack", "~>2.0.7"
    spec.add_runtime_dependency "json", "~>2.1.0"

    View Slide

  73. LET'S START IN THE APP
    Let's start with the app this time and what using it looks like.
    # quotes/app/q_controller.rb
    class QController
    def fq
    @q = FileModel.find(params["q"] || 1)
    render :quote
    end
    end

    View Slide

  74. A VIEW
    And we'll need that view:
    # quotes/app/views/quote.html.erb

    "<%= @q["text"] %>"

    - <%= @q["speaker"] %>

    View Slide

  75. BUT HOW TO DO IT?
    # r00lz/lib/r00lz.rb
    class FileModel
    def initialize(fn)
    @fn = fn
    cont = File.read fn
    @hash = JSON.load cont
    end

    View Slide

  76. STILL GOING...
    # r00lz/lib/r00lz.rb / FileModel
    def [](field)
    @hash[field.to_s]
    end
    #...

    View Slide

  77. AND FINALLY...
    # r00lz/lib/r00lz.rb
    #(...continued...)
    def self.find(id)
    self.new "data/#{id}.json"
    rescue
    nil
    end
    end # class FileModel

    View Slide

  78. YOU MADE IT!
    You know what sucks? Typing code from slides.
    You know what you just did anyway? Built a
    model, like a boss.
    Take a breath, then "bundle exec rackup -p
    3000", and point your browser at "http://
    localhost:3000/q/fq".

    View Slide

  79. A PAUSE, A BREATH

    ALSO POSSIBLY SWEARING
    Let's take a minute. Some of you saw it just work.
    Some of you didn't. This was the biggest chunk
    of code in this whole workshop.
    (After this you're done.)
    So let's debug it.

    View Slide

  80. BY THE WAY...
    You just built a framework. Then a controller. Then
    a view. Then a model.
    What did you just build from nothing? A real
    working MVC framework.
    Take a bow. Or cheer. Seriously.

    View Slide

  81. ONCE MORE
    Still here? AWESOME. You rock.
    I'm impressed with you. Seriously.
    Here's that coupon again. I'd be honored if you worked
    through the book - but life's busy, so up to you.
    http://bit.ly/rebuilding2019free

    View Slide

  82. ALL DONE
    Questions? Anything else?
    These slides: http://bit.ly/railsconf2019-gibbs
    Book coupon: http://bit.ly/rebuilding2019free
    Git: https://github.com/noahgibbs/r00lz
    Thank you, AppFolio!
    Twitter: codefolio blog: engineering.appfolio.com
    Github: noahgibbs & rmacklin

    View Slide