Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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.

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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.

Slide 7

Slide 7 text

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.

Slide 8

Slide 8 text

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.

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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.

Slide 12

Slide 12 text

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"

Slide 13

Slide 13 text

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.

Slide 14

Slide 14 text

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!"]] }

Slide 15

Slide 15 text

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.

Slide 16

Slide 16 text

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.

Slide 17

Slide 17 text

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"

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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?"

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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.

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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.

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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.

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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.

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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!

Slide 43

Slide 43 text

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.

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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?

Slide 51

Slide 51 text

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.

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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.

Slide 54

Slide 54 text

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)

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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.

Slide 67

Slide 67 text

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.

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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.

Slide 70

Slide 70 text

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.

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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"

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

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.

Slide 80

Slide 80 text

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.

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

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