music photography code blog about
  1. portmanteau of "URLs" and "rb"
  2. as in "Urbs in Horto": it's run by the machine

Urbs, HRP, DRP... 200 OK

In general, "explaining" a concept often involves expressing it in a different vocabulary or conceptual scheme. Details that can be assumed or ignored are left out of the translation. This is also how most abstraction layers work: they allow a program to "think about" a resource in terms familiar to its own API.

Urbs "explains" HTTP transactions in resource-oriented terms; that is, it exposes an interface which deals only with the concepts of resources and their representations. This allows us to reduce the response process to two corresponding tasks:

  • A handler accepts requests, and uses it to secure a resource (or an error);
  • A presenter constructs a representation of the resource for the client (or an error).

...thus saving developer time and focus. (This approach hereafter referred to as HRP; the companion resource-layer approach, DRP, is naturally still on the drawing board.) Urbs uses a finite state machine to abstract and automate the rest of the necessary behavior, making the response predictable, reliable and easily duplicated.

It is also (sort of) router-less, and because Urbs responders are full-fledged Rack apps, they may be used on their own, or routed by another app, such as Sinatra.

The goal is to try some new ways of thinking about common stuff, and see how they pan out. The guiding principles are (a) orthogonality/OO design---Ruby virtues---and (b) putting the concepts of resource and representation front and center in the workflow of handling requests. A basic implementation is now running on the workbench, and it has some interesting properties.

The Machine

The first state change is called when the request is accepted; a callback fires the next event, and so on. The process terminates only in states corresponding to valid HTTP responses, or errors.

This abstraction doesn't entail any loss of meaning (I submit) because all HTTP transactions can be represented as a function from a request to a resource representation. (Everything is RESTful, if you think about the response the right way; it's just a question of how much conceptual gerrymandering it takes.) Urbs just makes this model explicit. The payoff is a dead simple design that lets developers think in architecturally beneficial terms.

Relationship to MVC

MVC separates code concerns, but because it generalizes across all kinds of application styles, it doesn't say anything about the details of handling a user event. When we know the user event is an HTTP request we can structure our code with finer granularity.

HRP can extend MVC. Thought of in that way, the controller is the entire responding object structure (responder + handler + presenter). The "resource" is the controller's reference to a model. ("Resource" isn't exactly synonymous with "model"; more like "model-as-seen-by-a-controller.") The controller's presenter then completely wraps the view layer. (This is very helpful for keeping the view state in order; see more below.)

Why not just call it a "controller?"

For reasons addressed below, an Urbs "responder" is closer to an entire application than an individual controller. Handlers and presenters split the functions usually assigned to a controller, say in Rails. Consequently there is no single Urbs component that maps cleanly to the controller concept.

Brass Tacks

Automating the controller allows common tasks to be reduced to the two objectives mentioned above. An Urbs handler:

# return a resource or an error 
verb :get do 
  # widget fetched in a before filter
  current_user.can?(:read, widget) ? widget : Failures::Unauthorized
end

The object returned from the verb block will be considered the responder's resource, unless it is an error, or if it is nil, which will result in a server error. This object will be available as #resource to the presenter:

layout :widgets
resource :widget # alias_method :widget, :resource

# return a response object or an error
verb :get do
  html do
    template :new_widget_html
    stylesheets :latest_widget_styles
    assign UserNavigation, :left_nav_content
    # implied:
    # response(render(resource...))
  end

  json do
    include :api_includes
  end
end

The presenter has full access to the response, and supports asynchronous operations via full and partial Rack hijacking.

States

State map from Urbs::Responder:

Urbs::Responder state chart

