Git Product home page Git Product logo

hyperclient's Introduction

Hyperclient

Gem Version Build Status Code Climate Coverage Status

Hyperclient is a Hypermedia API client written in Ruby. It fully supports JSON HAL.

Table of Contents

Usage

The examples in this README use the Splines Demo API running here. Use version 1.x with Faraday 1.x, and version 2.x with Faraday 2.x. If you're upgrading from a previous version, please make sure to read UPGRADING.

API Client

Create an API client.

require 'hyperclient'

api = Hyperclient.new('https://grape-with-roar.herokuapp.com/api')

By default, Hyperclient adds application/hal+json as Content-Type and Accept headers. It will also send requests as JSON and parse JSON responses. Specify additional headers or authentication if necessary.

api = Hyperclient.new('https://grape-with-roar.herokuapp.com/api') do |client|
  client.headers['Access-Token'] = 'token'
end

Hyperclient constructs a connection using typical Faraday middleware for handling JSON requests and responses. You can specify additional Faraday middleware if necessary.

api = Hyperclient.new('https://grape-with-roar.herokuapp.com/api') do |client|
  client.connection do |conn|
    conn.use Faraday::Request::Instrumentation
  end
end

You can pass options to the Faraday connection block in the connection block:

api = Hyperclient.new('https://grape-with-roar.herokuapp.com/api') do |client|
  client.connection(ssl: { verify: false }) do |conn|
    conn.use Faraday::Request::Instrumentation
  end
end

Or when using the default connection configuration you can use faraday_options:

api = Hyperclient.new('https://grape-with-roar.herokuapp.com/api') do |client|
  client.faraday_options = { ssl: { verify: false } }
end

You can build a new Faraday connection block without inheriting default middleware by specifying default: false in the connection block.

api = Hyperclient.new('https://grape-with-roar.herokuapp.com/api') do |client|
  client.connection(default: false) do |conn|
    conn.request :json
    conn.response :json, content_type: /\bjson$/
    conn.adapter :net_http
  end
end

You can modify headers or specify authentication after a connection has been created. Hyperclient supports Basic, Token or Digest auth as well as many other Faraday extensions.

require 'faraday/digestauth'

api = Hyperclient.new('https://grape-with-roar.herokuapp.com/api') do |client|
  client.connection(default: false) do |conn|
    conn.request :digest, 'username', 'password'
    conn.request :json
    conn.response :json, content_type: /\bjson$/
    conn.adapter :net_http
  end
end

You can access the Faraday connection directly after it has been created and add middleware to it. As an example, you could use the faraday-http-cache-middleware.

api = Hyperclient.new('https://grape-with-roar.herokuapp.com/api')
api.connection.use :http_cache

Resources and Attributes

Hyperclient will fetch and discover the resources from your API and automatically paginate when possible.

api.splines.each do |spline|
  puts "A spline with ID #{spline.uuid}."
end

Other methods, including [] and fetch are also available

api.splines.each do |spline|
  puts "A spline with ID #{spline[:uuid]}."
  puts "Maybe with reticulated: #{spline.fetch(:reticulated, '-- no reticulated')}"
end

Links and Embedded Resources

The splines example above followed a link called "splines". While you can, you do not need to specify the HAL navigational structure, including links or embedded resources. Hyperclient will resolve these for you. If you prefer, you can explicitly navigate the link structure via _links. In the following example the "splines" link leads to a collection of embedded splines. Invoking api.splines is equivalent to api._links.splines._embedded.splines.

api._links.splines

Templated Links

Templated links require variables to be expanded. For example, the demo API has a link called "spline" that requires a spline "uuid".

spline = api.spline(uuid: 'uuid')
puts "Spline #{spline.uuid} is #{spline.reticulated ? 'reticulated' : 'not reticulated'}."

Invoking api.spline(uuid: 'uuid').reticulated is equivalent to api._links.spline._expand(uuid: 'uuid')._resource._attributes.reticulated.

The client is responsible for supplying all the necessary parameters. Templated links don't do any strict parameter name checking and don't support required vs. optional parameters. Parameters not declared by the API will be dropped and will not have any effect when passed to _expand.

Curies

Curies are a suggested means by which to link documentation of a given resource. For example, the demo API contains very long links to images that use an "images" curie.

puts spline['image:thumbnail'] # => https://grape-with-roar.herokuapp.com/api/splines/uuid/images/thumbnail.jpg
puts spline.links._curies['image'].expand('thumbnail') # => /docs/images/thumbnail

Attributes

Resource attributes can also be accessed as a hash.

puts spline.to_h # => {"uuid" => "uuid", "reticulated" => true}

The above is equivalent to spline._attributes.to_h.

HTTP

Hyperclient uses Faraday under the hood to perform HTTP calls. You can call any valid HTTP method on any resource.

For example, you can examine the API raw JSON by invoking _get and examining the _response.body hash.

api._get
api._response.body

Other methods, including _head or _options are also available.

spline = api.spline(uuid: 'uuid')
spline._head
spline._options

Invoke _post to create resources.

splines = api.splines
splines._post(uuid: 'new uuid', reticulated: false)

Invoke _put or _patch to update resources.

spline = api.spline(uuid: 'uuid')
spline._put(reticulated: true)
spline._patch(reticulated: true)

Invoke _delete to destroy a resource.

spline = api.spline(uuid: 'uuid')
spline._delete

HTTP methods always return a new instance of Resource.

Testing Using Hyperclient

You can combine RSpec, Faraday::Adapter::Rack and Hyperclient to test your HAL API without having to ever examine the raw JSON response.

describe Acme::Api do
  def app
    Acme::App.instance
  end

  let(:client) do
    Hyperclient.new('http://example.org/api') do |client|
      client.headers['Content-Type'] = 'application/json'
      client.connection(default: false) do |conn|
        conn.request :json
        conn.response :json
        conn.use Faraday::Adapter::Rack, app
      end
    end
  end

  it 'splines returns 3 splines by default' do
    expect(client.splines.count).to eq 3
  end
end

For a complete example refer to this Splines Demo API test.

Reference

Hyperclient API Reference.

Hyperclient Users

Using Hyperclient? Add your project to our wiki, please: https://github.com/codegram/hyperclient/wiki.

Contributing

Hyperclient is work of many people. You're encouraged to submit pull requests, propose features and discuss issues. See CONTRIBUTING for details.

License

MIT License, see LICENSE for details.

Copyright (c) 2012-2018 Oriol Gual, Codegram Technologies and Contributors

hyperclient's People

Contributors

alabeduarte avatar bkeepers avatar col avatar dblock avatar dependabot-preview[bot] avatar dependabot[bot] avatar divins avatar dylanfareed avatar ismasan avatar ivoanjo avatar josepjaume avatar karlin avatar koenpunt avatar langalex avatar lorenzoplanas avatar milesgrimshaw avatar mrcasals avatar nebolsin avatar oriolgual avatar paulocdf avatar txus avatar yuki24 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

hyperclient's Issues

Incorrect interpretation of the HAL RFC wrt curies

Im having trouble with curies and what appears to be a difference in behavior in hyperclient vs hal-browser. Hyperclient is templating urls when not expected.
My server returns:

"curies": [
      {
        "name": "osdi",
        "href": "http://opensupporter.github.io/osdi-docs/{rel}",
        "templated": true
      }
    ],
    "osdi:tags": {
      "href": "http://demo.osdi.io/api/v1/tags",
      "title": "The collection of tags in the system"
    },

In the hal browser, the curie is just used for documentation links and it does not reformat the link URL, which is already absolute.
But in hyperclient, it is reformatting the url for the link itself as:

http://opensupporter.github.io/osdi-docs/http://demo.osdi.io/api/v1/tags

How do I reconcile this? Can I disable curie reformatting in hyperclient?

Reference: #64

Rename Hyperclient to HyperClient?

Hyperclient is spelled with a lowercase c, but in some places, like in the description of this repo and in .gemspec it says HyperClient. While we're making backwards incompatible changes all over the place, curious what @oriolgual and others think about doing the rename into HyperClient? I personally always try to spell it this way.

Between a _post and a _get, need a fresh client?

See ruby-grape/grape-with-roar#9.

Doesn't work:

require 'hyperclient'

client = Hyperclient.new('http://localhost:9292/api')

3.times do |i|
  client.splines._post(spline: { name: i.to_s, reticulated: [true, false].sample })
end

client.splines.each do |spline|
  puts "spline #{spline.id} #{spline.reticulated ? 'is' : 'is not'} reticulated"
end

client.splines.each(&:_delete)

Works:

require 'hyperclient'

client = Hyperclient.new('http://localhost:9292/api')
3.times do |i|
  client.splines._post(spline: { name: i.to_s, reticulated: [true, false].sample })
end

client = Hyperclient.new('http://localhost:9292/api')
client.splines.each do |spline|
  puts "spline #{spline.id} #{spline.reticulated ? 'is' : 'is not'} reticulated"
end

client = Hyperclient.new('http://localhost:9292/api')
client.splines.each(&:_delete)

Error parsing response

Hi all, I'm trying to use Hyperclient to consume the API of a server that it's build with perl+Catalyst and exposes outside as JSON+HAL, but I'm stuck with a parsing error that Hyperclient throw me and I'm unnable to figure what's happen.

This is the full response when I query the resource 'subscribers':

{
   "_embedded" : {
      "ngcp:subscribers" : [
         {
            "_links" : {
               "collection" : {
                  "href" : "/api/subscribers/"
               },
               "curies" : {
                  "href" : "http://purl.org/sipwise/ngcp-api/#rel-{rel}",
                  "name" : "ngcp",
                  "templated" : true
               },
               "ngcp:callforwards" : {
                  "href" : "/api/callforwards/52"
               },
               "ngcp:calls" : {
                  "href" : "/api/calls/?subscriber_id=52"
               },
               "ngcp:customers" : {
                  "href" : "/api/customers/3"
               },
               "ngcp:domains" : {
                  "href" : "/api/domains/3"
               },
               "ngcp:journal" : {
                  "href" : "/api/subscribers/52/journal/"
               },
               "ngcp:reminders" : {
                  "href" : "/api/reminders/?subscriber_id=52"
               },
               "ngcp:subscriberpreferences" : {
                  "href" : "/api/subscriberpreferences/52"
               },
               "ngcp:subscriberregistrations" : {
                  "href" : "/api/subscriberregistrations/?subscriber_id=52"
               },
               "ngcp:voicemailsettings" : {
                  "href" : "/api/voicemailsettings/52"
               },
               "profile" : {
                  "href" : "http://purl.org/sipwise/ngcp-api/"
               },
               "self" : {
                  "href" : "/api/subscribers/52"
               }
            },
            "administrative" : false,
            "alias_numbers" : [],
            "customer_id" : 3,
            "domain" : "proxy.testserver.org",
            "domain_id" : 3,
            "email" : null,
            "external_id" : null,
            "id" : 52,
            "password" : "testtest",
            "primary_number" : {
               "ac" : "949",
               "cc" : "1",
               "sn" : "3465301"
            },
            "profile_id" : null,
            "profile_set_id" : null,
            "status" : "active",
            "username" : "test.fax",
            "uuid" : "45cdcf6f-3f39-4245-bccb-3db775622701",
            "webpassword" : null,
            "webusername" : "test.fax"
         },
         {
            "_links" : {
               "collection" : {
                  "href" : "/api/subscribers/"
               },
               "curies" : {
                  "href" : "http://purl.org/sipwise/ngcp-api/#rel-{rel}",
                  "name" : "ngcp",
                  "templated" : true
               },
               "ngcp:callforwards" : {
                  "href" : "/api/callforwards/55"
               },
"ngcp:calls" : {
                  "href" : "/api/calls/?subscriber_id=55"
               },
               "ngcp:customers" : {
                  "href" : "/api/customers/3"
               },
               "ngcp:domains" : {
                  "href" : "/api/domains/3"
               },
               "ngcp:journal" : {
                  "href" : "/api/subscribers/55/journal/"
               },
               "ngcp:reminders" : {
                  "href" : "/api/reminders/?subscriber_id=55"
               },
               "ngcp:subscriberpreferences" : {
                  "href" : "/api/subscriberpreferences/55"
               },
               "ngcp:subscriberregistrations" : {
                  "href" : "/api/subscriberregistrations/?subscriber_id=55"
               },
               "ngcp:voicemailsettings" : {
                  "href" : "/api/voicemailsettings/55"
               },
               "profile" : {
                  "href" : "http://purl.org/sipwise/ngcp-api/"
               },
               "self" : {
                  "href" : "/api/subscribers/55"
               }
            },
            "administrative" : false,
            "alias_numbers" : [],
            "customer_id" : 3,
            "domain" : "proxy.testserver.org",
            "domain_id" : 3,
            "email" : null,
            "external_id" : null,
            "id" : 55,
            "password" : "tfp6LzOxTFa2OWHF",
            "primary_number" : {
               "ac" : "919",
               "cc" : "1",
               "sn" : "8675959"
            },
            "profile_id" : null,
            "profile_set_id" : null,
            "status" : "active",
            "username" : "7WOkIWgU",
            "uuid" : "df0ed22e-d0db-406b-b1cf-0f068f5778d4",
            "webpassword" : null,
            "webusername" : "7WOkIWgU"
         }
      ]
   },
   "_links" : {
      "curies" : {
         "href" : "http://purl.org/sipwise/ngcp-api/#rel-{rel}",
         "name" : "ngcp",
         "templated" : true
      },
      "ngcp:subscribers" : [
         {
            "href" : "/api/subscribers/52"
         },
         {
            "href" : "/api/subscribers/55"
         }
      ],
      "profile" : {
         "href" : "http://purl.org/sipwise/ngcp-api/"
      },
      "self" : {
         "href" : "/api/subscribers/?page=1&rows=10"
      }
   },
   "total_count" : 2
}

And this is the error Hyperclient throw me when I try to do a simple iteration over api.subscribers.each to just puts the subscriber.id:

RuntimeError: Invalid response for LinkCollection. The response was: "_links"
from /usr/local/rvm/gems/ruby-2.3.3/gems/hyperclient-0.8.5/lib/hyperclient/link_collection.rb:18:in `initialize'

Why it's unable the full parse the response and let my iterate over the subscribers collection ? ... I've checked that the response is ok.

How do I retrieve standalone instances?

I am new to HAL, so maybe my API is not doing the right thing per spec, will be glad to change it if needed.

I have a a collection of applications and the following API root.

{
_links: {
  self: {
    href: "http://localhost:3000/api/"
  },
  applications: {
    href: "http://localhost:3000/api/applications"
  }
 }
}

I can do api.links.applications which calls /api/applications which returns an embedded collection of applications, all good. Each application has a link to "self".

Now, I have a Rails app that needs to edit one of those applications. So I get some application ID. How am I supposed to fetch a single application? I do have a route /api/applications/:id which returns the application JSON directly, with its own links.

{
  id: "53fc980f64626c2dd9000000",
  created_at: "2014-08-26T14:22:07.000Z",
  name: "test",
  ...
  _links: {
    self: {
      href: "http://localhost:3000/api/applications/53fc980f64626c2dd9000000"
    }
}

Am I missing a template at the root? What should that be? How can I retrieve that with hyperclient?

Bad interaction between URITemplate and Faraday parameter parsing means dropped values from queries including multiple values for a parameter

The uri_template gem, which hyperclient uses to build urls, implements RFC6570, that expands a parameter in the {?foo*} format as ?foo=a&foo=b&foo=c.

Unfortunately, faraday's default argument parser (Faraday::Utils.default_params_encoder) is set to the NestedParamsEncoder which only considers parameters to be arrays if they have [] in the key.

The result is that values for a parameter are dropped in this process:

require 'faraday'
require 'uri_template'

Faraday::Utils.default_params_encoder.decode(
  URITemplate.new('http://example.com/{?foo*}').expand(foo: ['a', 'b']).split('?').last
)
=> {"foo"=>"b"}

And thus the resulting faraday request will be missing some of the values.

The solution is to use faraday's other parameter encoder (the FlatParamsEncoder).

I'm working on a PR to fix this, but wanted to open the issue to have something to reference.

A simple identity map for resources

Implement a simple Resource identity map. Check if there's already a Resource created with that url and return that instance instead of creating it.

How do I shut off Futuruscope?

How can I tell hyperclient to either not use futuroscope or to stop using a thread pool?
In production i can see the use of it, but in development debugging it makes it near impossible to step through what is going on.

Honour Allow headers

If the server sends and Allow header, honour it and don't let the client send unallowed requests

Merge links & embedded

This is a major refactor, but Hyperclient should not differntiate between links and embeded they are all resource with more or less information.

Instead of accessing client.links.posts.first or client.embedded.posts.first one should be able to simply access client.resources.posts.first

Resource / Attributes is mutating the response.body

I'd like to be able to access the raw JSON response from a Resource. I've noticed that the Resource class now has an attribute accessor for the response object. Unfortunately there seems to be a bug caused by the Attributes class deleting the '_link' and '_embedded' keys from the representation parameter. When these keys are removed from the representation it mutates the response.body.

I think this can be fixed fairly easily by calling .dup on the response.body when initialising the Resource. Does this sounds like a reasonable solution?

I'm happy to put a pull request together if you agree with the approach.

Changing settings to deal with "end of file reached" after one minute

I am getting "Faraday::Error::ConnectionFailed: end of file reached" if my request takes any longer than one minute.

However, I can't figure out how to get options to Faraday through the Hyperclient API. Hyperclient::EntryPoint.connection calls Faraday.new with a hard-coded options hash, and from there I can't figure out how I might access/modify that hash. Any ideas?

Loading full representations of embedded resources

If the changes I proposed in #34 happen, it will be common for a consumer to unknowingly receive a partial representation of a resource.

HAL draft, Section 4.1.2:

Embedded Resources MAY be a full, partial, or inconsistent version of the representation served from the target URI.

For example, say a root resource has the an embedded categories relation:

{
  "_embedded": {
    "categories": [
      {"name": "Hypermedia", "_links": {"self": {"href": "http://example.com/hypermedia"}}}
    ]
  }

Following the self link on the embedded category gives something like this:

{
  "id": 1,
  "name": "Hypermedia",
  "_embedded": {
    "talks": [
      {"title": "Introduction to Hypermedia", "_links": {"self": {"href": "..."}}}
    ]
  }

If the proposed changes in #34 happen, I would imagine a consumer doing something like:

Hyperclient.new(url).categories.each do |category|
  puts category.attributes.name
  category.talks.each do |talk|
    puts "* #{talk.attributes.title}"
  end
end

The problem is that this could would blow up when the consumer calls category.talks because the category resource has no clue that it is only a partial representation.

It would be amazing if hyperclient were aware of this and could attempt to reload the partial resource (which it knows is partial because it came from an embed) from the self link if an unknown attribute or relation is requested.

This should be fairly easy to implement, and still be clean, but I wanted to get your thoughts before starting on it.

new release

care to release a new gem version with the faraday changes included?

HTTP status/code not accessible

Within link#resource(), when get() is called, only the body is saved, and the status code is discarded. That's not very handy. I'd like to add an attribute to Hyperclient::Resource for the status. Does that sound ok? Is there anything I should consider before making a PR?

Async background requests with futuroscope have not been async since 2014

While working on #122 I started wondering if Link#http_method was correct when using the Futuroscope::Future:

    def http_method(method, body = nil)
      @resource = begin
        response =
          if @entry_point.options[:async]
            Futuroscope::Future.new do
              @entry_point.connection.run_request(method, _url, body, nil)
            end
          else
            @entry_point.connection.run_request(method, _url, body, nil)
          end

        Resource.new(response.body, @entry_point, response)
      end
    end

More specifically, we create a new Futuroscope::Future, store it in response, but then immediately ask for it's resource.body when creating a new resource. This means that we basically block the current thread while waiting for the future to resolve so we can call body on it. After the request finishes executing in the background, we finally get the body, and are able to create an return the new Resource.

Looking at git history, this has been broken since the Resource was introduced in 5de6f84, which was released in version 0.6 in 2014.

So this means that for all of almost three years nobody really noticed that this was broken, which leads me to make some observations:

  • Would it make sense for async: false to be the default behavior instead, since that's how people have been using it for a long time?
  • Would it make sense to consider removing the background/async functionality altogether?
  • Alternatively, would it make sense to consider replacing futuroscope using the concurrent-ruby which has been adopted by the likes of activesupport, dry-*, graphql, hanami, rom, sidekiq, etc. and thus most ruby applications probably already have it as a direct or indirect dependency?

hyperclient v0.8.3 breaks compatibility with Faraday < v0.9

Hyperclient v0.8.3 implements FlatParamsEncoder, which was implemented in Faraday v0.9 and above. This breaks compatibility with Faraday less than v0.9. I wanted to see if this change was intensional, and is so, if you'd consider adding the minimum version of faraday in your gemspec so that bundler can figure out what version of hyperclient to use based on the app's version of Faraday.

Multiple copies of the same resource

I'm getting the weirdest bug with my Hyperscore client.

If I do:

Hyperscore.new.links.news.resources
#=> a bunch of different news

I can see that everything is fine, as the resources are as they should.

But, once I try to access them, either of the following ways, then every resource turns into the newest one:

Hyperscore.new.links.news.resources.news
#=> a bunch of copies of news 19

Do you have any idea what's going on? It's really confusing to me.

Dealing With Other Media Types

Imagine I get some HAL from a server that looks like this (in part, maybe):

{
  '_links': {
    'self': { 'href': '/users/12345' },
    'profile_image': { 'href': '/user/12345/face' }
  }
}

And I am super interested in profile images, so I want to get that sucker. Right now, if I call, you know, user.links.profile_image.resource I get back a sort of confused Hyperclient::Resource that can't tell me the media type is jpeg or whatever. I could call get instead of resource, but then I have to know more about the foreign API.

I wonder if we shouldn't make it so that Hyperclient, when it sees a response from a server that isn't Content-Type: application/hal+json (or maybe a list to include normal application/json and that a user could add to), it just hands over the Faraday::Response object or something similar. Basically, it punts and says, "This isn't hypermedia I know about, you handle this bullshit."

Thoughts?

Link method_missing delegation wrongly returns nil when field value is false

When a resource includes some field that is false, trying to read it through the Link class will result in the value being returned as nil instead of false.

Test case:

diff --git a/test/hyperclient/link_test.rb b/test/hyperclient/link_test.rb
index 823efa6..20e8872 100644
--- a/test/hyperclient/link_test.rb
+++ b/test/hyperclient/link_test.rb
@@ -479,6 +479,16 @@ module Hyperclient
           resource.orders.first.id.must_equal 1
         end
 
+        it 'can handle false values in the response' do
+          resource = Resource.new({ '_links' => { 'orders' => { 'href' => '/orders' } } }, entry_point)
+
+          stub_request(entry_point.connection) do |stub|
+            stub.get('http://api.example.org/orders') { [200, {}, { 'any' => false }] }
+          end
+
+          resource.orders.any.must_equal false
+        end
+
         it "doesn't delegate when link key doesn't match" do
           resource = Resource.new({ '_links' => { 'foos' => { 'href' => '/orders' } } }, entry_point)

Current result:

Hyperclient::Link::method_missing::delegation
     FAIL (0:00:00.145) test_0002_can handle false values in the response
          Expected: false
            Actual: nil
        @ (eval):8:in `must_equal'
          test/hyperclient/link_test.rb:489:in `block (4 levels) in <module:Hyperclient>'

This seems to me to be due to a bad interaction in Link#method_missing (https://github.com/codegram/hyperclient/blob/master/lib/hyperclient/link.rb#L129):

    # Internal: Delegate the method further down the API if the resource cannot serve it.
    def method_missing(method, *args, &block)
      if _resource.respond_to?(method.to_s)
        _resource.send(method, *args, &block) || delegate_method(method, *args, &block)
      else
        super
      end
    end

In this case, _resource.send(method, *args, &block) is returning false, which we are mistaking for the case where we're trying to call an array method on the resource, and thus instead of returning false we are calling delegate_method (which will then return the nil).

I was thinking about it and it seems to me that changing it to treat false differently would fix the issue:

diff --git a/lib/hyperclient/link.rb b/lib/hyperclient/link.rb
index 1fd1169..cc5b635 100644
--- a/lib/hyperclient/link.rb
+++ b/lib/hyperclient/link.rb
@@ -126,7 +126,8 @@ module Hyperclient
     # Internal: Delegate the method further down the API if the resource cannot serve it.
     def method_missing(method, *args, &block)
       if _resource.respond_to?(method.to_s)
-        _resource.send(method, *args, &block) || delegate_method(method, *args, &block)
+        result = _resource.send(method, *args, &block)
+        result.nil? ? delegate_method(method, *args, &block) : result
       else
         super
       end

But I was not entirely sure, hence I decided to open an issue first, instead of jumping straight to a PR. Any thoughts on this approach?

Accept-Encoding: gzip

From what I gleaned from the issue tracker, HTTParty should support this already so it shouldn't be too hard to add it.
This cuts HAL payloads considerably, especially with several embedded resources.

Extending this to support JSON-API

We're big fans of json-api @balanced and I think your code could be something core to the balanced/balanced-ruby project.

We currently do not support HAL however I would like to extend HyperClient for json-api support.

Wanted to get your thoughts on creating a milestone that we can contribute to.

Reducing inconsistency in object vs. hash approach

Hi,

I'm running into a code smell in my Hyperclient. It has to do with Hyperclient::Attributes being returned from top-level attributes of a resource, but not for anything beneath.

Thus, on a top-level resource, you can do: my_resource.attributes.foo or my_resource.attributes['foo']

but not:

my_resource.attributes.foo.bar

Instead you must:
my_resource.attributes.foo['bar'] or the more consistent but not enforced and less concise my_resource.attributes['foo']['bar']

That's not great, but it's ok. But there are other smells. For example, you can:
my_resource.attributes.respond_to?(:foo) but not my_resource.attributes.foo.respond_to?(:bar)

Also you can my_resource.attributes.foo.has_key?('bar'), but you can't do my_resource.attributes.has_key?('foo') (even though you can access foo by key)

One way to reduce this smell is to change Hyperclient::Collection to extend Hash. The majority of the methods could be removed except for method_missing & responds_to_missing? See, for example, Thor::CoreExt::HashWithIndifferentAccess. This approach would allow .has_key?() on the top-level Attributes object.

Another way would be to provide an option to not use the Attribute class, ensuring consistency because everything past .attributes is a simple Hash.

There's also the recursive-open-struct gem, but I'm not sure that's a great idea.

Would you accept a PR adding an option to disable returning an Attribute object (returning the hash instead)? If not, would you accept a PR that somehow allows .has_key?() on Attribute?

Irregular Use of Faraday?

I'm using Hyperclient and have configured the underlying Faraday instance to use Net::HTTP::Persistent which, when you're making a ton of requests to the same server, can be a huge win for a process that's talking to an external API.

In my testing, I'm using WebMock to stub out and set expectations about my usage of the external API. I ran into a weird issue: When I'm using Net::Http::Persistent, it seems like Hyperclient is making 2 requests where I would expect 1.

Example script:

require 'net/http/persistent'
require 'webmock'
require 'pry'
require 'webmock/rspec'
require 'rspec'
require 'hyperclient'

uri = "http://example.com"

http = Hyperclient.new(uri).tap do |client|
  client.headers['Accept'] = 'application/hal+json'
  client.connection.adapter :net_http_persistent
end

WebMock.disable_net_connect!

describe "Using WebMock and Net::HTTP::Persistent together" do
  it "makes only one request" do
    WebMock.stub_request(:get, uri)

    http.get

    expect(WebMock).to have_requested(:get, uri)
  end
end

When I run this, behold:

❱ rspec -fd net_http_persistent_web_mock.rb 

Using WebMock and Net::HTTP::Persistent together
  makes only one request (FAILED - 1)

Failures:

  1) Using WebMock and Net::HTTP::Persistent together makes only one request
     Failure/Error: expect(WebMock).to have_requested(:get, uri)
       The request GET http://example.com/ was expected to execute 1 time but it executed 2 times

       The following requests were made:

       GET http://example.com/ with headers {'Accept'=>'application/hal+json', 'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Content-Type'=>'application/json', 'User-Agent'=>'Faraday v0.8.8'} was made 1 time
       GET http://example.com/ with headers {'Accept'=>'application/hal+json', 'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Connection'=>'keep-alive', 'Content-Type'=>'application/json', 'Keep-Alive'=>'30', 'User-Agent'=>'Faraday v0.8.8'} was made 1 time

       ============================================================
     # ./net_http_persistent_web_mock.rb:23:in `block (2 levels) in <top (required)>'

