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