Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

some questions to get us started #3

Open
ericgj opened this issue Jul 23, 2012 · 3 comments
Open

some questions to get us started #3

ericgj opened this issue Jul 23, 2012 · 3 comments

Comments

@ericgj
Copy link
Member

ericgj commented Jul 23, 2012

\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
@cyx
Copy link

cyx commented Jul 26, 2012

I'll take a stab briefly:

  1. We pretty much design with the mindset: "first to appear, gets the call". In that example you pasted, it goes like:
  • Are we on root? (meaning PATH_INFO is / or ""), if so do this and finish.
  • Is the current user authenticated? If so, pass on the control to Users.
  • Does the PATH_INFO start with /admin? If so, pass control to Admins.

Then much like an else in a case or if-elseif chain, we have an on default which gets run if no other handler gets called.

  1. The example you mentioned provides the most straightforward approach, where your app root runs on Cuba, and then it delegates certain responsibilities to subclasses of Cuba (i.e. Users, Admins, Guests).

But being a subclass of Cuba, you could quite literally run anything else, e.g.

class App < Cuba
  define do
    on root do
      res.write 'Home'
    end
  end
end

and in your config.ru,

require File.expand_path("app", File.dirname(__FILE__))

run App

I guess it boils down to this: Cuba and any subclass of it is a Rack app, and can be expected to behave properly when you do Cuba.call(env), App.call(env), etc.

  1. Nesting an on is actually ~ almost ~ identical to a Rack::URLMap in the sense that both share the implementations. It's no secret that Cuba was originally based and inspired from Rum.

So what Cuba (and Rum, and Rack::URLMap) does is it mutates PATH_INFO and SCRIPT_NAME. I discuss this in detail in one of my articles.

The catch - instance_eval combo, we admit, does a bit of trickery (well they say catch is a poor man's GOTO).

  1. catch's return value is what you throw at it. In a simple example
res = catch :foo do
  throw :foo, "bar"
end

assert_equal "bar", res

So in Cuba context, that would be:

response = catch(:halt) do
  throw :halt, [200, { "Content-Type" => "text/html" }, ["Hello world"]]
end

Anytime an on executes completely, it does a throw :halt essentially, and sends the rack response tuple.

IF on the event we don't match anything, that's where these lines get executed:

res.status = 404
res.finish

Which results to our app returning a 404.

  1. The answer to this is tied closely to what I said in number 3 above with PATH_INFO and SCRIPT_NAME being mutated. At the point that do you the delegation, both are still in their mutated states, i.e.
# PATH_INFO = /users/1
# SCRIPT_NAME = ""
on "users/:id" do |id|
  # PATH_INFO = ""
  # SCRIPT_NAME = "/users/1"

  SubApp.run(req.env) # gets called with PATH_INFO = "", SCRIPT_NAME = "/users/1"
end

Anyway hope these answers shed more light on the questions you posted above.

@ericgj
Copy link
Member Author

ericgj commented Jul 26, 2012

Thanks @cyx. I had meant these questions to spur others to delve into the code, but I'm glad to hear some of your thinking about the design "from the inside". And it looks like @RoboDisco has started a code dive now.

I hadn't quite grasped how the routing works re. the mutation of PATH_INFO and SCRIPT_NAME, that's really a key to the whole implementation.

BTW I think Cuba is a very nice framework. So much leverage in so few lines of code! You are very careful about state, which is refreshing. And it's nicely documented too.

I have a whole set of questions as I start to use it on a real-ish project, is this the best place to ask or is there a mailing list/user group I should be looking at for 'best practices' first ?

@cyx
Copy link

cyx commented Jul 26, 2012

Hi Eric,

We typically hang out in #cuba.rb on freenode, we can probably give much better advice if we have more context
regarding what the problem domain is, and so on.

Thanks,
cyx

On Jul 26, 2012, at 9:14 PM, Eric Gjertsen wrote:

Thanks @cyx. I had meant these questions to spur others to delve into the code, but I'm glad to hear some of your thinking about the design "from the inside". And it looks like @RoboDisco has started a code dive now.

I hadn't quite grasped how the routing works re. the mutation of PATH_INFO and SCRIPT_NAME, that's really a key to the whole implementation.

BTW I think Cuba is a very nice framework. So much leverage in so few lines of code! You are very careful about state, which is refreshing. And it's nicely documented too.

I have a whole set of questions as I start to use it on a real-ish project, is this the best place to ask or is there a mailing list/user group I should be looking at for 'best practices' first ?


Reply to this email directly or view it on GitHub:
#3 (comment)

@ericgj ericgj mentioned this issue Jul 26, 2012
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants