ActionTree

Manual pages


         \/ |    |/
      \/ / \||/  /_/___/_
       \/   |/ \/
  _\__\_\   |  /_____/_
         \  | /          /
__ _-----`  |{,-----------~
          \ }{
           }{{     ACTION TREE:
           }}{
           {{}     a dry request router/controller
     , -=-~{ .-^- _
ejm        `}
            {

This document covers:

  1. Architecture
  2. Usage
    1. Defining routes
    2. Looking up requests
    3. Running match results
  3. Dialects
  4. Advanced usage
    1. Modularity
    2. Recursive routes

 

Architecture

ActionTree is a DRY request router/controller framweork suitable for web apps. Given a request, such as /houses/21/parties, it runs the relevant pieces of code in the right order, with the right variables. It is a bit similar to Sinatra's routing, in that it maps URLs to procs, but is much more powerful.

The routes are kept as a tree of nodes. Request paths are looked up through the tree in the same way that file paths match a file tree. Unlike file trees, one node can match several, and not just one path fragment. In other words, looking up '/house/32/info' first looks for a child node that matches 'house', then from there a child node matching '32', and from there child node matching 'info'.

Each node has:

  • A match token
  • N child nodes

Each node also has

  • One optional (namespaced) action
  • One optional not_found handler
  • N ordered before filters
  • N ordered after filters
  • N ordered postprocessors

All of these are procs. Everything except the action is inherited by all descending nodes.

Finally, each node has

  • A helper scope with n methods, stored as a module.

These helpers are also inherited by all descending nodes.


Since all routes are trees, all routes can easily be reused or mounted in several locations. Since inheritance happens per request, the same route mounted several places can work differently in each context.

This enables DRY and concise controllers and routes.

Actually, the routes are not even really trees, but graphs, which means you can build infinite, recursive routes, such as /me/dad/mom/mom/dad/dad/dad/... -- although that would be a strange thing to do.

Paths and locations

I will differentiate between request paths and route locations.

Paths, like /houses/21...

  • Express route lookups.
  • Are divided into fragments, like "21", "bobsleigh" or "Tirol".

Locations, like houses/:number...

  • Express route definitions.
  • Are divided into tokens, like :number, 'about' or /[a-z]+/.
  • Can be
    • plain: 'about'
    • captures:
    • :number,
    • ':embedded-:symbol'
    • /regex[p]?/.

 

Defining routes

DSL Methods

Each of the following DSL methods take an optional location argument and a block parameter. The location format is specified later in this document. If omitted, the location will be the same as the current context.

route(location=nil, &blk) : Also known as with, r, w, _ : Evaluates the code in blk in context of the specified location.

action(location=nil, layer=nil, &blk) : Also known as a, o : Attaches an action. Any number of actions can be attached to every node, and they will run in the same order later. layer specifies a layer name, to allow storing different actions for different contexts in the same node.

get, put, post and delete : Shortcuts to layering actions. : These methods are shortcuts to the action method above, to simplify layering of http verbs.

before(location=nil, &blk) : Also known as b : Attaches another before hook to be run for this action and all descendants.

after(location=nil, &blk) : Attaches another after hook to be run for this action and all descendants.

helpers(location=nil, &blk) : Use def inside the block to write helper methods that will be available to all descendants.

not_found(location=nil, &blk) : Also known as x : todo

mount(node) : todo

mount(location, node) : todo

Location format specification

Locations can be expressed in four ways:

  • A single token, such as "bobsleigh", :variety or /[0-9]+/
  • A path string, such as "/book/chapter/:page/:from-:to"
  • An array of tokens, like ["ideas", :id, "search", /[a-z]+/]
  • Omitting the path parameter, resulting in a reference to the current scope.

There are four different types of tokens:

  • "bobsleigh" matches the exact path segment "bobsleigh".
  • :variety matches any path segment and keeps it as @variety.
  • /[0-9]+/ matches the regexp, in this case numbers.
  • ":year-:month-:date" matches "anything-like-this", keeping it as @year, @month and @date

All these can be used with DSL methods, like this:

before('bobsleigh')   { polish_ice }
action(:variety)      { "Thank you for choosing #{@variety}" }
after(/[0-9]+/)       { log_number(@match.to_i) }
helper(':id-:name')   { Cow.get(@id) }
route [/[0-9]+/, /[0-9]+/, /[0-9]+/] do
  "You visited #{match.join('/')}"
end

Utility methods

descend

descend(location) #=> #<ActionTree::Node>

house_routes = routes.descend('houses')

Returns the node at location relative to the current context.

 

Looking up requests

  • matching
    • was it found?
    • on to run
    • match chain

 

Running match results

  • running
    • parameters: scope and action layer.
    • scope is created
    • precedence: captures overwrite source scope.
    • variable copy
    • captures
      • accumulation
      • regexps accumulate in @match
    • esoteric variables, such as @__match
    • dialect variables, such as @get and @post
    • actions are run
    • order each node with before hooks -> run in order of attachment actions of current node run in order of attachment each node with after filters -> run in order of attachment

 

Appendix

Re-using routes

Let's say we want to make a reusable authentication API:

simple_auth = ActionTree.new do
  before { authenticate }
  action('login')   { ... }
  action('logout')  { ... }
end

Now this can be mounted anywhere

some_app = ActionTree.new do
  # tada:
  mount simple_auth

  with('cars') do
    # or within a scope
    mount instant_storefront
  end

  # or with a path:
  mount 'docs/api', magic_api_doc_generator
end

If you want to pick out a subsection from routes you have already built, you can use the descend method:

car_routes = some_app.descend('cars')

Now we can mount just the car routes in another ActionTree, or even somewhere else within the same one. We can even make endless circular route constructs...

family = ActionTree.new do
  before('father') { @person = @person.father }
  before('mother') { @person = @person.mother }
  action('show')   { @person.render }
end

family.mount('father', family)
family.mount('mother', family)

me = ActionTree.new do
  before { @person = ME }
  mount family
end

me.match('mother/father/mother/father/father/father').run # => great-great-grand-whatnot

DSL variants

Mounting and advanced routes