Giter Club home page Giter Club logo

hyperresource's Introduction

HyperResource Build Status

HyperResource is a Ruby client library for hypermedia web services.

HyperResource makes using a hypermedia API feel like calling plain old methods on plain old objects.

It is usable with no configuration other than API root endpoint, but also allows incoming data types to be extended with Ruby code.

HyperResource supports the HAL+JSON hypermedia format, with support for Siren and other hypermedia formats planned.

For more insight into HyperResource's goals and design, read the paper!

Hypermedia in a Nutshell

Hypermedia APIs return a list of hyperlinks with each response. These links, each of which has a relation name or "rel", represent everything you can do to, with, or from the given response. They are URLs which can take arguments. The consumer of the hypermedia API uses these links to access the API's entire functionality.

A primary advantage to hypermedia APIs is that a client library can write itself based on the hyperlinks coming back with each response. This removes both the chore of writing a custom client library in the first place, and also the process of pushing client updates to your users.

HyperResource Philosophy

An automatically-generated library can and should feel as comfortable as a custom client library. Hypermedia brings many promises, but this is one we can deliver on.

If you're an API user, HyperResource will help you consume a hypermedia API with short, direct, elegant code. If you're an API designer, HyperResource is a great default client, or a smart starting point for a rich SDK.

Link-driven APIs are the future, and proper tooling can make it The Jetsons instead of The Road Warrior.

Install It

Nothing special is required, just: gem install hyperresource

HyperResource works on Ruby 1.8.7 to present, and JRuby in 1.8 mode or above.

HyperResource uses the uri_template and Faraday gems.

Use It - Zero Configuration

Set up API connection:

api = HyperResource.new(root: 'https://api.example.com',
                        headers: {'Accept' => 'application/vnd.example.com.v1+json'},
                        auth: {basic: ['username', 'password']})
# => #<HyperResource:0xABCD1234 @root="https://api.example.com" @href="" @namespace=nil ... >

Now we can get the API's root resource, the gateway to everything else on the API.

api.get
# => #<HyperResource:0xABCD1234 @root="https://api.example.com" @href="" @namespace=nil ... >

What'd we get back?

api.body
# => { 'message' => 'Welcome to the Example.com API',
#      'version' => 1,
#      '_links' => {
#        'curies' => [{
#          'name' => 'example',
#          'templated' => true,
#          'href' => 'https://api.example.com/rels/{rel}'
#        }],
#        'self' => {'href' => '/'},
#        'example:users' => {'href' => '/users{?email,last_name}', 'templated' => true},
#        'example:forums' => {'href' => '/forums{?title}', 'templated' => true}
#      }
#    }

Lovely. Let's find a user by their email.

jdoe_user = api.users(email: "[email protected]").first
# => #<HyperResource:0x12312312 ...>

HyperResource has performed some behind-the-scenes expansions here.

First, the example:users link was added to the api object at the time the resource was loaded with api.get. And since the link rel has a CURIE prefix, HyperResource will allow a shortened version of its name, users.

Then, calling first on the users link followed the link and loaded it automatically.

Finally, calling first on the resource containing one set of embedded objects -- like this one -- delegates the method to .objects.first, which returns the first object in the resource.

Here are some equivalent expressions to the above. HyperResource offers a very short, expressive syntax as its primary interface, but you can always fall back to explicit syntax if you like or need to.

api.users(email: "[email protected]").first
api.get.users(email: "[email protected]").first
api.get.links.users(email: "[email protected]").first
api.get.links['users'].where(email: "[email protected]").first
api.get.links['users'].where(email: "[email protected]").get.first
api.get.links['users'].where(email: "[email protected]").get.objects.first[1][0]

Use It - ActiveResource-style

If an API is returning data type information as part of the response, then we can assign those data types to ruby classes so that they can be extended.

For example, in our hypothetical Example API above, a user object is returned with a custom media type bearing a 'type=User' modifier. We will extend the User class with a few convenience methods.

class ExampleAPI < HyperResource
  self.root = 'https://api.example.com'
  self.headers = {'Accept' => 'application/vnd.example.com.v1+json'}
  self.auth = {basic: ['username', 'password']}

  class User < ExampleAPI
    def full_name
      first_name + ' ' + last_name
    end
  end
end

api = ExampleApi.new

user = api.users.where(email: '[email protected]').first
# => #<ExampleApi::User:0xffffffff ...>

