hyperclient 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +0 -1
- data/.rvmrc +1 -1
- data/Gemfile +1 -0
- data/Gemfile.lock +96 -0
- data/Guardfile +7 -0
- data/Rakefile +5 -1
- data/Readme.md +19 -9
- data/features/api_navigation.feature +23 -0
- data/features/default_config.feature +19 -0
- data/features/steps/api_navigation.rb +33 -0
- data/features/steps/default_config.rb +29 -0
- data/features/support/api.rb +24 -0
- data/features/support/env.rb +4 -0
- data/features/support/fixtures.rb +43 -0
- data/hyperclient.gemspec +9 -7
- data/lib/faraday/connection.rb +17 -0
- data/lib/faraday/request/digest_authentication.rb +83 -0
- data/lib/hyperclient/attributes.rb +5 -1
- data/lib/hyperclient/collection.rb +7 -1
- data/lib/hyperclient/entry_point.rb +37 -27
- data/lib/hyperclient/link.rb +41 -16
- data/lib/hyperclient/link_collection.rb +1 -0
- data/lib/hyperclient/resource.rb +8 -3
- data/lib/hyperclient/version.rb +1 -1
- data/lib/hyperclient.rb +11 -3
- data/test/faraday/connection_test.rb +29 -0
- data/test/faraday/digest_authentication_test.rb +41 -0
- data/test/hyperclient/entry_point_test.rb +15 -32
- data/test/hyperclient/link_test.rb +93 -12
- data/test/hyperclient_test.rb +12 -0
- data/test/test_helper.rb +1 -1
- metadata +119 -67
- data/lib/hyperclient/http.rb +0 -165
- data/test/hyperclient/http_test.rb +0 -214
data/lib/hyperclient/link.rb
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
require 'hyperclient/http'
|
2
1
|
require 'hyperclient/resource'
|
3
2
|
require 'uri_template'
|
4
3
|
|
@@ -6,11 +5,6 @@ module Hyperclient
|
|
6
5
|
# Internal: The Link is used to let a Resource interact with the API.
|
7
6
|
#
|
8
7
|
class Link
|
9
|
-
extend Forwardable
|
10
|
-
# Public: Delegate all HTTP methods (get, post, put, delete, options and
|
11
|
-
# head) to the http connection.
|
12
|
-
def_delegators :http, :get, :post, :put, :delete, :options, :head
|
13
|
-
|
14
8
|
# Public: Initializes a new Link.
|
15
9
|
#
|
16
10
|
# link - The String with the URI of the link.
|
@@ -23,15 +17,10 @@ module Hyperclient
|
|
23
17
|
@uri_variables = uri_variables
|
24
18
|
end
|
25
19
|
|
26
|
-
# Public: Returns the Resource which the Link is pointing to.
|
27
|
-
def resource
|
28
|
-
@resource ||=Resource.new(http.get, @entry_point)
|
29
|
-
end
|
30
|
-
|
31
20
|
# Public: Indicates if the link is an URITemplate or a regular URI.
|
32
21
|
#
|
33
22
|
# Returns true if it is templated.
|
34
|
-
# Returns false if it
|
23
|
+
# Returns false if it not templated.
|
35
24
|
def templated?
|
36
25
|
!!@link['templated']
|
37
26
|
end
|
@@ -56,12 +45,48 @@ module Hyperclient
|
|
56
45
|
@url ||= URITemplate.new(@link['href']).expand(@uri_variables)
|
57
46
|
end
|
58
47
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
48
|
+
# Public: Returns the Resource which the Link is pointing to.
|
49
|
+
def resource
|
50
|
+
@resource ||=Resource.new(get.body, @entry_point)
|
51
|
+
end
|
52
|
+
|
53
|
+
def connection
|
54
|
+
@entry_point.connection
|
55
|
+
end
|
56
|
+
|
57
|
+
def get
|
58
|
+
connection.get(url)
|
59
|
+
end
|
60
|
+
|
61
|
+
def options
|
62
|
+
connection.run_request(:options, url, nil, nil)
|
63
63
|
end
|
64
64
|
|
65
|
+
def head
|
66
|
+
connection.head(url)
|
67
|
+
end
|
68
|
+
|
69
|
+
def delete
|
70
|
+
connection.delete(url)
|
71
|
+
end
|
72
|
+
|
73
|
+
def post(params)
|
74
|
+
connection.post(url, params)
|
75
|
+
end
|
76
|
+
|
77
|
+
def put(params)
|
78
|
+
connection.put(url, params)
|
79
|
+
end
|
80
|
+
|
81
|
+
def patch(params)
|
82
|
+
connection.patch(url, params)
|
83
|
+
end
|
84
|
+
|
85
|
+
def inspect
|
86
|
+
"#<#{self.class.name} #{@link}>"
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
65
90
|
# Internal: Delegate the method to the API if it exists.
|
66
91
|
#
|
67
92
|
# This allows `api.links.posts.embedded` instead of
|
@@ -16,6 +16,7 @@ module Hyperclient
|
|
16
16
|
# entry_point - The EntryPoint object to inject the cofnigutation.
|
17
17
|
#
|
18
18
|
def initialize(collection, entry_point)
|
19
|
+
raise "Invalid response for LinkCollection. The response was: #{collection.inspect}" if collection && !collection.respond_to?(:inject)
|
19
20
|
@collection = (collection || {}).inject({}) do |hash, (name, link)|
|
20
21
|
hash.update(name => Link.new(link, entry_point))
|
21
22
|
end
|
data/lib/hyperclient/resource.rb
CHANGED
@@ -19,12 +19,13 @@ module Hyperclient
|
|
19
19
|
attr_reader :embedded
|
20
20
|
|
21
21
|
# Public: Delegate all HTTP methods (get, post, put, delete, options and
|
22
|
-
# head) to its
|
22
|
+
# head) to its self link.
|
23
23
|
def_delegators :self_link, :get, :post, :put, :delete, :options, :head
|
24
24
|
|
25
25
|
# Public: Initializes a Resource.
|
26
|
+
#
|
26
27
|
# representation - The hash with the HAL representation of the Resource.
|
27
|
-
# entry_point - The EntryPoint object to inject the
|
28
|
+
# entry_point - The EntryPoint object to inject the configutation.
|
28
29
|
def initialize(representation, entry_point)
|
29
30
|
@links = LinkCollection.new(representation['_links'], entry_point)
|
30
31
|
@embedded = ResourceCollection.new(representation['_embedded'], entry_point)
|
@@ -32,9 +33,13 @@ module Hyperclient
|
|
32
33
|
@entry_point = entry_point
|
33
34
|
end
|
34
35
|
|
36
|
+
def inspect
|
37
|
+
"#<#{self.class.name} self_link:#{self_link.inspect} attributes:#{@attributes.inspect}>"
|
38
|
+
end
|
39
|
+
|
35
40
|
private
|
36
41
|
# Internal: Returns the self Link of the Resource. Used to handle the HTTP
|
37
|
-
#
|
42
|
+
# methods.
|
38
43
|
def self_link
|
39
44
|
@links['self']
|
40
45
|
end
|
data/lib/hyperclient/version.rb
CHANGED
data/lib/hyperclient.rb
CHANGED
@@ -1,7 +1,15 @@
|
|
1
|
+
require 'hyperclient/entry_point'
|
2
|
+
require "hyperclient/version"
|
3
|
+
|
1
4
|
# Public: Hyperclient namespace.
|
2
5
|
#
|
3
6
|
module Hyperclient
|
7
|
+
# Public: Convenience method to create new EntryPoints.
|
8
|
+
#
|
9
|
+
# url - A String with the url of the API.
|
10
|
+
#
|
11
|
+
# Returns a Hyperclient::EntryPoint
|
12
|
+
def self.new(url)
|
13
|
+
Hyperclient::EntryPoint.new(url)
|
14
|
+
end
|
4
15
|
end
|
5
|
-
|
6
|
-
require 'hyperclient/entry_point'
|
7
|
-
require "hyperclient/version"
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require_relative '../test_helper'
|
2
|
+
require_relative '../../lib/faraday/connection'
|
3
|
+
|
4
|
+
module Faraday
|
5
|
+
describe Connection do
|
6
|
+
describe 'digest_auth' do
|
7
|
+
let(:connection) do
|
8
|
+
Faraday.new('http://api.example.org/') do |builder|
|
9
|
+
builder.request :url_encoded
|
10
|
+
builder.adapter :net_http
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'inserts the DigestAuth middleware at the top' do
|
15
|
+
connection.digest_auth('user', 'password')
|
16
|
+
|
17
|
+
connection.builder.handlers.first.klass.must_equal Faraday::Request::DigestAuth
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'passes the user and password to the middleware' do
|
21
|
+
connection.digest_auth('user', 'password')
|
22
|
+
|
23
|
+
Faraday::Request::DigestAuth.expects(:new).with(anything, 'user', 'password').returns(stub_everything)
|
24
|
+
|
25
|
+
connection.get('/')
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require_relative '../test_helper'
|
2
|
+
require 'faraday/request/digest_authentication'
|
3
|
+
|
4
|
+
module Faraday
|
5
|
+
describe Request::DigestAuth do
|
6
|
+
let(:connection) do
|
7
|
+
Faraday.new('http://api.example.org/') do |builder|
|
8
|
+
builder.request :digest, 'USER', 'PASS'
|
9
|
+
builder.adapter :net_http
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
describe 'when the server does not return a 401' do
|
14
|
+
it 'does nothing' do
|
15
|
+
stub_request(:get, 'http://api.example.org/productions/1').
|
16
|
+
to_return(status: 500, body: 'Foo body')
|
17
|
+
|
18
|
+
response = connection.get('/productions/1')
|
19
|
+
response.body.must_equal 'Foo body'
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe 'when the server returns a 401' do
|
24
|
+
let(:first_call_headers) { 'Digest realm="MyApp", algorithm=MD5' }
|
25
|
+
let(:second_call_headers) { 'Digest username="USER", realm="MyApp", uri="/", algorithm="MD5"' }
|
26
|
+
it 'authenticates using digest' do
|
27
|
+
stub_request(:get, 'http://api.example.org/productions/1').
|
28
|
+
with(body: nil).
|
29
|
+
to_return(status: 401, headers: {'www-authenticate' => first_call_headers})
|
30
|
+
|
31
|
+
stub_request(:get, 'http://api.example.org/productions/1').
|
32
|
+
with(body: "{\"foo\":1}",
|
33
|
+
headers: {'Authorization' => %r{second_call_headers}}).
|
34
|
+
to_return(body: '{"resource": "This is the resource"}',
|
35
|
+
headers: {content_type: 'application/json'})
|
36
|
+
|
37
|
+
connection.get('/productions/1')
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -3,48 +3,31 @@ require 'hyperclient/entry_point'
|
|
3
3
|
|
4
4
|
module Hyperclient
|
5
5
|
describe EntryPoint do
|
6
|
-
let(:
|
6
|
+
let(:entry_point) do
|
7
7
|
EntryPoint.new 'http://my.api.org'
|
8
8
|
end
|
9
9
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
end
|
14
|
-
|
15
|
-
describe 'initialize' do
|
16
|
-
it 'initializes a Resource at the entry point' do
|
17
|
-
api.links['self'].url.must_equal 'http://my.api.org'
|
10
|
+
describe 'connection' do
|
11
|
+
it 'creates a Faraday connection with the entry point url' do
|
12
|
+
entry_point.connection.url_prefix.to_s.must_equal 'http://my.api.org/'
|
18
13
|
end
|
19
14
|
|
20
|
-
it '
|
21
|
-
|
22
|
-
|
23
|
-
api = EntryPoint.new('http://my.api.org', options)
|
24
|
-
|
25
|
-
api.config[:headers].must_include 'accept-encoding'
|
15
|
+
it 'creates a Faraday connection with the default headers' do
|
16
|
+
entry_point.headers['Content-Type'].must_equal 'application/json'
|
17
|
+
entry_point.headers['Accept'].must_equal 'application/json'
|
26
18
|
end
|
27
19
|
|
28
|
-
it '
|
29
|
-
|
30
|
-
|
31
|
-
|
20
|
+
it 'creates a Faraday connection with the default block' do
|
21
|
+
handlers = entry_point.connection.builder.handlers
|
22
|
+
handlers.must_include FaradayMiddleware::EncodeJson
|
23
|
+
handlers.must_include FaradayMiddleware::ParseJson
|
24
|
+
handlers.must_include Faraday::Adapter::NetHttp
|
32
25
|
end
|
33
26
|
end
|
34
27
|
|
35
|
-
describe '
|
36
|
-
it '
|
37
|
-
|
38
|
-
api.foo.must_equal 'foo'
|
39
|
-
end
|
40
|
-
|
41
|
-
it 'responds to missing methods' do
|
42
|
-
Resource.any_instance.expects(:respond_to?).with('foo').returns(true)
|
43
|
-
api.respond_to?(:foo).must_equal true
|
44
|
-
end
|
45
|
-
|
46
|
-
it 'raises an error when the method does not exist in the API' do
|
47
|
-
lambda { api.this_method_does_not_exist }.must_raise(NoMethodError)
|
28
|
+
describe 'initialize' do
|
29
|
+
it 'sets a Link with the entry point url' do
|
30
|
+
entry_point.url.must_equal 'http://my.api.org'
|
48
31
|
end
|
49
32
|
end
|
50
33
|
end
|
@@ -1,9 +1,12 @@
|
|
1
1
|
require_relative '../test_helper'
|
2
2
|
require 'hyperclient/link'
|
3
|
+
require 'hyperclient/entry_point'
|
3
4
|
|
4
5
|
module Hyperclient
|
5
6
|
describe Link do
|
6
|
-
let(:entry_point)
|
7
|
+
let(:entry_point) do
|
8
|
+
EntryPoint.new('http://api.example.org/')
|
9
|
+
end
|
7
10
|
|
8
11
|
describe 'templated?' do
|
9
12
|
it 'returns true if the link is templated' do
|
@@ -19,17 +22,6 @@ module Hyperclient
|
|
19
22
|
end
|
20
23
|
end
|
21
24
|
|
22
|
-
describe 'resource' do
|
23
|
-
let(:http) { mock('HTTP', get: {}) }
|
24
|
-
|
25
|
-
it 'builds a resource with the hyperlink representation' do
|
26
|
-
HTTP.expects(:new).returns(http, {})
|
27
|
-
Resource.expects(:new).with({}, entry_point)
|
28
|
-
|
29
|
-
Link.new({}, entry_point).resource
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
25
|
describe 'expand' do
|
34
26
|
it 'buils a Link with the templated URI representation' do
|
35
27
|
link = Link.new({'href' => '/orders{?id}', 'templated' => true}, entry_point)
|
@@ -64,6 +56,95 @@ module Hyperclient
|
|
64
56
|
end
|
65
57
|
end
|
66
58
|
|
59
|
+
describe 'resource' do
|
60
|
+
it 'builds a resource with the link href representation' do
|
61
|
+
Resource.expects(:new).with({}, entry_point)
|
62
|
+
|
63
|
+
link = Link.new({'href' => '/'}, entry_point)
|
64
|
+
link.expects(:get).returns(mock(body: {}))
|
65
|
+
|
66
|
+
link.resource
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe 'connection' do
|
71
|
+
it 'returns the entry point connection' do
|
72
|
+
Link.new({}, entry_point).connection.must_equal entry_point.connection
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe 'get' do
|
77
|
+
it 'sends a GET request with the link url' do
|
78
|
+
link = Link.new({'href' => '/productions/1'}, entry_point)
|
79
|
+
|
80
|
+
entry_point.connection.expects(:get).with('/productions/1')
|
81
|
+
link.get
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
describe 'options' do
|
86
|
+
it 'sends a OPTIONS request with the link url' do
|
87
|
+
link = Link.new({'href' => '/productions/1'}, entry_point)
|
88
|
+
|
89
|
+
entry_point.connection.expects(:run_request).with(:options, '/productions/1', nil, nil)
|
90
|
+
link.options
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
describe 'head' do
|
95
|
+
it 'sends a HEAD request with the link url' do
|
96
|
+
link = Link.new({'href' => '/productions/1'}, entry_point)
|
97
|
+
|
98
|
+
entry_point.connection.expects(:head).with('/productions/1')
|
99
|
+
link.head
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
describe 'delete' do
|
104
|
+
it 'sends a DELETE request with the link url' do
|
105
|
+
link = Link.new({'href' => '/productions/1'}, entry_point)
|
106
|
+
|
107
|
+
entry_point.connection.expects(:delete).with('/productions/1')
|
108
|
+
link.delete
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
describe 'post' do
|
113
|
+
it 'sends a POST request with the link url and params' do
|
114
|
+
link = Link.new({'href' => '/productions/1'}, entry_point)
|
115
|
+
|
116
|
+
entry_point.connection.expects(:post).with('/productions/1', {'foo' => 'bar'})
|
117
|
+
link.post({'foo' => 'bar'})
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
describe 'put' do
|
122
|
+
it 'sends a PUT request with the link url and params' do
|
123
|
+
link = Link.new({'href' => '/productions/1'}, entry_point)
|
124
|
+
|
125
|
+
entry_point.connection.expects(:put).with('/productions/1', {'foo' => 'bar'})
|
126
|
+
link.put({'foo' => 'bar'})
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
describe 'patch' do
|
131
|
+
it 'sends a PATCH request with the link url and params' do
|
132
|
+
link = Link.new({'href' => '/productions/1'}, entry_point)
|
133
|
+
|
134
|
+
entry_point.connection.expects(:patch).with('/productions/1', {'foo' => 'bar'})
|
135
|
+
link.patch({'foo' => 'bar'})
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
describe 'inspect' do
|
140
|
+
it 'outputs a custom-friendly output' do
|
141
|
+
link = Link.new({'href'=>'/productions/1'}, 'foo')
|
142
|
+
|
143
|
+
link.inspect.must_include 'Link'
|
144
|
+
link.inspect.must_include '"href"=>"/productions/1"'
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
67
148
|
describe 'method_missing' do
|
68
149
|
before do
|
69
150
|
stub_request(:get, "http://myapi.org/orders").
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'hyperclient'
|
3
|
+
|
4
|
+
describe Hyperclient do
|
5
|
+
describe 'new' do
|
6
|
+
it 'creates a new EntryPoint with the url' do
|
7
|
+
Hyperclient::EntryPoint.expects(:new).with('http://api.example.org')
|
8
|
+
|
9
|
+
Hyperclient.new('http://api.example.org')
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|