When the handler enters the resource_pending state, the event calls the handler code above (an instance method named #get) to find the resource. If that code returns a resource, the presenter will be called. The presenter can return a response object, an error, or (worst case) raise an exception; the responder maps all of these outcomes to a terminal state. When a responder enters a terminal state, it validates the response object and returns it to Rack.

Components

The machine automates this entire process. I have more than once sat down to sketch a Rails controller, and wound up with a fairly complicated to-do list, that involves retrieving resources, managing error states, mapping error states to views, etc. Rails (it's not the only one) doesn't impose much structure on controller actions. The result can be a little confusing, like the moment you realize #render and #redirect_to don't terminate an action. Rails idioms don't help either, as they tend to express the result in terms of an object's internal state:

# find a widget and store it as one of my instance variables
@widget = Widget.find(params[:id])
# but that's in the controller, and we want our views to act
# as though they have the internal state of our controllers: (!)
<%= @widget.title %>

Obviously, the encapsulation isn't ideal, and it's not always clear what's going on---#render means two pretty different things in a view and a controller, but it's obscured by the effort to make them look like they're the same object from the inside. This also requires the introduction of helper methods to be imported by the view.

Urbs puts up a wall between these ends of the task: one goes to the handler, and the other goes to the presenter. Views then execute in the actual context of their presenters, so there is no faux shared state. In this way, Urbs abstracts more than Rails, which pretty much leaves you to your own devices in the controllers.

In other ways, Urbs is meant to abstract less than (say) Rails: Urbs handlers use HTTP verbs directly, instead of using public methods mapped through the routes table. For example, there are no #show actions in Urbs, only #gets. This may seem like a limitation, but (a) it makes the HTTP semantics clear at all times and (b) it's my feeling the extra abstraction in Rails---from HTTP verbs to controller instance methods---isn't necessary.

After all, either the method can be inferred from the verb itself, (as for Rails routes declared with resources) in which case you might as well just call the method "get" and be done with it; or it can't, and you have to wonder why, given that you can do things like this:

# config/routes.rb
get "widgets/:desired_action" => "widgets#get_dispatcher"

There's really no reason to bring another class (the routing table) into the process here, because the controller can infer the actual method from the path. The only time the additional abstraction is necessary is when the controller method somehow cannot be inferred from the verb or the URL. This sounds like a design problem. (Then again, unlike the Rails team, I don't have 4.2 bazillion existing applications to not break.)

Routes, or not

Anyway, there is no central dispatch, a la config/routes.rb. Instead, Urbs routes requests using a Chain of Responsibility. Handlers are loaded on a generic responder at runtime. Starting from the top, each handler is given a chance to claim the request. If nobody claims the request, 404 is sent. (Notes on implementation here.)

Urbs handlers therefore encapsulate their own routing logic. Example: say we expect many different, but possibly synonymous requests. (Perhaps you've got a desired route scheme, and a legacy scheme to support.) Discerning when two different requests are (semantically) identical is pretty domain specific. It feels out of place in a routing class shared across the whole application. (At least when consulted directly, a la Rails; Urbs allows you to define application-wide route schemas, but they are consulted by the handler itself, not a dispatcher. See the next example.) This way, the routing logic stays with the actions themselves. Plus, it lets you easily manage multiple routing strategies:

def claim?
  # check all of our available routing 
  !route_packages.map { ... }.compact.empty?
end

# meanwhile:
<%= link_to some_route_package.url_for(resource) %>

A few interesting properties/possibilities: (1) the handlers are easy to unit test: #claim? should return true or false, and methods with HTTP verb names should return either a resource or an error. (2) You can override single methods with a new class, and turn them on or off as the application runs. (3) Handlers can implement middleware by stopping a request, rewriting it, and then either declining or returning an error. For example, you can password protect routes by putting this after everything available to anonymous users:

# every handler below this on the chain will be password protected
def claim?
  current_user ? false : Failures::Unauthenticated.new("you need to be logged in")
end

That said, if you want a central dispatch, you can use (say) Sinatra as a router. The "Frankie Machine" setup:

get '/' do
  UrbsHomeApp.call(env)
end

Each Urbs responder is a fully functional Rack app, so you can call it from another application, or plug it straight into a web server.

This is also the meaning of the earlier comparison of a responder to a whole application: the responder comprises all of the available "controllers," so it can't be identical with any particular one of them.

Installation

Add this line to your application's Gemfile:

gem 'urbs'

And then execute:

$ bundle

Or install it yourself as:

$ gem install urbs

Usage

TODO: Write usage instructions here

Contributing

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request

Published on 9 February 2014 Tags: