Commute

Build Status Dependency Status

Commute helps you to:

  • Dynamically build HTTP requests for your favorite HTTP library (currently only Typhoeus).
  • Easily process request and response bodies.
  • Execute parallel HTTP requests.

Contexts and Api's

Almost everything in Commute is a context. Contexts represent all the information that Commute needs to build, execute and process your requests. This includes:

  • Request options: url, body, url params, headers, ....
  • Custom options: custom named parameters that can be translated in request options.
  • Stack options: used to provide information on how request/response bodies are processed.
  • A Stack: A sequence of layers that a request/response body is pushed through to be processed (these layers user the stack options).
  • A reference to an Api that is responsible for executing the request.

Api's are nothing more than a set of predefined, named contexts. If you think about it, options and stack layers needed for certain Api calls can be seen as an extension as a context. So these named contexts (aka Api calls) take a context and extend it with the needed information.

Stacks

Stacks are responsible for processing request and response bodies. A stack consists of a request and response sequence. The response sequence is generally the inverse of the request sequence.

A sequence consists of an ordered list of layers. Each layer provides some logic to transform a body according to some options. For example a chemicals layer could parse or render an xml document based on a given template. This would look like this:

class Chemicals < Layer
  def request body, template
    template.render body
  end

  def response body, template
    template.parse body
  end
end

When a context is built into a request. The body is ran through the request sequence. When the request is completed, the response body is ran through the response sequence.

Building Contexts

Extending Contexts 101

Commute is all about building partial contexts and extending them until enough information is present to build a real HTTP request. Basically it works like this:

# Assume there is an Api "GistApi".
# This would create a context containing a custom option 'user'
#   and then one with a context-type header.
defunkt_api = GistApi.with(user: 'defunkt')
defunkt_xml_api = defunkt_api.with(headers: {
  'Content-Type' => 'application/xml'
})

Any further context extensions or api calls on defunkt_xml_api would thus be in xml and for the user 'defunkt'.

For example, you could take an vanilla api, extend it to a context that contains needed authentication parameters, and pass that context through your app acting as an api that does not need authentication. Here lies the power of commute: A context will act as an api with some predefined options and behavior.

Default and raw contexts

Without you knowing, GistApi.with extended a special context of the GistApi called the default context. It contains options that every requests for this api will be needing. When you don't want this you can use the raw context.

Example:

class GistApi < Commute::Api
  def default
    # Extends the raw context with some options.
    # Every request/response will be json.
    raw.with headers: {
      'Content-Type' => 'application/json',
      'Accept' => 'application/json'
    }
  end
end

defunkt = GistApi.with(user: 'defunkt')
defunkt.options.inspect
# => { user: 'defunkt', headers: {
  'Content-Type' => 'application/json',
  'Accept' => 'application/json'
}}

Making basic Api calls

Let's say we want to create an Api call that fetches all gists for a certain user. This call would extend the context by using the custom :user option and putting it in the url.

class GistApi < Commute::Api
  def default
    # Extends the raw context with some options.
    # Every request/response will be json.
    raw.with headers: {
      'Content-Type' => 'application/json',
      'Accept' => 'application/json'
    }
  end

  def for_user context
    context.with url: "https://api.github.com/users/#{context[:user]}/gists"
  end
end

# A context for getting all gists from defunkt.
gists = GistApi.for_user user: 'defunkt'

# or, you can play with it. See the potential?
gists = GistApi.with(user: 'defunkt').for_user

Internally, those last two methods of creating the gists context are the same. When calling an api method from a context, passed arguments are used to create a context to be passed to the api call. That is why the argument of an api call is always context. The first method is only a convenient shortcut.

A basic stack

If we would want to parse and encode json for every request and response we could create a layer like this:

class Json < Commute::Layer
  # Enode on request.
  def request body, options
    Yajl::Encoder.encode body, options
  end

  # Decode on response.
  def response body, options
    Yajl::Parser.parse body, options
  end
end

And use it in our default context:

def default
  # Extends the raw context with some options.
  # Every request/response will be json.
  raw.with headers: {
    'Content-Type' => 'application/json',
    'Accept' => 'application/json'
  } do |stack|
    stack << Json.new(:format)
  end
end

When adding a layer to a stack, you can give it a name. This can then later be used in stack altering or as a reference to pass options to the stack.

Extending Contexts 201

We only showed simple context extending using some extra parameters. As said, contexts also define behavior (How does a request/response body have to be processed?) using stacks.

On extending a context, the stack can be altered.

Lat's say that in the basic stack, we want to send raw json, but we still want the stack to automatically parse json for the response. We could create a new context that does this:

raw_requests = GistApi.with { |stack|
  # Dump the format layer in the request sequence.
  stack.request.without! :format
}

Other stack altering methods are:

  • before!: inserts a layer before a certain named layer.
  • after!: inserts a layer after a certain named layer.

Passing options to the stack

Naming layers has the advantage that they can be used to pass options to the layer. In the case of the Json layer, we could pass options to yajl:

symbolized_api = GistApi.with(format: {
  symbolize_keys: true
})

# Now all further extensions will return bodies with symbolized keys.

Authorization

A authorization mechanism can be implemented for an api by implementing the authorize(context) method. This does not follow the standard context system because Authorization headers can be time sensitive. The Authorization header is computed just before the requests is actually fired.

Hooks

Commute provides a before (default) and after hook. They can be used to set up default context options/behavior or finishing up a context before it can be built.

We already showed how to provide default contexts. The after hook can be provided by implementing after(context).

Building requests and executing them.

We now know how to build a context, it's time to turn those contexts into requests and firing them onto the internets.

Building requests.

When you build a request from a context, you receive a pure Typhoeus::Request. You can do with it what you want, the context system just acts as a builder solution. You can pass a block to the build method that gets called when the request completes. This block gets called with:

  • The pure Typhoeus::Response as it was received from typhoeus.
  • A transformed response body (went through the stack).

For example:

request = GistApi.for_user(user: 'defunkt').build do |response, gists|
  puts gists.count
end

Executing requests

Executing requests is done through the rush method on a context. It executes the given request, runs the response through the stack and call the callback block.

Building on the previous example that would go like this:

GistApi.rush request
# => 10

Remember that GistApi.rush calls the rush method on the default context. It would be the same as calling:

GistApi.default.rush request

The rush method can be called on every context, it will be router to the underlaying Api.

Queueing requests for parallel execution

Using the commute method you can queue requests for later execution. It simply works like this (Let's add some coolness for fun):

# Define a callback.
callback = Proc.new { |response, gists|
  puts gists.count
}
# Build and queue some requests.
['defunkt', 'challengee'].map { |user|
  GistApi.commute GistApi.for_user(user: user).build
}
# Execute all requests in parallel
GistApi.rush
# => 10
# => 0

Error handling

Commute does not provide any help with handling errors. This is done purely through the underlaying HTTP client that is used (here Typhoeus).

In the callback one can for example check if the requests has timed out by issueing response.timed_out?.

What's next?

  • I would like an "adapter" system so that Typhoeus would just be an adapter.
  • Adding support for em-http-request.
  • Caching support (As a special adapter?).