-
Notifications
You must be signed in to change notification settings - Fork 33
Information on the API
##History coming soon
The API that Sequencescape is moving towards is intended to be fully RESTful: not only is it CRUD based through the HTTP POST, GET, PUT and DELETE verbs, it also returns URLs in the JSON responses that client applications must used. URLs are referenced through actions and these are standardised in the majority of cases, and documented where they differ. For instance:
- an HTTP GET of a read action URL will return the JSON for that resource
- a create action means that an HTTP POST to the URL for that action will create something
- an update action means an HTTP PUT to the URL will update the resource
- delete implies an HTTP DELETE will delete the resource at the URL
There are a number of additional actions that have predefined behaviour and are only present on list results, that are paged by the API to improve performance. They are:
- first which is the URL for the first page in the results list
- previous is the URL for the previous page in the results (if one exists)
- next is the next page of results (if one exists)
- last is obviously the last page of results
All of these respond to the HTTP GET verb.
The documentation in this wiki will often refer to these actions and it is important that the reader understands that the URLs that may be presented here are not necessarily those that will appear in a production environment: code to use the actions in the JSON, rather than generating URLs based on what you may read here.
There are only two exceptions to this rule:
- The root URL for the API is
/api/1/
and an HTTP GET will return a JSON body that tells the client application what it can do, through other actions. - Any accessible resource is identifiable through a UUID. If the resource has a UUID of
11111111-2222-3333-4444-555555555555
then the API guarantees that an HTTP GET of/api/1/11111111-2222-3333-4444-555555555555
will respond the with the JSON for that resource.
In other words: a client can discover everything that it is allowed access to, along with everything it can actually do, by knowing only the root URL.
The code for the API is built upon Rails Metal, specifically it is built upon a Sinatra application running inside Rails. All of the code for the API can be found under app/api and it is broken up as follows:
- app/api/core is the core of the API code.
- app/api/io contains classes for converting a model instance to and from JSON.
- app/api/endpoints holds the code that deals with the client interactions.
- app/api/model_extensions is currently being used to contain modules that are included into models to support the API requirements.
In general developers will only need to worry about app/api/io
and app/api/endpoints
. The code in api/api/model_extensions
will slowly disappear over time as much of the core of Sequencescape is refactored.
To aid in rapid development a generator is included that can be used to create endpoints and I/O classes for models. It can be executed thus:
./script/generate api <model> [create] [update]
If create
is passed as an option then the ability to create resources of the specified model will be added; equally update
will cause update code to be added to the endpoint. Note that both of these assume that the model behaves in the standard manner: create
assumes it responds to create!
, update
will use update_attributes!
.
The generator will automatically populate the I/O class with a mapping for attributes of the model, and the associations will be included in the generated endpoint code. Developers are advised to review this generated code and remove any of it that is unnecessary.
Developers are strongly advised not to provide create and update functionality unless it is absolutely necessary. The API is designed to be extremely locked down and care should be taken in what destructive actions clients can do.
Also, please note that although you can generate API endpoints and I/O classes for any model there is a question of whether you should. Try to look at the context something is happening in to see whether the API interface should be on a higher level model, rather than some of the low level ones. In particular, the following are specifically excluded from being exposed in the API:
- Join models, like AssetLink, as these are typically incidental and of absolutely no use to end-users. The advice is to look as to whether these models carry information and if that should be presented as part of the JSON directly.
-
Well, which is really a location on a Plate. Reference to
well
in the JSON should be avoided wherever possible as this model is due to be hidden from the JSON altogether (yes, it does have an API endpoint at the moment but this is going to be removed!).
Classes under app/api/io are responsible for marshaling a resource to and from JSON. They are separate from the model implementation only because the relevant ActiveRecord models methods are in use for a legacy API provided by sequencescape.
There are two "sections" to an I/O class:
- A number of setup method calls that define what model is being dealt with and how to load it appropriately for marshaling
- A description of the model to JSON attribute mappings
Probably the most unfamiliar looking bit of this is the mapping of attributes through the define_attribute_and_json_mapping
method call. If properly formatted a typical line in this text block looks like this:
model_attribute operator json_attribute
The operator
describes the mapping:
-
=>
means that the specified model attribute is output as the corresponding JSON attribute -
<=
means the model attribute is input from the JSON attribute -
<=>
means that the model attribute can be output to, and input from, the JSON attribute
Developers are strongly advised to use only the output operator (=>
) as, in most cases, the resources will be read-only.
The "proper formatting" for this is such that the =
of all operators is aligned, that the model_attribute
is right aligned, and the json_attribute
is left aligned. It makes it easier to scan the output and input fields which is more common that identifying what attributes are being used. A good example of proper formatting can be found in app/api/io/request.rb.
It is rarely necessary, but sometimes imposed by the model, to map one JSON attribute to different model attributes on output and input. This can be achieved with the following mappings:
output_model_attribute => json_attribute
input_model_attribute <= json_attribute
In effect the JSON attribute json_attribute
will be generated from the contents of output_model_attribute
and will be used to assign values to input_model_attribute
.
Both the model_attribute
and json_attribute
can be "dotted"; in other words a mapping of some_association.field => container.key
means that the JSON would look like this:
{
"container": {
"key": ...value from some_association.field call on the resource ...
}
}
Output of associations is typically handled by the endpoint for a model.
Associations of a model that can be updated from the JSON must be specified in the I/O attribute mappings as the core of the API assumes all resources are read-only. However, output associations are handled by the endpoint for the model unless they are to be inlined within the JSON, in which case specify them as output attributes. As an example, if you had a model defined like this:
class MyModel < ActiveRecord::Base
has_many :others
has_many :of_them_too
end
And the following I/O mapping:
others <= json.others
of_them_too => json.of_them_too
Then the JSON attribute json.others
is assumed to be an array of UUIDs that are mapped to models that exist and are assigned to the others
association. However, the others
association may not be present in the JSON as that is defined by the endpoint.
The of_them_too
association is output as inlined records in the JSON element json.of_them_too
and cannot be updated.
Just as Rails has controllers, the API has endpoints: when a client application is interacting with a resource it does so through an endpoint, implemented under app/api/endpoints. An individual endpoint is separated into two sections:
-
model
describes the actions that are available on the collection of resources. For example, acreate
action will connect the client to this side of the endpoint. -
instance
is responsible for the associations and actions of a specified resource. For example, anupdate
action will be in this side of the endpoint.
Please note that all endpoints exist under the Endpoints
namespace and are pluralized versions of them model they are related to; for example, the endpoint for the Dog
model is called Endpoints::Dogs
. A minimal implementation of this endpoint, providing read-only capabilities, would be:
class Endpoints::Dogs < Core::Endpoint::Base
model do
end
instance do
end
end
Hierarchies are allowed in endpoints and the API will choose the most appropriate version for the resource being loaded. It is typically not necessary to provide a hierarchy for models that are themselves hierarchical as, in doing so, it suggests that there is behaviour that is not common between them and that the model structure itself might be incorrect.
Associations are simply specified using the has_many
and belongs_to
definitions. For example:
class ::Endpoints::Dogs < Core::Endpoint::Base
instance do
has_many :fleas, :json => 'fleas', :to => 'fleas'
belongs_to :owner, :json => 'owner'
end
end
By specifying these two associations the JSON output for an individual Dog
will contain URL references that can be followed to retrieve the records. For instance, the read
action on the owner
element will return the JSON for the owner. The :to
parameter to has_many
defines the unique identifier in the URL for this action; if a Dog
instance has the UUID 11111111-2222-3333-4444-555555555555
then the URL generated by the core of the API would be http://host.name/api/1/11111111-2222-3333-4444-555555555555/fleas
(although remember that this is not guaranteed).
An update action can be added to this by:
class ::Endpoints::Dogs < Core::Endpoint::Base
instance do
action(:update, :to => :standard_update!)
end
end
The standard_update!
is a helper method that will perform an update_attributes!
call, inside a transaction, based on the attributes marshaled by the I/O class.
Similarly there is a standard_create!
helper too:
class ::Endpoints::Dogs < Core::Endpoint::Base
model do
action(:create, :to => :standard_create!)
end
end
Notice that this is done on model
, not instance
.
Developers are strongly encouraged to use this format for updates and creates, and to use the Rails callbacks and accepts_nested_attributes_for
to get their desired behaviour. Straying from this pattern will mean that more code will be written in the endpoint and, as you'll quickly discover, it does not feel very nice. The API is extremely opinionated in this and will not change. Simply do not write lots of code in your endpoints.
On the occasions where non-standard behaviour is required a block can be passed to the action
method instead of the :to
parameter, thus:
class ::Endpoints::Dogs < Core::Endpoint::Base
instance do
action(:update) do |request, response|
# non-standard code goes here
end
end
end
It is the responsibility of the code block to maintain transactions, set the HTTP response code through response#status
, and do anything that would normally be advisable in an action. The target object, be that the model or an instance of a model, can be accessed through request.target
and whatever is returned from the block will be marshaled to JSON by the core of the API. The unmarshaled attributes are accessible through request.instance_attributes
.
Providing update or create actions on an association is a matter of using action
within the association definition:
class ::Endpoints::Dogs < Core::Endpoint::Base
instance do
has_many :fleas, :json => 'fleas', :to => 'fleas' do
action(:create, :standard_create!)
end
end
end
Although this is not really relevant to developing server side code on the API, the format of the JSON is fairly self explanatory. The typical structure looks like this:
{
"root_element": {
"actions": {
"read": "http://...../",
"update": "http://...../"
},
"association": {
"actions": {
"read": "http://...../",
"update": "http://...../",
"create": "http://...../"
}
},
"inline_association": [
{
"actions": {
... actions here ...
},
... attributes here ...
},
... more records here ...
]
"attribute": "value"
}
}
From this you can see that the actions that this documentation has been talking about are contained within an actions
sections of the JSON. Clients should only use the actions presented in the JSON for a resource and attempting to do something not specifically listed will result in an error.
It is important to note that inline associations do not wrap their contained records in their root name. It is assumed that the association name in the JSON gives enough information for the client to interpret what the records are, rather than repeating information and nesting the JSON structure one level further.