Skip to content

Working with the Ruby client library

mattdenner edited this page Mar 11, 2011 · 3 revisions

The Ruby client library attempts to make interacting with the low-level Sequencescape API as simple and familiar as possible. It therefore follows many of the conventions of Rails' ActiveModel and ActiveRecord.

Connecting to the API

Connecting to the API is as simple as constructing an instance of the Sequencescape::Api class:

api = Sequencescape::Api.new(:url => 'http://..../api/1/', :cookie => 'value')

Clients can provide:

  • The root URL of the API through :url.
  • :cookie contains the institute enforced single sign-on cookie.
  • :authorisation contains the authorisation information for special applications (optional).
  • :namespace is the Ruby module/class from which to find model classes (see namespaces section below).

All interaction with the API is then performed through this instance and, as was mentioned in the authentication discussion in the low-level API page, it is bound to the authenticated user: if the client wants to perform an operation as another user (as a web application may want to do) a separate Sequencescape::Api instance should be constructed.

Retrieving a resource via its UUID

Retrieving a particular resource is as simple as finding it by its UUID:

object = api.sample.find('11111111-2222-3333-4444-555555555555')

Here we have looked up the resource through the sample model and stored it in the object variable. If the resource cannot be found then the call to find will raise a Sequencescape::Api::ResourceNotFound exception.

Retrieving the pages of a particular model

Although the low-level API uses pages of results for lists the Ruby client API attempts to hide much of this from the client. In other words, where the low-level API has pages the client can assume a normal Ruby Enumerable instance.

So, if the client wanted to print out all of the sample names:

api.sample.all.each do |sample|
  puts sample.name
end

The pages are therefore completely transparent to the client code. If, however, the client wanted to work with the pages the code could become:

api.sample.all.each_page do |samples_on_this_page|
  samples_on_this_page.each do |sample|
    puts sample.name
  end
end

All of the enumerable operations are available (each, map, inject, first, last, etc) and there are each_page, first_page, and last_page methods.

Updating a resource

Updating resources is a restricted operation, in most cases, but when it is available it follows the standard ActiveRecord pattern that Ruby developers are used to. The client application can either modify attributes of the resource individually and then save it:

sample.name = 'New name for sample'
sample.save!

Or it can perform a bulk update of attributes:

sample.update_attributes!(:name => 'New name for sample')

If there are issues with the update then the code will raise a Sequencescape::Api::ResourceInvalid and the errors of the resource will be filled in, ala ActiveRecord. In other words, client code could display all of the errors on the resource using code like this:

begin
  sample.update_attributes!(:name => 'New name for sample')
rescue Sequencescape::Api::ResourceInvalid => exception
  exception.resource.errors.full_messages.map(&$stderr.method(:puts))
  raise
end

Unlike in ActiveRecord, client code can update attribute of associations without having to fiddle with the attribute names. For example, if a sample is related to a study the client code could do:

sample.update_attributes!(:study => { :name => 'New study name' })

And the name of the study associated with the sample would be updated. Equally, updating the name of the study directly and calling save! on the sample would produce the same result.

Client applications are encouraged to avoid doing this too much as there is a potential for cyclic dependencies to cause problems.

If the client wanted to change the study for a sample completely the code would be:

study = api.study.find('11111111-2222-3333-4444-565656565656')
sample = api.sample.find('11111111-2222-3333-4444-555555555555')
sample.update_attributes!(:study => study)

Namespaces

By default the API will look for model classes in the Sequencescape namespace. In other words, if the API or client application requires the sample model (say through api.sample) then the Ruby class used is Sequencescape::Sample. The classes in this namespace are extremely low-level, providing much of the generic functionality, but applications sometimes need to extend this with their own behaviour. In this case the Sequencescape::Api instance can be constructed with a namespace:

api = Sequencescape::Api.new(:url => 'http://..../api/1/', :cookie => 'value', :namespace => MyApplication::Models)

Here the api object will use the MyApplication::Models namespace, falling back to Sequencescape if models do not exist. If the client application then wanted to extend the functionality of the sample model it could do:

class MyApplication::Models::Sample < Sequencescape::Sample
  def reversed_name
    self.name.reverse
  end
end

puts api.sample.find('11111111-2222-3333-4444-555555555555').reversed_name

It is also possible to override associations, which is extremely useful within the Sequencescape::Request model. A request represents a piece of work to be performed on a source asset to generate a target asset. Whilst the Sequencescape::Request class is defined as:

class Sequencescape::Request < Sequencescape::Api::Resource
  belongs_to :source_asset, :class_name => 'Sequencescape::Asset'
  belongs_to :target_asset, :class_name => 'Sequencescape::Asset'