Finished in 0.00544 seconds
1 example, 1 failure

Failed examples:

rspec ./net_http_persistent_web_mock.rb:18 # Using WebMock and Net::HTTP::Persistent together makes only one request

If you comment out line 12 of the test script, it works fine, and if I use Faraday directly (with Net::HTTP::Persistent configured), it works fine.

Any ideas, here? If someone can point me in the right direction, I'll gladly investigate on my own and work up a fix. I'd hate to have to abandon persistent HTTP connections because hypermedia APIs tend to be pretty chatty.

Using Faraday HTTP Cache (or just using Faraday middleware)

I'm trying to use faraday-http-cache with Hyperclient, but it isn't proving easy.

faraday-http-cache expects to sit before :net_http in the middleware stack. A working example from their docs looks like this:

client = Faraday.new('https://api.github.com') do |stack|
  stack.response :json, content_type: /\bjson$/
  stack.use :http_cache, logger: ActiveSupport::Logger.new(STDOUT)
  stack.adapter Faraday.default_adapter
end

# which has this middleware:
[FaradayMiddleware::ParseJson, Faraday::HttpCache, Faraday::Adapter::NetHttp]

However, trying to achieve the same thing in Hyperclient doesn't work as expected. The following setup, which I would expect to work, results in duplicate :net_http in the middleware.

