Skip to content

Extending the Ruby client library

mattdenner edited this page Mar 14, 2011 · 3 revisions

There are many cases where extending the client library code is necessary, typically when the client application requires additional functionality beyond what is provided by the Sequencescape namespace.

Adding new types of resource

There are several different types of resource that come as part of the client library and it is unlikely that anyone will need to add more unless they are part of the Sequencescape development team.

All types of resource should extend Sequencescape::Api::Resource, or another class derived from it. This provides the basic functionality of a resource, including various helper methods for declaring the class itself. Therefore, the most simple implementation of a new resource type is:

class Sequencescape::MyResource < Sequencescape::Api::Resource
end

Should you need to do anything in the initialize method you are advised to make its signature initialize(*args), as you will not be able to guarantee what values are passed, and to call super first:

class Sequencescape::MyResource < Sequencescape::Api::Resource
  def initialize(*args)
    super
    # Do something here
  end
end

If a resource can be created or updated then the convenience methods of has_create_action and has_update_action come into play:

class Sequencescape::MyResource < Sequencescape::Api::Resource
  has_create_action   # Allows you to call Sequencescape::MyResource.create!
  has_update_action   # Provides the Sequencescape::MyResource#update_attributes! ability
end

Any attributes that are to be made accessible to a client application should be declared using attribute_accessor, which behaves as attr_accessor does except that it maintains information for the JSON sent during updates. For example, if the new resource has a name then:

class Sequencescape::MyResource < Sequencescape::Api::Resource
  attribute_accessor :name
end

In combination we would then be able to write:

api = Sequencescape::Api.new(:url => 'http://.../api/1/', :cookie => '...')
resource = api.my_resource.create!(:name => 'Original name')
resource.update_attributes!(:name => 'New name')

Assuming that the Sequencescape API supported all of these actions.

Declaring associations is a matter of choosing has_many, for a one-to-many relationship, or belongs_to, for a one-to-one. Note that there is no has_one: it is always belongs_to. You can specify the class name to construct for the association using the :class_name option, or leave it off if the name can be inferred:

class Sequencescape::MyResource < Sequencescape::Api::Resource
  has_many :my_resources                                     # Will infer 'MyResource'
  belongs_to :another_resource, :class_name => 'MyResource'  # Will use 'MyResource' in the configured namespace
end

has_many also supports a :disposition option which can be :inline, meaning that the JSON contains an array of elements rather than referencing another action that would list the elements. The difference is subtle but important: where a normal one-to-many association appears in the JSON like this:

{
  "my_resource": {
    "my_resources": {
      "actions": {
        "read": "http://..../api/1/XXY/foo"
      }
    }
  }
}

An inline one would appear like this:

{
  "my_resource": {
    "my_resources": [
      { ... JSON for resource 1 ... },
      { ... JSON for resource 2 ... }
    ]
  }
}

If you want to be able to create instances of resources through an association you can use has_create_action in the declaration:

class Sequencescape::MyResource < Sequencescape::Api::Resource
  has_many :my_resources do
    has_create_action
  end
end

If you need to provide custom actions on an association, as Sequencescape::PlatePurpose does with its plates association, you are strongly encouraged to separate out the methods into a module and include that in the association. For instance:

class Sequencescape::MyResource < Sequencescape::Api::Resource
  module ExtraActions

  end

  has_many :my_resources do
    include ExtraActions
  end
end

More on this in the section on interacting with the API connection.

Extending the behaviour of an existing resource type

This is, surprisingly, a fair common occurrence not because the API is lacking the functionality, but because the client application has a much narrower perspective. The client library is intended to be highly generic and will only ever contain generalised functionality, applicable to all applications; it is the responsibility of client applications to refine and add to this behaviour for their purpose.

You are advised to avoid adding behaviour directly to the classes in the Sequencescape namespace as these are shared across all other namespaces. In other words, if you wanted to add a method to the sample model you do not do:

class Sequencescape::Sample
  def my_new_method
  end
end

Instead you should be extending the Sequencescape::Sample class in your own namespace and using that namespace when constructing the Sequencescape::Api instance. It is better to do:

class MyNamespace::Sample < Sequencescape::Sample
  def my_new_method
  end
end

api = Sequencescape::Api.new(:namespace => MyNamespace, ...)

All instances of the sample model, even those that are part of associations, will now be instances of MyNamespace::Sample.

Interacting with the API connection

This is extremely low-level and of no interest to the casual user of the library; if you are using this then you are either (a) doing something wrong, or (b) me.

An instance of Sequencescape::Api is really a wrapper around a Sequencescape::Api::Connection, which provides low level methods for creating, reading, updating and deleting (actually not deleting, but the others) resources. Each of the methods that handle these actions takes an object that can deal with the outcome of the request, whether that is a success, a redirect, an error, etc. The handler object must implement the following methods:

  • success(json) is called on a successful operation, handing the JSON to the handler for interpretation.
  • missing(json) implies that a resource could not be found, whether that is the one requested or one referenced.
  • unauthenticated(json) is called if the request was an authentication issue.
  • failure(json) covers all other (unpredicted) failure reasons.
  • redirection(json, &block) is called if the client is being redirected.

The majority of these have fairly self-evident semantics from the perspective of the operation you are trying to perform; the only one that looks slightly awkward is redirection. In the case of the API responding with a redirection the operation code can decide to follow that (note that the HTTP Multiple Choices response is also a redirection) by yielding another handler (or itself) to given block; or it can simply use the JSON obtained from the redirection body, and choose not to yield.

This may be easier to understand if we examine the Sequencescape::Search implementation. Firstly it should be noted that line 47 makes a create request to the API, passing the search criteria and using an instance of the SingleResultHandler class to deal with the response. That class has a fairly bare implementation (lines 15-24):

class SingleResultHandler < BaseHandler
  def initialize(api)
    @api = api
  end
  def redirection(json, &block)
    json.delete('uuids_to_ids')
    Sequencescape::Api::FinderMethods::FindByUuidHandler.new(@api.send(json.keys.first)).success(json)
  end
end

The implementation of the search model in this case actually treats all responses, except for a redirection, as an error. In the case of a redirection (defined by the search specification in this case to be a 301 Moved Permanently) the body of the response is the JSON for the resource, which this code then interprets as a success response for a find by UUID. It could be split into a success and a redirection handler:

def success(json)
  json.delete('uuid_to_ids')
  @api.send(json.keys.first).new(@api, json, true)
end
def redirection(json, &block)
  yield(self)
end

Although this introduces unnecessary network request-response cycles.

It should be noted that the implementation of the all method (lines 58-60), and the associated handler definition (lines 28-41), deal with the case where the search is for more than one result. In this case the response is 300 Multiple Choices, and so there is no possibility to follow the redirection and the results are returned directly.

The methods provided by Sequencescape::Api, and accessible through the api attribute reader in most cases, are:

  • read(url, handler)
  • create(url, body, handler)
  • update(url, body, handler)

Note that in the case of create and update the body will be converted to JSON by calling the as_json(options) method and assuming that the returned result can be passed to the Yajl::Encoder. There are several options that can be passed to control the conversion:

  • :action will be either :read, :create, :update.
  • :force will be true if a full JSON output is being force, otherwise it is acceptable to only return changes.
  • :root will be true if this is the root object in the conversion, otherwise false.

In the case of :force being true, or the :action being :create, the JSON should include all necessary details. In other cases the JSON can be reduced to only the changes that are require to be passed across, nil being assumed to be ignoreable. In most cases implementers do not care about this as it's handled by framework, this is only for reference if you have to step outside of this.

Testing

(coming soon)

Clone this wiki locally