end

It may be that the client application has more details on what type of asset they are, for example:

class MyApplication::Models::Request < Sequencescape::Request
  belongs_to :source_asset, :class_name => 'Sequencescape::SampleTube'
  belongs_to :target_asset, :class_name => 'Sequencescape::LibraryTube'
end

Note that extending models in this manner does not provide any ability to perform operations that the API deems unavailable to the client application. Don't think that by defining a create! method on your model you will be able to create instances of it through the API!

If your application is using a custom namespace you are advised to read the Extending the Ruby client library page thoroughly.

A full example

Here is some example code to create a submission of a number of assets, effectively asking the laboratories involved to perform several tasks.

# Stuff you should fill in ...
submission_template = ''  # The name of the submission template to use
study_name          = ''  # The name of the study to use
project_name        = ''  # The name of the project to use

# Details required by the API itself
API_URL              = ''  # The root URL of the API
AUTHENTICATION_TOKEN = ''  # The institute single sign-on value

# List of all the asset UUIDs you want to submit.
all_assets = %w( )

require 'sequencescape-api'
require 'sequencescape'

begin
  # You need to get your own personal instance of the client API to work with sequencescape ...
  api = Sequencescape::Api.new(:url => API_URL, :cookie => AUTHENTICATION_TOKEN)

  # Then you can lookup things from through it.  If you know the UUID of the things you want then use
  # 'api.model.find(UUID)' to retrieve it, otherwise you just treat the 'all' as an array and can use
  # select/detect/reject/map, etc.
  template = api.submission_template.all.detect { |template| template.name == submission_template } or raise StandardError, "Cannot find submission template #{submission_template.inspect}"
  project  = api.project.all.detect { |project| project.name == project_name } or raise StandardError, "Cannot find project #{project_name.inspect}"
  study    = api.study.all.detect { |study| study.name == study_name } or raise StandardError, "Cannot find study #{study_name}"

  # Submit all of the assets in groups of 8 ...
  all_assets.each_slice(8) do |asset_uuids|
    submission = template.submissions.create!(:project => project, :study => study).tap { |s| puts "Your submission has UUID #{s.uuid}" }
    submission.update_attributes!(:request_options => { :read_length => 4, :fragment_size => { :from => 100, :to => 200 }, :library_type => 'Standard', :number_of_lanes => 1 })
    submission.update_attributes!(:assets => asset_uuids)
    submission.submit!
  end
rescue Sequencescape::Api::ResourceInvalid => exception
  $stderr.puts "Resource invalid:"
  exception.resource.errors.full_messages.map(&$stderr.method(:puts))
  raise
end

Note that the two calls to update_attributes! on the submission instance result in communication with with the API, demonstrating that submissions can be built in stages and can be submitted through submit! when finally ready.

Although the lookup of the submission template, project and study is presented here as something that is awkward, it shows that the standard Ruby Enumerable methods are available to client applications. There are, in a version of the API coming soon, search models that would enable these lookups to become extremely simple:

project = api.search.find('11111111-2222-3333-4444-555555555555').first(:name => project_name)
study   = api.search.find('11111111-2222-3333-4444-666666666666').first(:name => study_name)

Relying on the client to only have to remember the UUIDs for the searches it requires.

Working with Rails

Web applications are typically going to need to obtain a new connection to the API for each user, rather than share a connection across multiple users, and so the Ruby client library provides a convenience mechanism for Rails. To obtain a connection to the API for each request you only need to make the following change to your ApplicationController:

class ApplicationController < ActionController::Base
  include Sequencescape::Api::Rails::ApplicationController
  def api_connection_options
    { :url => '', :namespace => ... }
  end
  private :api_connection_options
end

The include of Sequencescape::Api::Rails::ApplicationController automatically adds a before filter that will establish a Sequencescape::Api instance (accessible through api in controller actions) for every request. To control the connection details the before filter expects the api_connection_options instance method to return the parameters for the call to new (:cookie is not required as it will be pulled from the current request, unless it is specified here).

It is suggested that you move as much of this into the Rails configuration files as possible, reducing the minimum implementation to:

class ApplicationController < ActionController::Base
  include Sequencescape::Api::Rails::ApplicationController
  delegate :api_connection_options, :to => 'MyApplication::Application.config'
end

And meaning that the configuration can vary by environment and be driven something like this:

config.api_connection_options = ActiveSupport::OrderedOptions.new
config.api_connection_options.url = 'http://..../api/1/'

Clone this wiki locally