user.full_name
# => "John Doe"

Don't worry if your API uses some other mechanism to indicate resource data type; you can override the .get_data_type method and implement your own logic.

Configuration for Multiple Hosts

HyperResource supports the concept of different APIs connected in one ecosystem by providing a mechanism to scope configuration parameters by a URL mask. This allows a simple way to provide separate authentication, headers, etc. to different services which link to each other.

As a toy example, consider two servers. http://localhost:12345/ returns:

{ "name": "Server One",
  "_links": {
    "self":       {"href": "http://localhost:12345/"},
    "server_two": {"href": "http://localhost:23456/"}
  }
}

And http://localhost:23456/ returns:

{ "name": "Server Two",
  "_links": {
    "self":       {"href": "http://localhost:23456/"},
    "server_one": {"href": "http://localhost:12345/"}
  }
}

The following configuration would ensure proper namespacing of the two servers' response objects:

class APIEcosystem < HyperResource
  self.config(
    "localhost:12345" => {"namespace" => "ServerOneAPI"},
    "localhost:23456" => {"namespace" => "ServerTwoAPI"}
  )
end

root_one = APIEcosystem.new(root: ‘http://localhost:12345’).get
root_one.name      # => ‘Server One’
root_one.url       # => ‘http://localhost:12345
root_one.namespace # => ServerOneAPI

root_two = root_one.server_two.get
root_two.name      # => ‘Server Two’
root_two.url       # => ‘http://localhost:23456’
root_two.namespace # => ServerTwoAPI

Fuzzy matching of URL masks is provided by the FuzzyURL gem; check there for full documentation of URL mask syntax.

Error Handling

HyperResource raises a HyperResource::ClientError on 4xx responses, and HyperResource::ServerError on 5xx responses. Catch one or both (HyperResource::ResponseError). The exceptions contain as much of cause (internal exception which led to this one), response (Faraday::Response object), and body (the decoded response as a Hash) as is possible at the time.

Contributors

Many thanks to the people who've sent pull requests and improved this code:

Authorship and License

Copyright 2013-2015 Pete Gamache, [email protected].

Released under the MIT License. See LICENSE.txt.

If you got this far, you should probably follow me on Twitter. @gamache

hyperresource's People

Contributors

etiennebarrie avatar gamache avatar jasisk avatar jmartelletti avatar julienxx avatar montague 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  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  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

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

hyperresource's Issues

Support for POST/PUT/DELETE

I want to help contribute to this gem and where I would like to start is by adding support for other HTTP actions. How are we going to support these actions? Should we just implement '.save' on a Resource and know thats an HTTP PUT or am I missing a better way?

Links following curies href

Hi.
I'm trying to use hyperresource to consume the following API from server:

{
  "_links" : {
    "curies" : [ {
      "href" : "http://example.com/docs/{rel}",
      "name" : "common",
      "templated" : true
    } ],
    "self" : {
      "href" : "http://example.com"
    },
    "common:version" : [ {
      "href" : "/version/{name}",
      "templated" : true
    } ]
}

The problem is the following

When I do api.get.version[0].url it returns http://example.com/version based on self.href link, which is not what I expected. Since version link belongs to common cury, I expected url property to look like http://example.com/docs/version/{some_name_parametr}

I see it uses resource.root which is passed in Hyperresource constructor.
https://github.com/gamache/hyperresource/blob/master/lib/hyper_resource/link.rb#L73

Probably the case to consider.
Regards.

Updating a resource's attributes does not work at all.

2.1.2 :025 > api.transactions[1].description = 'foo'
 => "foo" 
2.1.2 :026 > api.transactions[1].changed?
 => false 
2.1.2 :027 > api.transactions[1].attributes
 => {"id"=>2, "description"=>"example", "amount"=>"0.1235E2", "credit_account_name"=>"debtor", "debit_account_name"=>"savings", "date"=>"2000-06-12 00:00:00 +0200"} 
2.1.2 :028 > api.transactions[1].attributes["description"] = 'foo'
 => "foo" 
2.1.2 :029 > api.transactions[1].attributes
 => {"id"=>2, "description"=>"example", "amount"=>"0.1235E2", "credit_account_name"=>"debtor", "debit_account_name"=>"savings", "date"=>"2000-06-12 00:00:00 +0200"} 
2.1.2 :030 > api.transactions[1].changed?
 => false 
2.1.2 :031 > api.transactions[1].description
 => "example" 

Changing attributes does not work like I would expect it to.

Add support for JWT

It would be really cool if Hyperresource also could handle JWT besides plain JSON.

Doesn't work with foxycart sandbox

I don't have time to investigate further right now, but

$ irb
irb(main):001:0> require 'hyperresource'
=> true
irb(main):002:0> api = HyperResource.new(root: "https://api-sandbox.foxycart.com/")
=> #<HyperResource:0x3fcbbecbfd08 @root="https://api-sandbox.foxycart.com/" @href="" @loaded=false @namespace=nil ...>
irb(main):003:0> root = api.get
TypeError: no implicit conversion of String into Integer
    from /Users/steve/.gem/ruby/2.0.0/gems/hyperresource-0.1.1/lib/hyper_resource/objects.rb:7:in `[]'
    from /Users/steve/.gem/ruby/2.0.0/gems/hyperresource-0.1.1/lib/hyper_resource/objects.rb:7:in `init_from_hal'
    from /Users/steve/.gem/ruby/2.0.0/gems/hyperresource-0.1.1/lib/hyper_resource.rb:108:in `init_from_response_body!'
    from /Users/steve/.gem/ruby/2.0.0/gems/hyperresource-0.1.1/lib/hyper_resource/modules/http.rb:35:in `finish_up'
    from /Users/steve/.gem/ruby/2.0.0/gems/hyperresource-0.1.1/lib/hyper_resource/modules/http.rb:13:in `get'
    from (irb):3
    from /opt/rubies/2.0.0-p0/bin/irb:12:in `<main>'

Support for JSON API

I am currently working on a JSON API adapter for HyperResource.

It is still a work in progress, but I'm wanting to ask feedback from the community early.

For now it can successfully parse objects with attributes and embedded links. Remote link support is not supported for now and I will probably only partial support for it because I don't need it for my project.

I have based my testing on a variation of hyper_resource_test which loads a JSON API hash using my adapter implementation instead of the default HAL+JSON ones.

The code is based on the current stable release. I intend to rebase it on the development release once finished.

I am now in the process to add a new live test for JSON API and I have the following issue :

In the live HAL+JSON test, in the root request, there is a name attribute. In JSON API, this kind of attributes are in a meta object. I'm hesitant between making meta an object and exposing it or add the attributes in the root resource.

In the former case, the caller would need to use api.meta.name rather than api.name using hal. In the latter case, the syntax would be the same but it might lead to collision issues.

For now, I am just asking which one do you prefer. If you happen to have any other suggestion, feel free to comment here.

My fork (if you feel like playing with it) : https://github.com/ybart/hyperresource

Header caching bug across different HyperResource object instances

I threw together a Gist describing the problem, but in sum: when I instantiate a new HyperResource instance with different headers, the effects of the headers don't take effect until I call get on the new instance.

This has a bunch of negative implications for multi-threaded applications (e.g., a Web server). (Consider the example of a memoized instance variable: @api ||= HyperResource.new(...).)

If you can point me at the code where these attributes are cached at the HyperResource class level, I can take a stab at fixing the bug.

Add support for `profile` attribute for links (other attributes?)

In our API we use the profile attribute of links extensively, but they are not available from the HyperResource::Link instances. It would be great to add this as an attribute for that class.

That also makes me wonder though about other link relation attributes that might be on the link.
Should there be a way for any attribute of a link to be available, perhaps an attributes hash that has whatever was passed in to create the HyperResource::Link?

Support for HTTP keepalive connections

Given that hypermedia APIs can be on the chatty side, HTTP keepalive would be a useful feature from a performance perspective. It doesn't look like hyperresource currently supports this. Consider adding this feature. (Or if it's already supported, make that clearer in the documentation.)

'name' property lost

Given the following node:

{
  "name": "foo"
}
#<HyperResource::Link:0x007f8d8644b978
 @base_href="/api/nodes/e22d486a-f2cc-4ecf-8659-617cd80cf41f",
 @default_method="get",
 @name=nil,
 @params={},
 @resource=
  #<HyperResource:0x3fc6c32651dc @root="http://trowel.dev/api" @href="/api/nodes" @loaded=true @namespace=nil ...>,
 @templated=false>

node.name and node['name'] both return nil

URI templates choke when given structured data

URI templates don't interpolate structured values into URLs "properly", which is to say "identically to jQuery.param".

The underlying issue is in URITemplate but this affects HyperResource strongly enough that it warrants an issue here.

Call to api.products does not wrong

I've verified my API server using the HAL-browser, but when I try hyperresource it doesn't search properly on the underlying products collection.

GET /products returns this:

{
  "_links": {
    "self": {
      "href": "/"
    },
    "curies": [
      {
        "name": "ht",
        "href": "http://localhost:8080:/rels/{rel}",
        "templated": true
      }
    ],
    "ht:products": {
      "href": "/products"
    },
    "ht:users": {
      "href": "/users"
    }
  },
  "welcome": "Welcome to Kiffin's Demo HAL Server.",
  "hint_1": "This is the first hint.",
  "hint_2": "This is the second hint.",
  "hint_3": "This is the third hint.",
  "hint_4": "This is the fourth hint.",
  "hint_5": "This is the last hint."
}

GET /products returns this:

{
  "_links": {
    "self": {
      "href": "/products"
    },
    "curies": [
      {
        "name": "ht",
        "href": "http://localhost:8080:/rels/{rel}",
        "templated": true
      }
    ],
    "ht:product": [
      {
        "href": "/products/1",
        "id": 1,
        "name": "bathtub",
        "category": "animal",
        "price": 4510
      },
      {
        "href": "/products/2",
        "id": 2,
        "name": "dragon",
        "category": "book",
        "price": 9617
      },
      ...
    ]
  }
}

GET /products/2 returns this:

{
  "_links": {
    "self": {
      "href": "/products/2"
    },
    "curies": [
      {
        "name": "ht",
        "href": "http://localhost:8080:/rels/{rel}",
        "templated": true
      }
    ]
  },
  "name": "dragon",
  "category": "book",
  "price": 9617
}

So far so good. However, following the documentation verbatim, here's what I see:

api.get => GET /
api.products => GET /
api.products(id: 2) => GET /products

I have the following questions:

  • What am I doing wrong (if anything)?
  • How can I display the product w/attributes?
  • How do I iterate through the products and displaying the attributes of each one?

`outgoing_body_filter` not used

the outgoing_body_filter method is described and has a base implementation in hyper_resource.rb

  ## +outgoing_body_filter+ filters a hash of attribute keys and values
  ## on their way from a HyperResource to a request body.  Override this
  ## in a subclass of HyperResource to implement filters on outgoing data.
  def outgoing_body_filter(attr_hash)
    attr_hash
  end

https://github.com/gamache/hyperresource/blob/master/lib/hyper_resource.rb#L152-L157

But, it isn't called anywhere, so implementing it in a subclass does nothing.

Support embedded resources with no "_links" property or "self" link

Presently HyperResource will fail trying to load an embedded resource object that has no "_links" property, or a "_links" property but no "self" link, with an error message like

NoMethodError: undefined method `[]' for nil:NilClass
    /.../lib/hyper_resource/adapter/hal_json.rb:50:in `block in apply_objects'
    /.../lib/hyper_resource/adapter/hal_json.rb:45:in `each'
    /.../lib/hyper_resource/adapter/hal_json.rb:45:in `apply_objects'
    /.../lib/hyper_resource/adapter/hal_json.rb:30:in `apply'
    ...

But according to the HAL spec,

The reserved "_links" property is OPTIONAL.

and,

Each Resource Object SHOULD [not MUST] contain a 'self' link...

This makes sense, as a resource corresponding to a weak or associative entity in a data model may have no identity of its own and thus no "self" link or any meaningful links at all. HyperResource should be able to work with resources of this type.

I've fixed this (I believe) locally and will submit a pull request soon.

Add support for offsite links

In theory, HyperResource should be able to work with APIs that refer to URLs originating on a different server. In practice, this opens up a class of security problems that could result from sharing headers, etc. among the various servers.

I don't have a clear idea how to solve this most elegantly, but I recognize that this needs to be handled. A single-origin policy should be in place at minimum, but I would rather provide a richer set of controls around authentication to different servers.

Content-type application/x-www-form-urlencoded

I think given a response for a resource that has a Content-type: application/json, when you make a PUT or POST to it, it would be nice if it assumed the content type was the same. Currently hyperresource sets the header as Content-type application/x-www-form-urlencoded.

I worked around it by doing this:

HyperResource.new(
  headers: {
    'Accept' => 'application/vnd.trowel-v5+json',
    'Content-Type' => 'application/json'
  }
)

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.