Giter VIP home page Giter VIP logo

hal-client's Introduction

Build Status Code Climate Gem Version

HalClient

An easy to use client interface for REST APIs that use HAL.

Usage

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::Representations 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"}

Link navigation

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 RepresentationSets when following links, even when there is only one result. RepresentationSets are Enumerable so they expose all your favorite methods like #each, #map, #any?, etc. RepresentationSets 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"

Request evaluation order

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.

CURIEs

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.

Templated links

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.

Identity

All HalClient::Representations 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/"

Hash like interface

Representations 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:...>

Paged collections

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.

PUT/POST/PATCH requests

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.

PUT requests

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.

Editing representation

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.

Forms

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!"
)

Custom media types

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")

Parsing representations on the server side

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::Representations of both embedded and linked resources.

Installation

Add this line to your application's Gemfile:

gem 'hal-client'

And then execute:

$ bundle

Or install it yourself as:

$ gem install hal-client

Upgrading from 5.x to 6.x

  • 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 a HalClient::StaleRepresentationError.
  • All URIs are represented as Addressable::URI.
  • HalClient::Representation#as_enum removed. Use HalClient::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

Upgrading from 4.x to 5.x

HalClient::RepresentationEditor#add_link now raises if passed nil or empty values. This is the only breaking change.

Upgrading from 3.x to 4.x

Support for ruby 2.0 has been removed. Please upgrade to ruby 2.1 or later. No other breaking changes were made.

Upgrading from 2.x to 3.x

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.

Upgrading from 1.x to 2.x

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.

Contributing

  1. Fork it ( http://github.com/pezra/hal-client/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Implement your improvement
  4. Update lib/hal_client/version.rb following semantic versioning rules
  5. Commit your changes (git commit -am 'Add some feature')
  6. Push to the branch (git push origin my-new-feature)
  7. Create new Pull Request

hal-client's People

Contributors

adherr avatar glassbead0 avatar ivoanjo avatar jqmtor avatar lmarlow avatar pezra avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

hal-client's Issues

Allow new hash syntax in Representation and Interpreter

Hi there,

I stumbled upon this issue while developing some specs for my project. I wanted to mock a HalClient::Representation by providing the parsed_json option to the constructor.

If the parsed_json hash keys are symbols instead of strings, things don't work quite like they should.

I'll leave a sample interaction for you to validate if this is something you'd like to see patched or not. Let me know !

[6] pry(main)> json = {
                        :plan => {
                          :id => "fake_plan_id", 
                          :name =>"Awesome Name", 
                          :amount => 0
                        },
                        :counter => 250
                      }
=> {:plan=>{:id=>"fake_plan_id", :name=>"Awesome Name", :amount=>0}, :counter=>250}
[7] pry(main)> resp = HalClient::Representation.new(parsed_json: json)
=> #<HalClient::Representation:0x75d402d4 @hal_client=nil, @href=nil, @raw={:plan=>{:id=>"fake_plan_id", :name=>"Awesome Name", :amount=>0}, :counter=>250}>
[8] pry(main)> resp.property(:plan)
KeyError: key not found: :plan
[9] pry(main)> resp.property('plan')
KeyError: key not found: :plan
[10] pry(main)> json = {
                          'plan' => {
                            'id' => "fake_plan_id", 
                            'name' => "Awesome Name", 
                            'amount' => 0
                          }, 
                          'counter' => 250
                        }
=> {"plan"=>{"id"=>"fake_plan_id", "name"=>"Awesome Name", "amount"=>0}, "counter"=>250}
[11] pry(main)> resp = HalClient::Representation.new(parsed_json: json)
=> #<HalClient::Representation:0x3d42b756 @hal_client=nil, @href=nil, @raw={"plan"=>{"id"=>"fake_plan_id", "name"=>"Awesome Name", "amount"=>0}, "counter"=>250}>
[12] pry(main)> resp.property('plan')
=> {"id"=>"fake_plan_id", "name"=>"Awesome Name", "amount"=>0}
[13] pry(main)> resp.property(:plan)
=> {"id"=>"fake_plan_id", "name"=>"Awesome Name", "amount"=>0}

How to perform non-GET request to related resource

Is there a way to perform a non-GET request to a related resource (with an interface similar to #related)? I realize I can call #related and then call #post in the RepresentationSet, but I wanted to avoid the first GET request altogether.

Maybe I am missing something in the documentation but I've been struggling a bit with this.

Navigate by related links using POST method

I am facing a situation where I would need to navigate by related resources using POST method instead of GET method.

I have this scenario:

client = HalClient.new
items = client.get('http://myhalapi.com')
              .related('offers', args)
              .related('items')

And need to use something like that:

client = HalClient.new
items = client.get('http://myhalapi.com')
              .related('offers', args, method: :post) # POST
              .related('items') # GET

I am sending a lot of information in query string in offers GET resource, specifically a big list of items in JSON format. For this amount of data is reasonable to make a POST request send the data in body.

Do you think is it possible to implement this feature or another solution to solve this problem?

No way access the response headers

For example, the application im accessing with this gem, has an application version rendered in custom http header. And it seems, there's no way to access it as response headers are not stored in the representation.

Missing Examples

Do someone have a bunch of examples on how to use the gem?

I'm more used to the dev technique of CPCU (Copy Paste Change and Use) :-)

I'm trying to use the HalClient to access a API, but I have no luck using it, that what I did so far:

# load the SSL cert and key
cert = OpenSSL::X509::Certificate.new File.read(cert.pem')
key = OpenSSL::PKey::RSA.new File.read('key.pem')
# client instance
client = HTTP::Client.new({ ssl: {key: key,cert: cert,verify_mode: 0}})
hal=HalClient.new(base_client: client)

# Now point to Subscribers resource
subscribers = hal.get('https://api.server.tld:1443/api/subscribers')

Till that point, everything perfect ... but now, how do I iterate over the elements of the resouce collection, the reply of the API server have and _embedded section with the first 10 entries of the collection with all their attributes, only one attribute "total_count", and a links sections like this:

 "_links"=>
  {"curies"=>{"href"=>"http://purl.org/sipwise/ngcp-api/#rel-{rel}", "name"=>"ngcp", "templated"=>true},
   "next"=>{"href"=>"/api/subscribers/?page=2&rows=10"},
   "ngcp:subscribers"=>
    [{"href"=>"/api/subscribers/52"},
     {"href"=>"/api/subscribers/55"},
     {"href"=>"/api/subscribers/59"},
     {"href"=>"/api/subscribers/63"},
     {"href"=>"/api/subscribers/67"},
     {"href"=>"/api/subscribers/71"},
     {"href"=>"/api/subscribers/75"},
     {"href"=>"/api/subscribers/79"},
     {"href"=>"/api/subscribers/83"},
     {"href"=>"/api/subscribers/87"}],
   "profile"=>{"href"=>"http://purl.org/sipwise/ngcp-api/"},
   "self"=>{"href"=>"/api/subscribers/?page=1&rows=10"}},

I want to be able to do something like:

subscribers.each do |sub| puts sub.uuid end

But I get:

NoMethodError: undefined method each' for #HalClient::Representation:0x0055abaeb107d8`

So, some examples would help a lot.

Support for absolute path links (without hostname?)

From the hal spec:

   {
     "_links": {
       "self": { "href": "/orders/523" },
       "warehouse": { "href": "/warehouse/56" },
       "invoice": { "href": "/invoices/873" }
     },
     "currency": "USD",
     "status": "shipped",
     "total": 10.20
   }

On similar representations, I'm getting unknown scheme errors when following links.

hal-client never reuses client instances and always creates new connections

I was profiling a JRuby app which makes heavy use of hal-client and noticed that a lot of time was spent creating connections. That seemed rather strange to me as we initialize hal_client with a :base_client using an HTTP::Client instance configured for persistent connections.

Debugging it a bit led me to the client_for_post and client_for_get functions:

  def client_for_get(options={})
    override_headers = options[:override_headers]

    if !override_headers
      @client_for_get ||= base_client.with_headers(default_message_request_headers)
    else
      client_for_get.with_headers(override_headers)
    end
  end

In both functions, a new client is created if custom headers are passed, instead of the existing one being used. But I wasn't passing any custom headers in. But if you look at the caller:

  def get(url, headers={})
    headers = auth_headers(url).merge(headers)
    client = client_for_get(override_headers: headers)
    # ...
  end

with auth_headers being

  def auth_headers(url)
    if h_val = auth_helper.call(url)
      {"Authorization" => h_val}
    else
      {}
    end
  end

This means that there's absolutely no way for headers to ever be nil. It will always at least be an empty hash. And thus, the base client is always ignored and a new client is created every time.

`item` relation required by Collection?

Hello,

I'm new to HAL and Hal-Client. I noticed that the Collection class will only pass an element of the collection to the each block if it has an item relation. This was preventing the element from being sent to the block in an API that I was trying to use. When I changed Collection to look for self instead of item I was able to iterate through the collection as I expected to be able to.

Looking at the spec I can't find any mention of item, though self is mentioned.

Have I run into an edge case with a broken HAL API or is this a problem that others have experienced?

Very much a newb here, so any clarification would be appreciated.

SSL URLs

How to use the HalClient gem with ssl urls? ... does someone have an example of that?.

I've tryed to pass a options: {ssl: {client_ke: key, client_cert:cert,verify:false}} to the get method ... but no luck.

CURIES are misunderstood?

According to the HAL specification, CURIES are for the documentation
http://stateless.co/hal_specification.html
while hal-client uses them as link identifiers
Isn't there a misconcept implemented in hal-client?
My hal api exposes links with curies like:

"_links": {
  "curies": [
    {
      "name": "doc",
      "href": "http://haltalk.herokuapp.com/docs/{rel}",
      "templated": true
    }
  ],

  "doc:latest-posts": {
    "href": "/posts/latest"
  }
}

so i want to be able to access the latest-posts links via doc:latest-posts and not via the full documentation URI, which makes the client code really strange

How does it work?

I have a /api with:

{
  "_links": {
    "self": {
      "href": "/api"
    },
    "curies": [
      {
        "name": "doc",
        "href": "/#{rel}",
        "templated": true
      }
    ],
    "doc:site": {
      "href": "/api/site"
    }
  }
}

When I call rep.related('doc:site') I get KeyError: No resources are related via 'doc:site', what am I doing wrong?

Providing public Hash representation

Hey there,

What are your feelings about exposing the internal representation of a resource as a Hash (in Representation and RepresentationSet)? Or exposing only the properties bit of the representation as a Hash?

I am using hal-client to navigate through a JSON HAL API and build a component that abstracts away the consumption of such API. In order to return a business meaningful object to the clients of my component I need to access the properties of the HAL representation to create that object.

I understand that exposing the whole representation would be too much, but maybe it would make sense to expose only the properties as a Hash?

I see two ways of doing this:

  • Create a to_h or to_hash method in representation that strips everything but the properties from raw and returns that Hash;
  • Make the Representation object injectable, allowing users to provide their own implementation and possibly exposing the aforementioned functionality. This one would probably be a little tricky, since it would probably require some effort to identify all the things Representation (and probably RepresentationSet would need to implement) and establish a well-defined interface prepared for extension.

I can help if you think some of this makes sense.

Thanks,
Joaquim

Add ability to opt out of raising errors on non 2xx responses

In our API we occasionally encounter dead links. I would like a version of Representation#related that does not raise when I ask it to reify a dead link. What it should return is interesting.

We return HAL bodies with most errors, and I would like access to that and possibly the response code. Maybe an ErrorRepresentation that acts like a real Representation, giving reasonable responses to the public methods, but that includes details of the error?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.