client = Hyperclient.new('https://api.github.com')
client.connection.use :http_cache, logger: ActiveSupport::Logger.new(STDOUT)
client.connection.adapter Faraday.default_adapter

# which has this middleware (see the duplicate NetHttp):
[FaradayMiddleware::EncodeJson, FaradayMiddleware::ParseJson, Faraday::Adapter::NetHttp, Faraday::HttpCache, Faraday::Adapter::NetHttp]

This is because of the way Hyperclient initialises the Faraday connection (see default_faraday_block) with the :net_http adapter when it creates the connection.

The only way I've got this working was by monkey patching or subclassing EntryPoint and overriding the default_faraday_block so the :http_cache middleware is inserted before :net_http.

class Hyperclient::EntryPoint
  def default_faraday_block
    lambda do |faraday|
      faraday.request  :json
      faraday.response :json, content_type: /\bjson$/
      faraday.use :http_cache, logger: ActiveSupport::Logger.new(STDOUT)
      faraday.adapter :net_http
    end
  end
end

Advice? This is less than ideal.

Follow redirects

I think hyperclient should by default follow redirects, which means adding to

faraday.adapter :net_http
:

        faraday.use FaradayMiddleware::FollowRedirects

Currently monkey-patching

require 'hyperclient/entry_point'

