-
Notifications
You must be signed in to change notification settings - Fork 2
Description
\1. The most striking thing about Cuba coming to it from Rails or Sinatra is the 'finite state machine' routing. The Readme does a good job explaining this, but even so, I find it sometimes hard to get my head around concrete examples. Maybe someone would like to walk us through the cuba-app example's routes from the top? This gets somewhat into Shield as well (the authentication library).
on root do
res.write view("home", title: "My Site Home")
end
on authenticated(User) do
run Users
end
on "admin" do
run Admins
end
on default do
run Guests
end\2. Another starting point is the config.ru. In the cuba-app example, this consists of basically one line after loading the app: run Cuba . Interesting to consider how this differs from Sinatra, where you would subclass Sinatra::Base and run that. Closely related to this is that routes are defined within a Cuba.define block, rather than directly on the class. And the individual routes are not compiled when defined, and then matched to the request, but evaluated upon each request. Can someone walk us through Cuba.define?
def self.define(&block)
app.run new(&block)
end\3. Yet another starting point is to consider when a request comes in, how it is matched to a route, and how captures are extracted. The Readme gives a good explanation of how this works, but it's a bit tricky to work out how it's implemented with nested calls to on, and the catch(:halt) :
def call!(env)
@env = env
@req = Rack::Request.new(env)
@res = Cuba::Response.new
# This `catch` statement will either receive a
# rack response tuple via a `halt`, or will
# fall back to issuing a 404.
#
# When it `catch`es a throw, the return value
# of this whole `_call` method will be the
# rack response tuple, which is exactly what we want.
catch(:halt) do
instance_eval(&@blk)
res.status = 404
res.finish
end
end
# The heart of the path / verb / any condition matching.
#
# @example
#
# on get do
# res.write "GET"
# end
#
# on get, "signup" do
# res.write "Signup
# end
#
# on "user/:id" do |uid|
# res.write "User: #{uid}"
# end
#
# on "styles", extension("css") do |file|
# res.write render("styles/#{file}.sass")
# end
#
def on(*args, &block)
try do
# For every block, we make sure to reset captures so that
# nesting matchers won't mess with each other's captures.
@captures = []
# We stop evaluation of this entire matcher unless
# each and every `arg` defined for this matcher evaluates
# to a non-false value.
#
# Short circuit examples:
# on true, false do
#
# # PATH_INFO=/user
# on true, "signup"
return unless args.all? { |arg| match(arg) }
# The captures we yield here were generated and assembled
# by evaluating each of the `arg`s above. Most of these
# are carried out by #consume.
yield(*captures)
halt res.finish
end
end\4. Another interesting feature of Cuba compared to Sinatra is it allows you to switch to another handler entirely from within a route, while preserving the current env. (At least, I don't know a straightforward way to do this in Sinatra.) In this way it functions somewhat like Rack::URLMap (as can be seen in the top-level routes in the cuba-app example). Also, the inline example in the code shows how to use this to implement a redirect helper. Anyone care to walk us through this?
# If you want to halt the processing of an existing handler
# and continue it via a different handler.
#
# @example
# def redirect(*args)
# run Cuba.new { on(default) { res.redirect(*args) }}
# end
#
# on "account" do
# redirect "/login" unless session["uid"]
#
# res.write "Super secure account info."
# end
def run(app)
halt app.call(req.env)
end