api_navigator 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +26 -0
- data/.yardopts +8 -0
- data/CHANGELOG.md +3 -0
- data/CONTRIBUTING.md +4 -0
- data/Dangerfile +2 -0
- data/Gemfile +23 -0
- data/Guardfile +5 -0
- data/LICENSE +22 -0
- data/README.md +5 -0
- data/RELEASING.md +4 -0
- data/Rakefile +34 -0
- data/UPGRADING.md +4 -0
- data/api_navigator.gemspec +23 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/fixtures_2.rb +253 -0
- data/lib/api_navigator.rb +45 -0
- data/lib/api_navigator/attributes.rb +20 -0
- data/lib/api_navigator/collection_hash.rb +90 -0
- data/lib/api_navigator/curie.rb +47 -0
- data/lib/api_navigator/entry_point.rb +157 -0
- data/lib/api_navigator/link.rb +160 -0
- data/lib/api_navigator/link_collection.rb +63 -0
- data/lib/api_navigator/resource.rb +130 -0
- data/lib/api_navigator/resources/collection_resource.rb +41 -0
- data/lib/api_navigator/resources/member_resource.rb +63 -0
- data/lib/api_navigator/version.rb +3 -0
- data/lib/faraday/connection.rb +17 -0
- data/spec/fixtures/requests.rb +157 -0
- data/spec/fixtures/sample.json +108 -0
- data/spec/lib/api_navigator/attribute_spec.rb +36 -0
- data/spec/lib/api_navigator/collection_hash_spec.rb +71 -0
- data/spec/lib/api_navigator/entry_point_spec.rb +185 -0
- data/spec/lib/api_navigator/link_collection_spec.rb +77 -0
- data/spec/lib/api_navigator/link_spec.rb +343 -0
- data/spec/lib/api_navigator/resource_spec.rb +368 -0
- data/spec/spec_helper.rb +112 -0
- data/spec/support/book_resource.rb +10 -0
- data/spec/support/request_helper.rb +8 -0
- metadata +172 -0
@@ -0,0 +1,108 @@
|
|
1
|
+
# root
|
2
|
+
|
3
|
+
data: {},
|
4
|
+
links: {},
|
5
|
+
meta: {},
|
6
|
+
errors*: []
|
7
|
+
|
8
|
+
|
9
|
+
# singular
|
10
|
+
|
11
|
+
data: {
|
12
|
+
atr1:
|
13
|
+
atr2:
|
14
|
+
nested: {
|
15
|
+
|
16
|
+
}
|
17
|
+
},
|
18
|
+
links: {
|
19
|
+
self: "self-url"
|
20
|
+
}
|
21
|
+
|
22
|
+
|
23
|
+
# multiple
|
24
|
+
|
25
|
+
data: [
|
26
|
+
|
27
|
+
]
|
28
|
+
},
|
29
|
+
links: {
|
30
|
+
self: "self-url"
|
31
|
+
}
|
32
|
+
|
33
|
+
# http://localhost:3000/sample/book/1?include=[publisher,authors]&render=compact
|
34
|
+
data: {
|
35
|
+
title: 'Book 1',
|
36
|
+
body: 'Book 1 Body',
|
37
|
+
year: 1999,
|
38
|
+
publisher: {
|
39
|
+
data: {
|
40
|
+
name: 'Manning Publisher',
|
41
|
+
},
|
42
|
+
_links: {
|
43
|
+
self: {
|
44
|
+
href: "http://localhost:3000/sample/publisher/1",
|
45
|
+
}
|
46
|
+
},
|
47
|
+
_meta: { type: "book" }
|
48
|
+
},
|
49
|
+
authors: {
|
50
|
+
data: [
|
51
|
+
# author1,
|
52
|
+
# author2
|
53
|
+
],
|
54
|
+
_links: {},
|
55
|
+
_meta: {}
|
56
|
+
}
|
57
|
+
},
|
58
|
+
_links: {
|
59
|
+
self: {
|
60
|
+
href: "http://localhost:3000/sample/books/1",
|
61
|
+
},
|
62
|
+
authors: {
|
63
|
+
href: "http://localhost:3000/sample/book/1/authors",
|
64
|
+
},
|
65
|
+
publisher: {
|
66
|
+
href: "http://localhost:3000/sample/book/1/publisher",
|
67
|
+
},
|
68
|
+
},
|
69
|
+
_meta: { type: "book" }
|
70
|
+
|
71
|
+
|
72
|
+
data: {
|
73
|
+
title: 'Book 1',
|
74
|
+
body: 'Book 1 Body',
|
75
|
+
year: 1999,
|
76
|
+
_relations: {
|
77
|
+
publisher: {
|
78
|
+
data: {
|
79
|
+
name: 'Manning Publisher',
|
80
|
+
},
|
81
|
+
_links: {
|
82
|
+
self: {
|
83
|
+
href: "http://localhost:3000/sample/publisher/1",
|
84
|
+
}
|
85
|
+
},
|
86
|
+
_meta: { type: "book" }
|
87
|
+
},
|
88
|
+
authors: {
|
89
|
+
data: [
|
90
|
+
# author1,
|
91
|
+
# author2
|
92
|
+
],
|
93
|
+
_links: {},
|
94
|
+
_meta: {}
|
95
|
+
}
|
96
|
+
},
|
97
|
+
_links: {
|
98
|
+
self: {
|
99
|
+
href: "http://localhost:3000/sample/books/1",
|
100
|
+
},
|
101
|
+
authors: {
|
102
|
+
href: "http://localhost:3000/sample/book/1/authors",
|
103
|
+
},
|
104
|
+
publisher: {
|
105
|
+
href: "http://localhost:3000/sample/book/1/publisher",
|
106
|
+
},
|
107
|
+
},
|
108
|
+
_meta: { type: "book" }
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module ApiNavigator
|
4
|
+
describe Attributes do
|
5
|
+
let(:representation) do
|
6
|
+
JSON.parse(Fixtures::Requests.book_response)
|
7
|
+
end
|
8
|
+
|
9
|
+
let(:attributes) do
|
10
|
+
Attributes.new(representation.fetch('data'))
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'does not set _links as an attribute' do
|
14
|
+
expect(attributes).not_to respond_to :_links
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'does not set _meta as an attribute' do
|
18
|
+
expect(attributes).not_to respond_to :_meta
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'sets normal attributes' do
|
22
|
+
expect(attributes).to respond_to :title
|
23
|
+
expect(attributes.title).to be == "Book 1"
|
24
|
+
|
25
|
+
expect(attributes).to respond_to :body
|
26
|
+
expect(attributes.body).to be == 'Book 1 Body'
|
27
|
+
|
28
|
+
expect(attributes).to respond_to :year
|
29
|
+
expect(attributes.year).to be == 1999
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'is a collection' do
|
33
|
+
expect(Attributes.ancestors).to include(CollectionHash)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module ApiNavigator
|
4
|
+
describe CollectionHash do
|
5
|
+
let(:representation) do
|
6
|
+
JSON.parse(Fixtures::Requests.book_response)
|
7
|
+
end
|
8
|
+
|
9
|
+
let(:collection) do
|
10
|
+
CollectionHash.new(representation)
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'exposes the collection as methods' do
|
14
|
+
expect(collection.data['title']).to be == 'Book 1'
|
15
|
+
expect(collection.data).to be_kind_of Hash
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'exposes collection as a hash' do
|
19
|
+
expect(collection['data']['body']).to be == 'Book 1 Body'
|
20
|
+
expect(collection['data']).to be_kind_of Hash
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'correctly responds to methods' do
|
24
|
+
expect(collection).to respond_to :data
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'acts as enumerable' do
|
28
|
+
names = collection.map do |name, _value|
|
29
|
+
name
|
30
|
+
end
|
31
|
+
expect(names).to include(*%w[data _links _meta])
|
32
|
+
end
|
33
|
+
|
34
|
+
describe '#to_hash' do
|
35
|
+
it 'returns the wrapped collection as a hash' do
|
36
|
+
expect(collection.to_hash).to be_kind_of Hash
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe 'include?' do
|
41
|
+
it 'returns true for keys that exist' do
|
42
|
+
expect(collection.include?('_links')).to be_truthy
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'returns false for missing keys' do
|
46
|
+
expect(collection.include?('missing key')).to be_falsey
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe '#fetch' do
|
51
|
+
it 'returns the value for keys that exist' do
|
52
|
+
expect(collection.fetch('data')).to be == representation['data']
|
53
|
+
expect(collection.fetch('data').fetch('title')).to be == 'Book 1'
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'raises an error for missing keys' do
|
57
|
+
expect { collection.fetch('missing key') }.to raise_error KeyError
|
58
|
+
end
|
59
|
+
|
60
|
+
describe 'with a default value' do
|
61
|
+
it 'returns the value for keys that exist' do
|
62
|
+
expect(collection.fetch('data', 'a_string')).to be_kind_of Hash
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'returns the default value for missing keys' do
|
66
|
+
expect(collection.fetch('missing key', 'default')).to be == 'default'
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,185 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module ApiNavigator
|
4
|
+
describe EntryPoint do
|
5
|
+
describe 'default' do
|
6
|
+
let(:entry_point) do
|
7
|
+
EntryPoint.new 'http://my.api.org'
|
8
|
+
end
|
9
|
+
|
10
|
+
describe 'connection' do
|
11
|
+
it 'creates a Faraday connection with the entry point url' do
|
12
|
+
expect(entry_point.connection.url_prefix.to_s).to be == 'http://my.api.org/'
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'creates a Faraday connection with the default headers' do
|
16
|
+
expect(entry_point.headers['Content-Type']).to be == 'application/hal+json'
|
17
|
+
expect(entry_point.headers['Accept']).to be == 'application/hal+json,application/json'
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'can update headers after a connection has been constructed' do
|
21
|
+
expect(entry_point.connection).to be_kind_of(Faraday::Connection)
|
22
|
+
entry_point.headers.update('Content-Type' => 'application/foobar')
|
23
|
+
expect(entry_point.headers['Content-Type']).to be == 'application/foobar'
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'can insert additional middleware after a connection has been constructed' do
|
27
|
+
expect(entry_point.connection).to be_kind_of(Faraday::Connection)
|
28
|
+
entry_point.connection.use :instrumentation
|
29
|
+
handlers = entry_point.connection.builder.handlers
|
30
|
+
expect(handlers).to include FaradayMiddleware::Instrumentation
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'creates a Faraday connection with the default block' do
|
34
|
+
handlers = entry_point.connection.builder.handlers
|
35
|
+
|
36
|
+
expect(handlers).to include Faraday::Response::RaiseError
|
37
|
+
expect(handlers).to include FaradayMiddleware::FollowRedirects
|
38
|
+
expect(handlers).to include FaradayMiddleware::EncodeHalJson
|
39
|
+
expect(handlers).to include FaradayMiddleware::ParseHalJson
|
40
|
+
expect(handlers).to include Faraday::Adapter::NetHttp
|
41
|
+
|
42
|
+
expect(entry_point.connection.options.params_encoder).to be == Faraday::FlatParamsEncoder
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'raises a ConnectionAlreadyInitializedError if attempting to modify headers' do
|
46
|
+
expect(entry_point.connection).to be_kind_of Faraday::Connection
|
47
|
+
expect{ entry_point.headers = {} }.to raise_error ConnectionAlreadyInitializedError
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'raises a ConnectionAlreadyInitializedError if attempting to modify the faraday block' do
|
51
|
+
expect(entry_point.connection).to be_kind_of Faraday::Connection
|
52
|
+
expect{ entry_point.connection {} }.to raise_error ConnectionAlreadyInitializedError
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe 'initialize' do
|
57
|
+
it 'sets a Link with the entry point url' do
|
58
|
+
expect(entry_point._url).to be == 'http://my.api.org'
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe 'faraday_options' do
|
64
|
+
let(:entry_point) do
|
65
|
+
EntryPoint.new 'http://my.api.org' do |entry_point|
|
66
|
+
entry_point.faraday_options = { proxy: 'http://my.proxy:8080' }
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe 'connection' do
|
71
|
+
it 'creates a Faraday connection with the entry point url' do
|
72
|
+
expect(entry_point.connection.url_prefix.to_s).to be == 'http://my.api.org/'
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'creates a Faraday connection with the default headers' do
|
76
|
+
expect(entry_point.headers['Content-Type']).to be == 'application/hal+json'
|
77
|
+
expect(entry_point.headers['Accept']).to be == 'application/hal+json,application/json'
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'creates a Faraday connection with options' do
|
81
|
+
expect(entry_point.connection.proxy).to be_kind_of Faraday::ProxyOptions
|
82
|
+
expect(entry_point.connection.proxy.uri.to_s).to be == 'http://my.proxy:8080'
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
describe 'options' do
|
88
|
+
let(:entry_point) do
|
89
|
+
EntryPoint.new 'http://my.api.org' do |entry_point|
|
90
|
+
entry_point.connection(proxy: 'http://my.proxy:8080')
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
describe 'connection' do
|
95
|
+
it 'creates a Faraday connection with the entry point url' do
|
96
|
+
expect(entry_point.connection.url_prefix.to_s).to be == 'http://my.api.org/'
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'creates a Faraday connection with the default headers' do
|
100
|
+
expect(entry_point.headers['Content-Type']).to be == 'application/hal+json'
|
101
|
+
expect(entry_point.headers['Accept']).to be == 'application/hal+json,application/json'
|
102
|
+
end
|
103
|
+
|
104
|
+
it 'creates a Faraday connection with options' do
|
105
|
+
expect(entry_point.connection.proxy).to be_kind_of Faraday::ProxyOptions
|
106
|
+
expect(entry_point.connection.proxy.uri.to_s).to be == 'http://my.proxy:8080'
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
describe 'custom' do
|
112
|
+
let(:entry_point) do
|
113
|
+
EntryPoint.new 'http://my.api.org' do |entry_point|
|
114
|
+
entry_point.connection(default: false) do |conn|
|
115
|
+
conn.request :json
|
116
|
+
conn.response :json, content_type: /\bjson$/
|
117
|
+
conn.adapter :net_http
|
118
|
+
end
|
119
|
+
|
120
|
+
entry_point.headers = {
|
121
|
+
'Content-Type' => 'application/foobar',
|
122
|
+
'Accept' => 'application/foobar'
|
123
|
+
}
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
describe 'connection' do
|
128
|
+
it 'creates a Faraday connection with the entry point url' do
|
129
|
+
expect(entry_point.connection.url_prefix.to_s).to be == 'http://my.api.org/'
|
130
|
+
end
|
131
|
+
|
132
|
+
it 'creates a Faraday connection with non-default headers' do
|
133
|
+
expect(entry_point.headers['Content-Type']).to be == 'application/foobar'
|
134
|
+
expect(entry_point.headers['Accept']).to be == 'application/foobar'
|
135
|
+
end
|
136
|
+
|
137
|
+
it 'creates a Faraday connection with the default block' do
|
138
|
+
handlers = entry_point.connection.builder.handlers
|
139
|
+
# TODO - we are below not included?
|
140
|
+
# expect(handlers).to include Faraday::Response::RaiseError
|
141
|
+
# expect(handlers).to include FaradayMiddleware::FollowRedirects
|
142
|
+
expect(handlers).to include FaradayMiddleware::EncodeJson
|
143
|
+
expect(handlers).to include FaradayMiddleware::ParseJson
|
144
|
+
expect(handlers).to include Faraday::Adapter::NetHttp
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
describe 'inherited' do
|
150
|
+
let(:entry_point) do
|
151
|
+
EntryPoint.new 'http://my.api.org' do |entry_point|
|
152
|
+
entry_point.connection do |conn|
|
153
|
+
conn.use Faraday::Request::OAuth
|
154
|
+
end
|
155
|
+
entry_point.headers['Access-Token'] = 'token'
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
describe 'connection' do
|
160
|
+
it 'creates a Faraday connection with the default and additional headers' do
|
161
|
+
expect(entry_point.headers['Content-Type']).to be == 'application/hal+json'
|
162
|
+
expect(entry_point.headers['Accept']).to be == 'application/hal+json,application/json'
|
163
|
+
expect(entry_point.headers['Access-Token']).to be == 'token'
|
164
|
+
end
|
165
|
+
|
166
|
+
it 'creates a Faraday connection with the entry point url' do
|
167
|
+
expect(entry_point.connection.url_prefix.to_s).to be == 'http://my.api.org/'
|
168
|
+
end
|
169
|
+
|
170
|
+
it 'creates a Faraday connection with the default block plus any additional handlers' do
|
171
|
+
handlers = entry_point.connection.builder.handlers
|
172
|
+
|
173
|
+
expect(handlers).to include Faraday::Request::OAuth
|
174
|
+
expect(handlers).to include Faraday::Response::RaiseError
|
175
|
+
expect(handlers).to include FaradayMiddleware::FollowRedirects
|
176
|
+
expect(handlers).to include FaradayMiddleware::EncodeHalJson
|
177
|
+
expect(handlers).to include FaradayMiddleware::ParseHalJson
|
178
|
+
expect(handlers).to include Faraday::Adapter::NetHttp
|
179
|
+
|
180
|
+
expect(entry_point.connection.options.params_encoder).to be == Faraday::FlatParamsEncoder
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module ApiNavigator
|
4
|
+
describe LinkCollection do
|
5
|
+
let(:entry_point) { double('EntryPoint', config: { base_uri: '/' }) }
|
6
|
+
|
7
|
+
let(:representation) do
|
8
|
+
JSON.parse(Fixtures::Requests.root_response)
|
9
|
+
end
|
10
|
+
|
11
|
+
let(:links) do
|
12
|
+
LinkCollection.new(representation['_links'], representation['_links']['curies'], entry_point)
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'is a CollectionHash' do
|
16
|
+
expect(LinkCollection.ancestors).to include CollectionHash
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'initializes the collection with links' do
|
20
|
+
expect(links).to respond_to(:books)
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'returns link objects for each link' do
|
24
|
+
expect(links.books).to be_kind_of Link
|
25
|
+
expect(links['self']).to be_kind_of Link
|
26
|
+
|
27
|
+
expect(links.two_links).to be_kind_of Array
|
28
|
+
expect(links['two_links']).to be_kind_of Array
|
29
|
+
end
|
30
|
+
|
31
|
+
describe 'plain link' do
|
32
|
+
let(:plain_link) { links.books }
|
33
|
+
it 'must be correct' do
|
34
|
+
expect(plain_link._url).to be == '/books'
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe 'templated link' do
|
39
|
+
let(:templated_link) { links.filter }
|
40
|
+
it 'must expand' do
|
41
|
+
expect(templated_link._expand(filter: 'gizmos')._url).to be == '/productions/1?categories=gizmos'
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe 'curied link collection' do
|
46
|
+
let(:curied_link) { links['api:authors'] }
|
47
|
+
let(:curie) { links._curies['api'] }
|
48
|
+
it 'must expand' do
|
49
|
+
expect(curied_link._expand(rel: 'assoc')._url).to be == '/api/authors'
|
50
|
+
end
|
51
|
+
it 'exposes curie' do
|
52
|
+
expect(curie.expand('authors')).to be == '/docs/resources/authors'
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe 'array of links' do
|
57
|
+
let(:two_links) { links.two_links }
|
58
|
+
|
59
|
+
it 'should have 2 items' do
|
60
|
+
expect(two_links.length).to be == 2
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'must be an array of Links' do
|
64
|
+
two_links.each do |link|
|
65
|
+
expect(link).to be_kind_of Link
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe 'null link value' do
|
71
|
+
let(:null_link) { links.null_link }
|
72
|
+
it 'must be nil' do
|
73
|
+
expect(null_link).to be_nil
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|