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