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.

5e8107f48d4471a40de325151d589b6d?s=128

Noah Gibbs

May 01, 2019
Tweet

Transcript

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

    & RICHARD MACKLIN
  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
  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.
  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
  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
  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.
  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.
  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.
  9. SECTION ONE:
 A FRAMEWORK, A TEST APP Git link: none

    We start anew, like our primitive ancestors
  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.)
  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.
  12. DEPENDENCIES ARE BAD -
 ADD ONE NOW! # r00lz/r00lz.gemspec spec.authors

    = [ "Mimi" ] spec.email = "me@me.com" 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"
  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.
  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!"]] }
  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.
  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.
  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"
  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...
  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
  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
  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
  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?"
  23. TESTS OR IT DIDN'T HAPPEN We wrote code. Yay! We

    didn't test it. Hm. Let's test it.
  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.
  25. FIRST THINGS FIRST Run "rake test" from inside R00lz. You

    should get one failure and zero errors.
  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
  27. END SECTION ONE How is everybody doing? We'll pause for

    a few moments before moving to section 2.
  28. SECTION TWO:
 A CONTROLLER Git link: https://github.com/noahgibbs/r00lz Branch: section_two git

    clone git@github.com:noahgibbs/r00lz.git -b section_two
  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.
  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
  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
  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
  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
  34. AND FINALLY... In config.ru: # quotes/config.ru require_relative "config/app" require_relative "app/q_controller"

    run Quotes::App.new
  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.
  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...
  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
  38. SECTION THREE:
 LOAD WITHOUT REQUIRE Git link: https://github.com/noahgibbs/r00lz Branch: section_three

    git clone git@github.com:noahgibbs/r00lz.git -b section_three
  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.
  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
  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
  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!
  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.
  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
  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
  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
  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
  48. BUT HOW TO USE IT? Pop into Quotes... # quotes/config.ru

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

    require_relative "config/app" $LOAD_PATH << "#{__dir__}/app" run Quotes::App.new
  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?
  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.
  52. SECTION FOUR:
 ERB VIEWS Git link: https://github.com/noahgibbs/r00lz Branch: section_four git

    clone git@github.com:noahgibbs/r00lz.git -b section_four
  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.
  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)
  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
  56. IN QUOTES... In the "quotes" app, add an app/ views

    subdirectory. You'll need it in a minute.
  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
  58. AND FINALLY, A VIEW # app/views/shakes.html.erb There is nothing either

    good or bad but <%= @noun %> makes it so.
  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".
  60. TAKE A BREATH Okay, I'll give folks a second to

    debug. Having trouble? Have a question? Let us know!
  61. SECTION FIVE:
 PARAMS AND RACK Git link: https://github.com/noahgibbs/r00lz Branch: section_five

    git clone git@github.com:noahgibbs/r00lz.git -b section_five
  62. RACK REQUESTS How do Ruby frameworks handle parameter parsing, HTTP

    and so on? Mostly they just let Rack do it. You can too.
  63. RACK::REQUEST Rack::Request is an object that parses the environment hash

    and accesses useful bits such as params.
  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
  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
  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.
  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.
  68. SECTION SIX: MODELS Git link: https://github.com/noahgibbs/r00lz Branch: section_six git clone

    git@github.com:noahgibbs/r00lz.git -b section_six
  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.
  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.
  71. AN EXAMPLE QUOTE # quotes/data/1.json { "text": "Ack!", "speaker": "Cathy"

    }
  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"
  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
  74. A VIEW And we'll need that view: # quotes/app/views/quote.html.erb <p>

    &quot;<%= @q["text"] %>&quot; <br/> - <%= @q["speaker"] %> </p>
  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
  76. STILL GOING... # r00lz/lib/r00lz.rb / FileModel def [](field) @hash[field.to_s] end

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

    nil end end # class FileModel
  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".
  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.
  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.
  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
  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