Skip to content

Commit

Permalink
Merge pull request #75 from dblock/faraday-block
Browse files Browse the repository at this point in the history
Added support for setting headers and overriding or extending the default Faraday connection block before a connection is constructed.
  • Loading branch information
dblock committed Oct 16, 2014
2 parents df37cc8 + 91eb6c9 commit 4350988
Show file tree
Hide file tree
Showing 6 changed files with 253 additions and 31 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

This version introduces several backwards incompatible changes. See [UPGRADING](UPGRADING.md) for details.

* [#41](https://github.com/codegram/hyperclient/issues/41): All Link HTTP methods now return a Resource, including `_get`, which has been aliased to `_resource`, `_post`, `_put`, `_patch`, `_head` and `_options` - [@dblock](https://github.com/dblock).
* [#51](https://github.com/codegram/hyperclient/issues/51), [#75](https://github.com/codegram/hyperclient/pull/75): Added support for setting headers and overriding or extending the default Faraday connection block before a connection is constructed - [@dblock](https://github.com/dblock).
* [#41](https://github.com/codegram/hyperclient/issues/41), [#73](https://github.com/codegram/hyperclient/pull/73): All Link HTTP methods now return a Resource, including `_get`, which has been aliased to `_resource`, `_post`, `_put`, `_patch`, `_head` and `_options` - [@dblock](https://github.com/dblock).
* [#72](https://github.com/codegram/hyperclient/pull/72): The default Faraday block now uses `Faraday::Response::RaiseError` and will cause HTTP errors to be raised as exceptions - [@dblock](https://github.com/dblock).
* Your contribution here.

Expand Down
52 changes: 41 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,51 @@ require 'hyperclient'
api = Hyperclient.new('https://grape-with-roar.herokuapp.com/api')
```

By default, Hyperclient adds `application/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. Hyperclient supports Basic, Token or Digest auth as well as many other [Faraday](http://github.com/lostisland/faraday) extensions.
By default, Hyperclient adds `application/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.

```ruby
api = Hyperclient.new('https://grape-with-roar.herokuapp.com/api').tap do |api|
api.digest_auth('username', 'password')
api.headers.update('Accept-Encoding' => 'deflate, gzip')
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](http://github.com/lostisland/faraday) middleware for handling JSON requests and responses. You can specify additional Faraday middleware if necessary.

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

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

```ruby
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.

```ruby
api = Hyperclient.new('https://grape-with-roar.herokuapp.com/api')
api.digest_auth('username', 'password')
api.headers.update('Accept-Encoding' => 'deflate, gzip')
```

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](https://github.com/plataformatec/faraday-http-cache).

```ruby
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.
Expand Down Expand Up @@ -117,13 +153,7 @@ spline = api.spline(uuid: 'uuid')
spline._delete
```

### Faraday Connection

You can access the Faraday connection directly to add middleware by calling `connection` on the entry point. As an example, you could use the [faraday-http-cache-middleware](https://github.com/plataformatec/faraday-http-cache).

```ruby
api.connection.use :http_cache
```
HTTP methods always return a new instance of Resource.

## Reference

Expand Down
4 changes: 2 additions & 2 deletions lib/hyperclient.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ module Hyperclient
# url - A String with the url of the API.
#
# Returns a Hyperclient::EntryPoint
def self.new(url)
Hyperclient::EntryPoint.new(url)
def self.new(url, &block)
Hyperclient::EntryPoint.new(url, &block)
end
end
82 changes: 73 additions & 9 deletions lib/hyperclient/entry_point.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,29 @@
require_relative '../faraday/connection'

module Hyperclient
# Public: Exception that is raised when trying to modify an
# already initialized connection.
class ConnectionAlreadyInitializedError < StandardError
# Public: Returns a String with the exception message.
def message
'The connection has already been initialized.'
end
end

# Public: The EntryPoint is the main public API for Hyperclient. It is used to
# initialize an API client and setup the configuration.
#
# Examples
#
# client = Hyperclient::EntryPoint.new('http://my.api.org')
#
# client = Hyperclient::EntryPoint.new('http://my.api.org') do |entry_point|
# entry_point.connection(default: true) do |conn|
# conn.use Faraday::Request::OAuth
# end
# entry_point.headers['Access-Token'] = 'token'
# end
#
class EntryPoint < Link
extend Forwardable
# Public: Delegates common methods to be used with the Faraday connection.
Expand All @@ -18,16 +34,64 @@ class EntryPoint < Link
# Public: Initializes an EntryPoint.
#
# url - A String with the entry point of your API.
def initialize(url)
def initialize(url, &_block)
@link = { 'href' => url }
@entry_point = self
yield self if block_given?
end

# Public: A Faraday connection to use as a HTTP client.
#
# options - A Hash containing additional options.
#
# default - Set to true to reuse default Faraday connection options.
#
# Returns a Faraday::Connection.
def connection
@connection ||= Faraday.new(_url, { headers: default_headers }, &default_faraday_block)
def connection(options = { default: true }, &block)
if block_given?
fail ConnectionAlreadyInitializedError if @connection
if options[:default]
@faraday_block = lambda do |conn|
default_faraday_block.call conn
block.call conn
end
else
@faraday_block = block
end
else
@connection ||= Faraday.new(_url, { headers: headers }, &faraday_block)
end
end

# Public: Set headers.
#
# value - A Hash containing headers to include with every API request.
def headers=(value)
fail ConnectionAlreadyInitializedError if @connection
@headers = value
end

# Public: Headers included with every API request.
#
# Returns a Hash.
def headers
return @connection.headers if @connection
@headers ||= default_headers
end

# Public: Faraday block used with every API request.
#
# Returns a Proc.
def faraday_block
@faraday_block ||= default_faraday_block
end

# Public: Set a Faraday block to use with every API request.
#
# value - A Proc accepting a Faraday::Connection.
def faraday_block=(value)
fail ConnectionAlreadyInitializedError if @connection
@faraday_block = value
end

private
Expand All @@ -42,12 +106,12 @@ def connection
#
# Returns a block.
def default_faraday_block
lambda do |faraday|
faraday.use Faraday::Response::RaiseError
faraday.use FaradayMiddleware::FollowRedirects
faraday.request :json
faraday.response :json, content_type: /\bjson$/
faraday.adapter :net_http
lambda do |conn|
conn.use Faraday::Response::RaiseError
conn.use FaradayMiddleware::FollowRedirects
conn.request :json
conn.response :json, content_type: /\bjson$/
conn.adapter :net_http
end
end

Expand Down
112 changes: 104 additions & 8 deletions test/hyperclient/entry_point_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,128 @@

module Hyperclient
describe EntryPoint do
describe 'default' do
let(:entry_point) do
EntryPoint.new 'http://my.api.org'
end

describe 'connection' do
it 'creates a Faraday connection with the entry point url' do
entry_point.connection.url_prefix.to_s.must_equal 'http://my.api.org/'
end

it 'creates a Faraday connection with the default headers' do
entry_point.headers['Content-Type'].must_equal 'application/json'
entry_point.headers['Accept'].must_equal 'application/json'
end

it 'can update headers after a connection has been constructed' do
entry_point.connection.must_be_kind_of Faraday::Connection
entry_point.headers.update('Content-Type' => 'application/foobar')
entry_point.headers['Content-Type'].must_equal 'application/foobar'
end

it 'can insert additional middleware after a connection has been constructed' do
entry_point.connection.must_be_kind_of Faraday::Connection
entry_point.connection.use :instrumentation
handlers = entry_point.connection.builder.handlers
handlers.must_include FaradayMiddleware::Instrumentation
end

it 'creates a Faraday connection with the default block' do
handlers = entry_point.connection.builder.handlers
handlers.must_include Faraday::Response::RaiseError
handlers.must_include FaradayMiddleware::FollowRedirects
handlers.must_include FaradayMiddleware::EncodeJson
handlers.must_include FaradayMiddleware::ParseJson
handlers.must_include Faraday::Adapter::NetHttp
end

it 'raises a ConnectionAlreadyInitializedError if attempting to modify headers' do
entry_point.connection.must_be_kind_of Faraday::Connection
lambda { entry_point.headers = {} }.must_raise ConnectionAlreadyInitializedError
end

it 'raises a ConnectionAlreadyInitializedError if attempting to modify the faraday block' do
entry_point.connection.must_be_kind_of Faraday::Connection
lambda { entry_point.connection {} }.must_raise ConnectionAlreadyInitializedError
end
end

describe 'initialize' do
it 'sets a Link with the entry point url' do
entry_point._url.must_equal 'http://my.api.org'
end
end
end
end

describe 'custom' do
let(:entry_point) do
EntryPoint.new 'http://my.api.org'
EntryPoint.new 'http://my.api.org' do |entry_point|
entry_point.connection(default: false) do |conn|
conn.request :json
conn.response :json, content_type: /\bjson$/
conn.adapter :net_http
end

entry_point.headers = {
'Content-Type' => 'application/foobar',
'Accept' => 'application/foobar'
}
end
end

describe 'connection' do
it 'creates a Faraday connection with the entry point url' do
entry_point.connection.url_prefix.to_s.must_equal 'http://my.api.org/'
end

it 'creates a Faraday connection with the default headers' do
entry_point.headers['Content-Type'].must_equal 'application/json'
entry_point.headers['Accept'].must_equal 'application/json'
it 'creates a Faraday connection with non-default headers' do
entry_point.headers['Content-Type'].must_equal 'application/foobar'
entry_point.headers['Accept'].must_equal 'application/foobar'
end

it 'creates a Faraday connection with the default block' do
handlers = entry_point.connection.builder.handlers
handlers.must_include FaradayMiddleware::FollowRedirects
handlers.wont_include Faraday::Response::RaiseError
handlers.wont_include FaradayMiddleware::FollowRedirects
handlers.must_include FaradayMiddleware::EncodeJson
handlers.must_include FaradayMiddleware::ParseJson
handlers.must_include Faraday::Adapter::NetHttp
end
end
end

describe 'inherited' do
let(:entry_point) do
EntryPoint.new 'http://my.api.org' do |entry_point|
entry_point.connection(default: true) do |conn|
conn.use Faraday::Request::OAuth
end
entry_point.headers['Access-Token'] = 'token'
end
end

describe 'connection' do
it 'creates a Faraday connection with the default and additional headers' do
entry_point.headers['Content-Type'].must_equal 'application/json'
entry_point.headers['Accept'].must_equal 'application/json'
entry_point.headers['Access-Token'].must_equal 'token'
end

describe 'initialize' do
it 'sets a Link with the entry point url' do
entry_point._url.must_equal 'http://my.api.org'
it 'creates a Faraday connection with the entry point url' do
entry_point.connection.url_prefix.to_s.must_equal 'http://my.api.org/'
end

it 'creates a Faraday connection with the default block plus any additional handlers' do
handlers = entry_point.connection.builder.handlers
handlers.must_include Faraday::Request::OAuth
handlers.must_include Faraday::Response::RaiseError
handlers.must_include FaradayMiddleware::FollowRedirects
handlers.must_include FaradayMiddleware::EncodeJson
handlers.must_include FaradayMiddleware::ParseJson
handlers.must_include Faraday::Adapter::NetHttp
end
end
end
Expand Down
31 changes: 31 additions & 0 deletions test/hyperclient_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,36 @@

Hyperclient.new('http://api.example.org')
end

describe 'with an optional block' do
let(:client) do
Hyperclient.new('http://api.example.org') do |client|
client.connection(default: true) do |conn|
conn.use Faraday::Request::OAuth
end
client.headers['Access-Token'] = 'token'
end
end

it 'creates a Faraday connection with the default and additional headers' do
client.headers['Content-Type'].must_equal 'application/json'
client.headers['Accept'].must_equal 'application/json'
client.headers['Access-Token'].must_equal 'token'
end

it 'creates a Faraday connection with the entry point url' do
client.connection.url_prefix.to_s.must_equal 'http://api.example.org/'
end

it 'creates a Faraday connection with the default block plus any additional handlers' do
handlers = client.connection.builder.handlers
handlers.must_include Faraday::Request::OAuth
handlers.must_include Faraday::Response::RaiseError
handlers.must_include FaradayMiddleware::FollowRedirects
handlers.must_include FaradayMiddleware::EncodeJson
handlers.must_include FaradayMiddleware::ParseJson
handlers.must_include Faraday::Adapter::NetHttp
end
end
end
end

0 comments on commit 4350988

Please sign in to comment.