Farscape
Farscape is a hypermedia agent that simplifies consuming Hypermedia API responses. It shoots through wormholes with Crichton at the helm and takes you to unknown places in the universe!
Checkout the Documentation for more info.
NOTE: THIS IS UNDER HEAVY DEV AND IS NOT READY TO BE USED YET
API Entry
There are various flavors of configuration that Farscape supports for entering a Hypermedia API. These all assume a response with a supported Hypermedia media-type and a root that lists available resources as links.
A Hypermedia API
For a interacting with an API (or individual service that supports a list of resources at its root), you enter the
API and follow your nose using the enter
method on the agent. This method returns a Farscape::Representor
instance with a simple state-machine interface of attributes
(data) and transitions
(link/form affordances) for
interacting with the resource representations.
agent = Farscape::Agent.instance
resources = agent.enter('http://example.com/my_api')
resources.attributes # => { meta: 'data', or: 'other data' }
resources.transitions.keys # => ['http://example.com/rel/drds', 'http://example.com/rel/leviathans']
A Hypermedia Discovery Service
For interacting with a discovery service, you can use enter and follow your nose entry to select a registered resource or setup Farscape with a discovery server.
# Setting discovery service to https://my_discovery_api
Farscape::Agent.config = { Farscape::Discovery::DISCOVERY_KEY => 'https://my_discovery_api' }
The discovery service must return a document with a list of resource names and their root URLs like:
{
"_links": {
"self": { "href": "https://my_discovery_api" },
"boxes": { "href": "https://smallboxesandpoliceboxes.com" },
"items": { "href": "https://sonicscrewdriversandotherthings.com/v1/{item}" }
}
}
Farscape then can be used directly with resource names. Farscape will already do the heavy-lifting of contacting the discovery service and retrieving the root document of the resource.
agent = Farscape::Agent.instance
boxes = agent.enter('boxes')
boxes.attributes # => { total_count: 13, items: [...] }
agent.enter('unknownresource') # raises Farscape::Discovery::NotFound
agent.enter('items', [{ items: 'bow-tie' }]) # Allows template variables
API Interaction
Entering an API takes you into its application state-machine and, as such, the interface for interacting with that application state is brain dead simple with Farscape. You have data that you read and hypermedia affordances that tell you what you can do next and you can invoke those affordances to do things. That's it.
Farscape recognizes a number of media-types that support runtime knowledge of the underlying REST uniform-interface methods. For these full-featured media-types, the interaction with with resources is as simple as a browser where implementation of requests is completely abstracted from the user.
The following simple examples highlight interacting with resource state-machines using Farscape.
Load a resource
resources = agent.enter
drds_transition = resources.transitions['http://example.com/rel/drds']
drds = drds_transition.invoke
Reload a resource
self_transition = drds.transitions['self']
reloaded_drds = self_transition.invoke
Explore
The sample code given below often depicts the client making assumptions that a specific transition or attribute will be available in a certain state. This is unsafe, and production code should include conditionals or rescues for the case when an assumption proves incorrect. Whenever possible, Farscape should be used more dynamically, by letting user interaction or a crawling algorithm drive transitions.
Apply query parameters
search_transition = drds.transitions['search']
search_transition.parameters # => ['search_term']
filtered_drds = search_transition.invoke do |builder|
builder.parameters = { search_term: '1812' }
end
You may also invoke transitions with automatic attribute and parameter matching
drds.transitions['search'].invoke(search_term: '1812')
Transform resource state
embedded_drd_items = drds.items
drd = embedded_drd_items.first
drd.attributes # => { name: '1812' }
drd.transitions # => ['self', 'edit', 'delete', 'deactivate', 'leviathan']
deactivate_transition = drd.transitions['deactivate']
deactivated_drd = deactivate_transition.invoke
deactivated_drd.attributes # => { name: '1812' }
deactivated_drd.transitions # => ['self', 'activate', 'leviathan']
deactivate_transition.invoke # => raise Farscape::Excpetions::Gone error
Transform application state
leviathan_transition = deactivated_drd.transitions['leviathan']
leviathan = leviathan_transition.invoke
leviathan.attributes # => { name: 'Elack' }
leviathan.transitions # => ['self', 'drds']
Use attributes
create_transition = drds.transitions['create']
create_transition.attributes # => ['name']
new_drd = create_transition.invoke do |builder|
builder.attributes = { name: 'Pike' }
end
new_drd.attributes # => { name: 'Pike' }
new_drd.transitions.keys # => ['self', 'edit', 'delete', 'deactivate', 'leviathan']
For more examples and information on using Faraday with media-types that require specifying uniform-interface methods and other protocol idioms when invoking transitions, see Using Farscape.
Alternate Interface
For developers more used to ActiveRecord syntax, Farscape resources also expose all transitions and attributes as Ruby methods. Safe (i.e. read) transitions are exposed verbatim.
drd.leviathan # => Equivalent to drd.transitions['leviathan'].invoke
Unsafe transitions have an exclamation point at the end.
drd.deactivate # => Raises NoMethodError
drd.deactivate! # => Equivalent to drd = drd.transitions['deactivate'].invoke
Request parameters can be passed as a hash or as a block.
# The following are all equivalent:
drd = drds.create!(name: 'Pike')
drd = drds.create! { |builder| builder.attributes = {name: 'Pike'} }
drd = drds.transitions['create'].invoke{ |d| d.attributes = {name: 'Pike'} }
Attributes are read-only.
drd.name # => "Pike"
drd.name = 'Susan' # => Raises NoMethodError
If an attribute or transition's name conflicts with an existing method or reserved word, it will not be methodized and must be accessed through the hash interface.
Disabling the Alternate Interface
If you're concerned about namespace collisions, or want to ensure that your code is highly flexible and explicit (albeit verbose), you may turn off the interface with the .safe method.
safe_drd = drd.safe # => returns a drd resource without the alternate interface
safe_drd.name # => UndefinedMethod error
drd.name # => "Pike"
You may reenable the alternate interface with .unsafe
.
Contributing
See CONTRIBUTING for details.
Copyright
Copyright (c) 2013 Medidata Solutions Worldwide. See LICENSE for details.