hyperclient 0.0.8 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +2 -1
- data/Readme.md +15 -21
- data/examples/cyberscore.rb +28 -16
- data/examples/hal_shop.rb +15 -22
- data/hyperclient.gemspec +2 -0
- data/lib/hyperclient.rb +2 -84
- data/lib/hyperclient/attributes.rb +20 -0
- data/lib/hyperclient/collection.rb +53 -0
- data/lib/hyperclient/entry_point.rb +49 -0
- data/lib/hyperclient/http.rb +38 -37
- data/lib/hyperclient/link.rb +93 -0
- data/lib/hyperclient/link_collection.rb +24 -0
- data/lib/hyperclient/resource.rb +26 -65
- data/lib/hyperclient/resource_collection.rb +33 -0
- data/lib/hyperclient/version.rb +1 -1
- data/test/fixtures/element.json +4 -15
- data/test/hyperclient/attributes_test.rb +26 -0
- data/test/hyperclient/collection_test.rb +39 -0
- data/test/hyperclient/entry_point_test.rb +51 -0
- data/test/hyperclient/http_test.rb +37 -23
- data/test/hyperclient/link_collection_test.rb +29 -0
- data/test/hyperclient/link_test.rb +93 -0
- data/test/hyperclient/resource_collection_test.rb +34 -0
- data/test/hyperclient/resource_test.rb +36 -42
- data/test/test_helper.rb +10 -1
- metadata +55 -16
- data/lib/hyperclient/discoverer.rb +0 -84
- data/lib/hyperclient/representation.rb +0 -43
- data/lib/hyperclient/resource_factory.rb +0 -53
- data/test/hyperclient/discoverer_test.rb +0 -101
- data/test/hyperclient/representation_test.rb +0 -52
- data/test/hyperclient/resource_factory_test.rb +0 -32
- data/test/hyperclient_test.rb +0 -68
@@ -0,0 +1,39 @@
|
|
1
|
+
require_relative '../test_helper'
|
2
|
+
require 'hyperclient/resource'
|
3
|
+
|
4
|
+
module Hyperclient
|
5
|
+
describe Collection do
|
6
|
+
let(:representation) do
|
7
|
+
JSON.parse( File.read('test/fixtures/element.json'))
|
8
|
+
end
|
9
|
+
|
10
|
+
let(:collection) do
|
11
|
+
Collection.new(representation)
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'exposes the collection as methods' do
|
15
|
+
collection.title.must_equal 'Real World ASP.NET MVC3'
|
16
|
+
collection.description.must_match(/production/)
|
17
|
+
collection.permitted.must_equal true
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'exposes collection as a hash' do
|
21
|
+
collection['title'].must_equal 'Real World ASP.NET MVC3'
|
22
|
+
collection['description'].must_match(/production/)
|
23
|
+
collection['permitted'].must_equal true
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'correctly responds to methods' do
|
27
|
+
collection.must_respond_to :title
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'acts as enumerable' do
|
31
|
+
names = collection.map do |name, value|
|
32
|
+
name
|
33
|
+
end
|
34
|
+
|
35
|
+
names.must_equal ['_links', 'title', 'description', 'permitted', '_embedded']
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require_relative '../test_helper'
|
2
|
+
require 'hyperclient/entry_point'
|
3
|
+
|
4
|
+
module Hyperclient
|
5
|
+
describe EntryPoint do
|
6
|
+
let(:api) do
|
7
|
+
EntryPoint.new 'http://my.api.org'
|
8
|
+
end
|
9
|
+
|
10
|
+
before do
|
11
|
+
stub_request(:get, "http://my.api.org/").
|
12
|
+
to_return(body: '{"_links": {"self": {"href": "http://my.api.org"}}}', headers: {content_type: 'application/json'})
|
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'
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'setups the HTTP config' do
|
21
|
+
options = {:headers => {'accept-encoding' => 'deflate, gzip'}}
|
22
|
+
|
23
|
+
api = EntryPoint.new('http://my.api.org', options)
|
24
|
+
|
25
|
+
api.config[:headers].must_include 'accept-encoding'
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'sets the base_uri for HTTP' do
|
29
|
+
api = EntryPoint.new('http://my.api.org')
|
30
|
+
|
31
|
+
api.config[:base_uri].must_equal 'http://my.api.org'
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe 'method missing' do
|
36
|
+
it 'delegates undefined methods to the API when they exist' do
|
37
|
+
Resource.any_instance.expects(:foo).returns 'foo'
|
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)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -3,33 +3,47 @@ require 'hyperclient/http'
|
|
3
3
|
|
4
4
|
module Hyperclient
|
5
5
|
describe HTTP do
|
6
|
-
let
|
7
|
-
|
8
|
-
resource.expect(:url, 'http://api.example.org/productions/1')
|
6
|
+
let(:url) do
|
7
|
+
'/productions/1'
|
9
8
|
end
|
10
9
|
|
11
|
-
let
|
10
|
+
let(:config) { {base_uri: 'http://api.example.org'} }
|
11
|
+
|
12
|
+
let(:http) do
|
12
13
|
HTTP.instance_variable_set("@default_options", {})
|
13
|
-
HTTP.new(
|
14
|
+
HTTP.new(url, config)
|
15
|
+
end
|
16
|
+
|
17
|
+
describe 'url' do
|
18
|
+
it 'merges the resource url with the base uri' do
|
19
|
+
http.url.to_s.must_equal 'http://api.example.org/productions/1'
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'returns the given url if it cannot merge it' do
|
23
|
+
config = {base_uri: nil}
|
24
|
+
http = HTTP.new(url, config)
|
25
|
+
http.url.to_s.must_equal '/productions/1'
|
26
|
+
end
|
14
27
|
end
|
15
28
|
|
16
29
|
describe 'authentication' do
|
17
30
|
it 'sets the authentication options' do
|
18
|
-
stub_request(:get, 'user:pass@api.example.org/productions/1').
|
19
|
-
to_return(body: 'This is the resource')
|
31
|
+
stub_request(:get, 'http://user:pass@api.example.org/productions/1').
|
32
|
+
to_return(body: '{"resource": "This is the resource"}')
|
33
|
+
|
34
|
+
config.update({auth: {type: :basic, user: 'user', password: 'pass'}})
|
20
35
|
|
21
|
-
http
|
22
|
-
http.get.must_equal 'This is the resource'
|
36
|
+
http.get.must_equal({'resource' => 'This is the resource'})
|
23
37
|
end
|
24
38
|
end
|
25
39
|
|
26
40
|
describe 'headers' do
|
27
41
|
it 'sets headers from the given option' do
|
28
|
-
|
42
|
+
config.update({headers: {'accept-encoding' => 'deflate, gzip'}})
|
29
43
|
|
30
|
-
stub_request(:get, 'api.example.org/productions/1').
|
44
|
+
stub_request(:get, 'http://api.example.org/productions/1').
|
31
45
|
with(headers: {'Accept-Encoding' => 'deflate, gzip'}).
|
32
|
-
to_return(body: 'This is the resource')
|
46
|
+
to_return(body: '{"resource": "This is the resource"}')
|
33
47
|
|
34
48
|
http.get
|
35
49
|
end
|
@@ -37,14 +51,14 @@ module Hyperclient
|
|
37
51
|
|
38
52
|
describe 'debug' do
|
39
53
|
it 'enables debugging' do
|
40
|
-
|
54
|
+
config.update({debug: true})
|
41
55
|
|
42
56
|
http.class.instance_variable_get(:@default_options)[:debug_output].must_equal $stderr
|
43
57
|
end
|
44
58
|
|
45
59
|
it 'uses a custom stream' do
|
46
60
|
stream = StringIO.new
|
47
|
-
|
61
|
+
config.update({debug: stream})
|
48
62
|
|
49
63
|
http.class.instance_variable_get(:@default_options)[:debug_output].must_equal stream
|
50
64
|
end
|
@@ -52,14 +66,14 @@ module Hyperclient
|
|
52
66
|
|
53
67
|
describe 'get' do
|
54
68
|
it 'sends a GET request and returns the response body' do
|
55
|
-
stub_request(:get, 'api.example.org/productions/1').
|
56
|
-
to_return(body: 'This is the resource')
|
69
|
+
stub_request(:get, 'http://api.example.org/productions/1').
|
70
|
+
to_return(body: '{"resource": "This is the resource"}')
|
57
71
|
|
58
|
-
http.get.must_equal 'This is the resource'
|
72
|
+
http.get.must_equal({'resource' => 'This is the resource'})
|
59
73
|
end
|
60
74
|
|
61
75
|
it 'returns the parsed response' do
|
62
|
-
stub_request(:get, 'api.example.org/productions/1').
|
76
|
+
stub_request(:get, 'http://api.example.org/productions/1').
|
63
77
|
to_return(body: '{"some_json": 12345 }', headers: {content_type: 'application/json'})
|
64
78
|
|
65
79
|
http.get.must_equal({'some_json' => 12345})
|
@@ -68,7 +82,7 @@ module Hyperclient
|
|
68
82
|
|
69
83
|
describe 'post' do
|
70
84
|
it 'sends a POST request' do
|
71
|
-
stub_request(:post, 'api.example.org/productions/1').
|
85
|
+
stub_request(:post, 'http://api.example.org/productions/1').
|
72
86
|
to_return(body: 'Posting like a big boy huh?', status: 201)
|
73
87
|
|
74
88
|
response = http.post({data: 'foo'})
|
@@ -80,7 +94,7 @@ module Hyperclient
|
|
80
94
|
|
81
95
|
describe 'put' do
|
82
96
|
it 'sends a PUT request' do
|
83
|
-
stub_request(:put, 'api.example.org/productions/1').
|
97
|
+
stub_request(:put, 'http://api.example.org/productions/1').
|
84
98
|
to_return(body: 'No changes were made', status: 204)
|
85
99
|
|
86
100
|
response = http.put({attribute: 'changed'})
|
@@ -92,7 +106,7 @@ module Hyperclient
|
|
92
106
|
|
93
107
|
describe 'options' do
|
94
108
|
it 'sends a OPTIONS request' do
|
95
|
-
stub_request(:options, 'api.example.org/productions/1').
|
109
|
+
stub_request(:options, 'http://api.example.org/productions/1').
|
96
110
|
to_return(status: 200, headers: {allow: 'GET, POST'})
|
97
111
|
|
98
112
|
response = http.options
|
@@ -102,7 +116,7 @@ module Hyperclient
|
|
102
116
|
|
103
117
|
describe 'head' do
|
104
118
|
it 'sends a HEAD request' do
|
105
|
-
stub_request(:head, 'api.example.org/productions/1').
|
119
|
+
stub_request(:head, 'http://api.example.org/productions/1').
|
106
120
|
to_return(status: 200, headers: {content_type: 'application/json'})
|
107
121
|
|
108
122
|
response = http.head
|
@@ -112,7 +126,7 @@ module Hyperclient
|
|
112
126
|
|
113
127
|
describe 'delete' do
|
114
128
|
it 'sends a DELETE request' do
|
115
|
-
stub_request(:delete, 'api.example.org/productions/1').
|
129
|
+
stub_request(:delete, 'http://api.example.org/productions/1').
|
116
130
|
to_return(body: 'Resource deleted', status: 200)
|
117
131
|
|
118
132
|
response = http.delete
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require_relative '../test_helper'
|
2
|
+
require 'hyperclient/link_collection'
|
3
|
+
|
4
|
+
module Hyperclient
|
5
|
+
describe LinkCollection do
|
6
|
+
let(:entry_point) { stub('Entry point', config: {base_uri: '/'}) }
|
7
|
+
|
8
|
+
let(:representation) do
|
9
|
+
JSON.parse( File.read('test/fixtures/element.json'))
|
10
|
+
end
|
11
|
+
|
12
|
+
let(:links) do
|
13
|
+
LinkCollection.new(representation['_links'], entry_point)
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'is a collection' do
|
17
|
+
LinkCollection.ancestors.must_include Collection
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'initializes the collection with links' do
|
21
|
+
links.must_respond_to :filter
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'returns link objects for each link' do
|
25
|
+
links.filter.must_be_kind_of Link
|
26
|
+
links['self'].must_be_kind_of Link
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require_relative '../test_helper'
|
2
|
+
require 'hyperclient/link'
|
3
|
+
|
4
|
+
module Hyperclient
|
5
|
+
describe Link do
|
6
|
+
let(:entry_point) { stub('Entry point', config: {base_uri: '/'}) }
|
7
|
+
|
8
|
+
describe 'templated?' do
|
9
|
+
it 'returns true if the link is templated' do
|
10
|
+
link = Link.new({'templated' => true}, entry_point)
|
11
|
+
|
12
|
+
link.templated?.must_equal true
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'returns false if the link is not templated' do
|
16
|
+
link = Link.new({}, entry_point)
|
17
|
+
|
18
|
+
link.templated?.must_equal false
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
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
|
+
describe 'expand' do
|
34
|
+
it 'buils a Link with the templated URI representation' do
|
35
|
+
link = Link.new({'href' => '/orders{?id}', 'templated' => true}, entry_point)
|
36
|
+
|
37
|
+
Link.expects(:new).with(anything, entry_point, {id: '1'})
|
38
|
+
link.expand(id: '1')
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'raises if no uri variables are given' do
|
42
|
+
link = Link.new({'href' => '/orders{?id}', 'templated' => true}, entry_point)
|
43
|
+
|
44
|
+
proc { link.resource }.must_raise MissingURITemplateVariablesException
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe 'url' do
|
49
|
+
it 'raises when missing required uri_variables' do
|
50
|
+
link = Link.new({'href' => '/orders{?id}', 'templated' => true}, entry_point)
|
51
|
+
|
52
|
+
lambda { link.url }.must_raise MissingURITemplateVariablesException
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'expands an uri template with variables' do
|
56
|
+
link = Link.new({'href' => '/orders{?id}', 'templated' => true}, entry_point, {id: 1})
|
57
|
+
|
58
|
+
link.url.must_equal '/orders?id=1'
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'returns the link when no uri template' do
|
62
|
+
link = Link.new({'href' => '/orders'}, entry_point)
|
63
|
+
link.url.must_equal '/orders'
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
describe 'method_missing' do
|
68
|
+
before do
|
69
|
+
stub_request(:get, "http://myapi.org/orders").
|
70
|
+
to_return(body: '{"resource": "This is the resource"}')
|
71
|
+
Resource.expects(:new).returns(resource).at_least_once
|
72
|
+
end
|
73
|
+
|
74
|
+
let(:link) { Link.new({'href' => 'http://myapi.org/orders'}, entry_point) }
|
75
|
+
let(:resource) { mock('Resource') }
|
76
|
+
|
77
|
+
it 'delegates unkown methods to the resource' do
|
78
|
+
resource.expects(:embedded)
|
79
|
+
|
80
|
+
link.embedded
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'raises an error when the method does not exist in the resource' do
|
84
|
+
lambda { link.this_method_does_not_exist }.must_raise(NoMethodError)
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'responds to missing methods' do
|
88
|
+
resource.expects(:respond_to?).with('embedded').returns(true)
|
89
|
+
link.respond_to?(:embedded).must_equal true
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require_relative '../test_helper'
|
2
|
+
require 'hyperclient/resource_collection'
|
3
|
+
|
4
|
+
module Hyperclient
|
5
|
+
describe ResourceCollection do
|
6
|
+
let(:entry_point) { stub('Entry point', config: {base_uri: '/'}) }
|
7
|
+
|
8
|
+
let(:representation) do
|
9
|
+
JSON.parse( File.read('test/fixtures/element.json'))
|
10
|
+
end
|
11
|
+
|
12
|
+
let(:resources) do
|
13
|
+
ResourceCollection.new(representation['_embedded'], entry_point)
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'is a collection' do
|
17
|
+
ResourceCollection.ancestors.must_include Collection
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'initializes the collection with resources' do
|
21
|
+
resources.must_respond_to :author
|
22
|
+
resources.must_respond_to :episodes
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'returns resource objects for each resource' do
|
26
|
+
resources.author.must_be_kind_of Resource
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'also builds arras of resource' do
|
30
|
+
resources.episodes.must_be_kind_of Array
|
31
|
+
resources.episodes.first.must_be_kind_of Resource
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -3,66 +3,60 @@ require 'hyperclient/resource'
|
|
3
3
|
|
4
4
|
module Hyperclient
|
5
5
|
describe Resource do
|
6
|
-
let(:
|
7
|
-
File.read('test/fixtures/element.json')
|
8
|
-
end
|
9
|
-
|
10
|
-
let(:parsed_representation) do
|
11
|
-
JSON.parse(representation)
|
12
|
-
end
|
6
|
+
let(:entry_point) { mock('Entry point') }
|
13
7
|
|
14
|
-
|
15
|
-
|
16
|
-
|
8
|
+
describe 'initialize' do
|
9
|
+
it 'initializes its links' do
|
10
|
+
LinkCollection.expects(:new).with({"self" => { "href" => "/orders/523" }}, entry_point)
|
17
11
|
|
18
|
-
|
19
|
-
it 'merges the resource url with the entry point' do
|
20
|
-
resource = Resource.new('/path/to/resource')
|
21
|
-
resource.url.to_s.must_equal 'http://api.example.org/path/to/resource'
|
12
|
+
Resource.new({'_links' => {"self" => { "href" => "/orders/523" } }}, entry_point)
|
22
13
|
end
|
23
14
|
|
24
|
-
it '
|
25
|
-
|
26
|
-
resource.url.to_s.must_equal '/search={terms}'
|
27
|
-
end
|
28
|
-
end
|
15
|
+
it 'initializes its attributes' do
|
16
|
+
Attributes.expects(:new).with({foo: :bar})
|
29
17
|
|
30
|
-
|
31
|
-
before do
|
32
|
-
stub_request(:get, 'http://api.example.org')
|
18
|
+
Resource.new({foo: :bar}, entry_point)
|
33
19
|
end
|
34
20
|
|
35
|
-
it 'initializes
|
36
|
-
|
21
|
+
it 'initializes links' do
|
22
|
+
ResourceCollection.expects(:new).with({"orders" => []}, entry_point)
|
37
23
|
|
38
|
-
|
24
|
+
Resource.new({'_embedded' => {"orders" => [] }}, entry_point)
|
39
25
|
end
|
26
|
+
end
|
40
27
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
resource.name.must_equal 'posts'
|
28
|
+
describe 'accessors' do
|
29
|
+
let(:resource) do
|
30
|
+
Resource.new({}, entry_point)
|
45
31
|
end
|
46
|
-
end
|
47
32
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
33
|
+
describe 'links' do
|
34
|
+
it 'returns a LinkCollection' do
|
35
|
+
resource.links.must_be_kind_of LinkCollection
|
36
|
+
end
|
52
37
|
end
|
53
38
|
|
54
|
-
|
55
|
-
|
56
|
-
|
39
|
+
describe 'attributes' do
|
40
|
+
it 'returns a Attributes' do
|
41
|
+
resource.attributes.must_be_kind_of Attributes
|
42
|
+
end
|
43
|
+
end
|
57
44
|
|
58
|
-
|
45
|
+
describe 'embedded' do
|
46
|
+
it 'returns a ResourceCollection' do
|
47
|
+
resource.embedded.must_be_kind_of ResourceCollection
|
48
|
+
end
|
59
49
|
end
|
50
|
+
end
|
60
51
|
|
61
|
-
|
62
|
-
|
52
|
+
it 'uses its self Link to handle HTTP connections' do
|
53
|
+
self_link = mock('Self Link')
|
54
|
+
self_link.expects(:get)
|
63
55
|
|
64
|
-
|
65
|
-
|
56
|
+
LinkCollection.expects(:new).returns({'self' => self_link})
|
57
|
+
resource = Resource.new({}, entry_point)
|
58
|
+
|
59
|
+
resource.get
|
66
60
|
end
|
67
61
|
end
|
68
62
|
end
|