An easy to use client interface for REST APIs that use HAL.
The first step in using a HAL based API is getting a representation of one of its entry points. The simplest way to do this is using the get
class method of HalClient
.
blog = HalClient.get("http://blog.me/")
# => #<Representation: http://blog.me/>
HalClient::Representation
s expose a #property
method to retrieve a single property from the HAL document.
blog.property('title')
#=> "Some Person's Blog"
They also expose a #properties
method to retrieve all the properties from the document, as a Hash
.
blog.properties
#=> {"title"=>"Some Person's Blog", "description"=>"Some description"}
Once we have a representation we will want to navigate its links. This can be accomplished using the #related
method.
articles = blog.related("item")
# => #<RepresentationSet:...>
In the example above item
is the link rel. The #related
method extracts embedded representations and link hrefs with the specified rel. The resulting representations are packaged into a HalClient::RepresentationSet
. HalClient
always returns RepresentationSet
s when following links, even when there is only one result. RepresentationSet
s are Enumerable
so they expose all your favorite methods like #each
, #map
, #any?
, etc. RepresentationSet
s expose a #related
method which calls #related
on each member of the set and then merges the results into a new representation set.
all_the_authors = blog.related("author").related("item")
all_the_authors.first.property("name")
# => "Bob Smith"
If the author
relationship was a regular link (that is, not embedded) in the above example the HTTP GET to retrieve Bob's representation from the server does not happen until the #property
method is called. This lazy dereferencing allows for working more efficiently with larger relationship sets.
Links specified using a compact URI (or CURIE) as the rel are fully supported. They are accessed using the fully expanded version of the curie. For example, given a representations of an author:
{ "name": "Bob Smith,
"_links": {
"so:homeLocation": { "href": "http://example.com/denver" },
"curies": [{ "name": "so", "href": "http://schema.org/{rel}", "templated": true }]
}}
Bob's home location can be retrieved with
author.related("http://schema.org/homeLocation")
# => #<Representation: http://example.com/denver>
Links are always accessed using the full link relation, rather than the CURIE, because the document producer can use any arbitrary string as the prefix. This means that clients must not make any assumptions regarding what prefix will be used because it might change over time or even between documents.
The #related
methods takes a Hash
as its second argument which is used to expand any templated links that are involved in the navigation.
old_articles = blog.related("index", before: "2013-02-03T12:30:00Z")
# => #<RepresentationSet:...>
Assuming there is a templated link with a before
variable this will result in a request being made to http://blog.me/archive?before=2013-02-03T12:30:00Z
, the response parsed into a HalClient::Representation
and that being wrapped in a representation set. Any options for which there is not a matching variable in the link's template will be ignored. Any links with that rel that are not templates will be dereferenced normally.
All HalClient::Representation
s exposed an #href
attribute which is its identity. The value is extracted from the self
link in the underlying HAL document.
blog.href # => "http://blog.me/"
Representation
s expose a Hash
like interface. Properties, and related representations can be retrieved using the #[]
and #fetch
method.
blog['title'] # => "Some Person's Blog"
blog['item'] # => #<RepresentationSet:...>
HalClient provides a high level abstraction for paged collections encoded using standard item
, next
and prev
link relations.
articles = blog.to_enum
articles.each do |an_article|
# do something with each article representation
end
If the collection is paged this will navigate to the next page after yielding all the items on the current page. The return is an Enumerable
so all your favorite collection methods are available.
HalClient supports PUT/POST/PATCH requests to remote resources via it's #put
, #post
and #patch
methods, respectively.
blog.put(update_article_as_hal_json_str)
#=> #<Representation: http://blog.me>
blog.post(new_article_as_hal_json_str)
#=> #<Representation: http://blog.me>
blog.patch(diffs_of_article_as_hal_json_str)
#=> #<Representation: http://blog.me>
The first argument to #put
, #post
and #patch
may be a String
, a Hash
or any object that responds to #to_hal
. Additional options may be passed to change the content type of the post, etc.
HalClient supports PUT requests to remote resources via it's #put
method.
blog.put(new_article_as_hal_json_str)
#=> #<Representation: http://blog.me>
The argument to post may be String
or any object that responds to #to_hal
. Additional options may be passed to change the content type of the post, etc.
HalClient supports editing of representations. This is useful when creating resources from a template or updating resources. For example, consider a resource whose "author" relationship we want to update to point the author's new profile page.
doc = HalClient.get("http://example.com/somedoc")
improved_doc =
HalClient::RepresentationEditor.new(doc) # create an editor
.reject_related("author") { |it| it.property("name") == "John Doe"} # unlink Johe Doe's old page
.add_link("author", "http://example.com/john-doe") # add link to his new page
doc.put(improved_doc) # save changes to server
This removes the obsolete link to "John Doe" from the documents list of authors and replaces it with the correct link then performs an HTTP PUT request with the updated document.
HalClient supports Dwolla HAL forms. For example, given a collection with a create-form
link to a resource with a default
form that creates new members of the collection, the following code would create a new member of http://example.com/somecollection
.
collection = HalClient.get("http://example.com/somecollection")
create_form = collection.related("create-form").form
create_form.submit(
name: "my item",
author_url: URI("http://example.com/john-doe")
description: "super duper!"
)
If the API uses one or more a custom mime types we can specify that they be included in the Accept
header field of each request.
my_client = HalClient.new(accept: "application/vnd.myapp+hal+json")
my_client.get("http://blog.me/")
# => #<Representation: http://blog.me/>
Similarly we can set the default Content-Type
for post requests.
my_client = HalClient.new(accept: "application/vnd.myapp+hal+json",
content_type: "application/vnd.myapp+hal+json")
HalClient can be used by servers of HAL APIs to interpret the bodies of requests. For example,
new_post_repr = HalClient::Representation.new(parsed_json: JSON.load(request.raw_post))
author = Author.by_href(new_post_repr.related('author').first.href)
new_post = Post.new title: new_post_repr['title'], author: author, #...
Created this way the representation will not dereference any links (because it doesn't have a HalClient) but it will provide HalClient::Representation
s of both embedded and linked resources.
Add this line to your application's Gemfile:
gem 'hal-client'
And then execute:
$ bundle
Or install it yourself as:
$ gem install hal-client
HalClient::Representation
become stale after unsafe requests. This means that after calling#put
,#post
, etc any future attempts to use the instance will fail with aHalClient::StaleRepresentationError
.- All URIs are represented as
Addressable::URI
. HalClient::Representation#as_enum
removed. UseHalClient::Representation#to_enum
HalClient#clone_for_use_in_different_thread
andHalClient::Representation#clone_for_use_in_different_thread
removed. They didn't actually work.- Drop support for Ruby versions < 2.3
HalClient::RepresentationEditor#add_link
now raises if passed nil or empty values. This is the only breaking change.
Support for ruby 2.0 has been removed. Please upgrade to ruby 2.1 or later. No other breaking changes were made.
For most uses no change to client code is required. At 3.0 the underlying HTTP library changed to https://rubygems.org/gems/http to better support our parallelism needs. This changes the interface of #get
and #post
on HalClient
and HalClient::Representation
in the situation where the response is not a valid HAL document. In those situations the return value is now a HTTP::Response
object, rather than a RestClient::Response
.
The signature of HalClient::Representation#new
changed such that keyword arguments are required. Any direct uses of that method must be changed. This is the only breaking change.
- Fork it ( http://github.com/pezra/hal-client/fork )
- Create your feature branch (
git checkout -b my-new-feature
) - Implement your improvement
- Update
lib/hal_client/version.rb
following semantic versioning rules - Commit your changes (
git commit -am 'Add some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create new Pull Request