module Hyperclient
  class EntryPoint < Link
    def default_faraday_block
      lambda do |faraday|
        faraday.use FaradayMiddleware::FollowRedirects
        faraday.request  :json
        faraday.response :json, content_type: /\bjson$/
        faraday.adapter :net_http
      end
    end
  end
end

Thoughts?

Intermittent test failure

  Scenario: Send JSON data
    ✔  Given I use the default hyperclient config     # features/steps/default_config.rb:4
    ✔  When I send some data to the API               # features/steps/default_config.rb:12
    !  Then it should have been encoded as JSON       # features/steps/default_config.rb:17

        The request POST http://api.example.org/posts with body "{\"title\":\"My first blog post\"}" was expected to execute 1 time but it executed 0 times

        The following requests were made:

        GET http://api.example.org/ with headers {'Accept'=>'application/json', 'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Content-Type'=>'application/json', 'User-Agent'=>'Faraday v0.9.0'} was made 1 time

        ============================================================
        /Users/dblock/.rvm/rubies/ruby-2.0.0-p353/lib/ruby/gems/2.0.0/gems/webmock-1.18.0/lib/webmock/assertion_failure.rb:7:in `failure'

  Scenario: Parse JSON data
    ✔  Given I use the default hyperclient config     # features/steps/default_config.rb:4
    ✔  When I get some data from the API              # features/steps/default_config.rb:21
    ✔  Then it should have been parsed as JSON        # features/steps/default_config.rb:25

Error summary:
  Errors (1)
    Default config :: Send JSON data :: Then it should have been encoded as JSON

This is intermittent, 3 out of 10 runs. Looks suspicious.

Hyperclient::Resource#to_h != Hyperclient::Resource#to_hash

Hi guys, I'm using v0.8.1 and am having a discrepancy when using #to_h in place of #to_hash on a Hyperclient::Resource.

cluster_meters = Source.clusters._get.by_id(id: id)._get.meters._embedded
meter = gateway_meters.all.first
expect(meter.to_h).to eq(meter.to_hash)

However the call to #to_h returns nil, while the call to #to_hash returns the expected hash

Advice For Use In Testing?

Does anyone have any pointers for using Hyperclient in testing? I'm writing an API (using Rails) and would love to use Hyperclient in my high level tests (RSpec request specs) in a similar manner to doing Capybara stuff (RSpec feature specs). I just don't know enough about the environment to set the entry point right so that it hits the local app and doesn't make real requests.

I'd be happy to make a PR adding any collective wisdom to the README. Or if code needs to be tweaked, I'd be happy to do that, too, given a little guidance.

Provide abstraction of linking vs. embedding?

The idea here is that the client shouldn't care whether an object is linked or embedded. Hyperclient could provide an API that could navigate to an object regardless of which way the server provided it. It would work with the GET verb only (if it's a link). Does this sound like an idea that has merit? Do you think it would be acceptable as a new feature of Hyperclient?

Link#post return Resource?

I don't know that Link#post should actually return a Resource since you'd lose access to the Faraday response (though, if #32 gets sorted, that'll fade away). But somehow, it would be nice to be able to make a POST request and still get a rich Hyperclient object back, rather than the Faraday response.

Honestly, if #32 were resolved and you had the response stored with a Resource, you could get rid of Link#resource (deprecate it first, of course) and just use methods named for the HTTP verbs and have them all return a Resource. That would be perfect, I think.

Imagine a scenario:

### Rels

#### create_article
This rel will create a new blog post. You must POST to it and include the `body` and `title` parameters. All others are optional.
c = Hyperclient.new('http://example.com/')
response = c.links.create_article.expand(title: 'Awesome Article', body: awesome_article_body).post

Say that works and returns you 201 with a response body that is hal describing the newly create article. It'll have links you might want to follow, etc. However, if you really want to follow them you currently have two options:

  1. Hand-build the Resource object and then go to work.
  2. Call resource on the expanded link to get the representation anew, using another HTTP request to refetch data the server already sent you.

Thoughts? Concerns? Pitchforks?

Content type hal+json doesn't agree with Faraday MIME json type

If you are making a request from a spec with setup that is something like this

Hyperclient.new('http://example.org/') do |client|
      client.connection(default: false) do |conn|
        conn.request :json
        conn.response :json
        conn.use Faraday::Adapter::Rack, app
      end
    end

FaradayMiddleware::EncodeJson will not convert the request to json because it isn't aware of 'application/hal+json'

We monkeypatched it locally to work around the problem but it still isn't pretty.

module FaradayMiddleware
  class EncodeJson < Faraday::Middleware
    MIME_TYPE = 'application/hal+json'.freeze
  end
end

Alternatively, resetting the client's content type to application/json will work.

Hyperclient.new('http://example.org/') do |client|
      client.headers['Content-Type'] = 'application/json'
      client.connection(default: false) do |conn|
        conn.request :json
        conn.response :json
        conn.use Faraday::Adapter::Rack, app
      end
    end

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.