hyperclient 2.0.0 → 2.1.0

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.
@@ -11,11 +11,13 @@ module Hyperclient
11
11
  describe prop do
12
12
  it 'returns the property value' do
13
13
  link = Link.new('key', { prop => 'value' }, entry_point)
14
+
14
15
  _(link.send("_#{prop}")).must_equal 'value'
15
16
  end
16
17
 
17
18
  it 'returns nil if the property is not present' do
18
19
  link = Link.new('key', {}, entry_point)
20
+
19
21
  _(link.send("_#{prop}")).must_be_nil
20
22
  end
21
23
  end
@@ -53,11 +55,13 @@ module Hyperclient
53
55
  describe 'required argument' do
54
56
  it 'builds a Link with the templated URI representation' do
55
57
  link = Link.new('key', { 'href' => '/orders/{id}', 'templated' => true }, entry_point)
58
+
56
59
  _(link._expand(id: '1')._url).must_equal '/orders/1'
57
60
  end
58
61
 
59
62
  it 'expands an uri template without variables' do
60
63
  link = Link.new('key', { 'href' => '/orders/{id}', 'templated' => true }, entry_point)
64
+
61
65
  _(link._expand._url).must_equal '/orders/'
62
66
  _(link._url).must_equal '/orders/'
63
67
  end
@@ -66,27 +70,32 @@ module Hyperclient
66
70
  describe 'query string argument' do
67
71
  it 'builds a Link with the templated URI representation' do
68
72
  link = Link.new('key', { 'href' => '/orders{?id}', 'templated' => true }, entry_point)
73
+
69
74
  _(link._expand(id: '1')._url).must_equal '/orders?id=1'
70
75
  end
71
76
 
72
77
  it 'expands an uri template without variables' do
73
78
  link = Link.new('key', { 'href' => '/orders{?id}', 'templated' => true }, entry_point)
79
+
74
80
  _(link._expand._url).must_equal '/orders'
75
81
  _(link._url).must_equal '/orders'
76
82
  end
77
83
 
78
84
  it 'does not expand unknown variables' do
79
85
  link = Link.new('key', { 'href' => '/orders{?id}', 'templated' => true }, entry_point)
86
+
80
87
  _(link._expand(unknown: '1')._url).must_equal '/orders'
81
88
  end
82
89
 
83
90
  it 'only expands known variables' do
84
91
  link = Link.new('key', { 'href' => '/orders{?id}', 'templated' => true }, entry_point)
92
+
85
93
  _(link._expand(unknown: '1', id: '2')._url).must_equal '/orders?id=2'
86
94
  end
87
95
 
88
96
  it 'only expands templated links' do
89
97
  link = Link.new('key', { 'href' => '/orders{?id}', 'templated' => false }, entry_point)
98
+
90
99
  _(link._expand(id: '1')._url).must_equal '/orders{?id}'
91
100
  end
92
101
  end
@@ -119,6 +128,7 @@ module Hyperclient
119
128
 
120
129
  it 'returns the link when no uri template' do
121
130
  link = Link.new('key', { 'href' => '/orders' }, entry_point)
131
+
122
132
  _(link._url).must_equal '/orders'
123
133
  end
124
134
 
@@ -141,9 +151,53 @@ module Hyperclient
141
151
 
142
152
  link._resource
143
153
  end
154
+
155
+ it 'fetches a fresh resource after a mutating request instead of returning the cached response (#107)' do
156
+ link = Link.new('key', { 'href' => '/productions/1' }, entry_point)
157
+
158
+ stub_request(entry_point.connection) do |stub|
159
+ stub.post('http://api.example.org/productions/1') { [201, {}, { 'status' => 'created' }] }
160
+ stub.get('http://api.example.org/productions/1') { [200, {}, { 'status' => 'fetched' }] }
161
+ end
162
+
163
+ link._post('foo' => 'bar')
164
+
165
+ _(link._resource.status).must_equal 'fetched'
166
+ end
144
167
  end
145
168
 
146
169
  describe 'get' do
170
+ %w[application/hal+json application/json].each do |content_type|
171
+ it "parses a #{content_type} response into a resource" do
172
+ link = Link.new('key', { 'href' => '/productions/1' }, entry_point)
173
+
174
+ stub_request(entry_point.connection) do |stub|
175
+ stub.get('http://api.example.org/productions/1') do
176
+ [200, { 'Content-Type' => content_type },
177
+ '{"title":"Production","_links":{"self":{"href":"/productions/1"}}}']
178
+ end
179
+ end
180
+
181
+ resource = link._get
182
+
183
+ _(resource.title).must_equal 'Production'
184
+ _(resource._links.self._url).must_equal '/productions/1'
185
+ _(resource._response.body).must_be_kind_of Hash
186
+ end
187
+ end
188
+
189
+ it 'raises a parsing error for an invalid JSON response' do
190
+ link = Link.new('key', { 'href' => '/productions/1' }, entry_point)
191
+
192
+ stub_request(entry_point.connection) do |stub|
193
+ stub.get('http://api.example.org/productions/1') do
194
+ [200, { 'Content-Type' => 'application/hal+json' }, '{"title":']
195
+ end
196
+ end
197
+
198
+ _(-> { link._get }).must_raise Faraday::ParsingError
199
+ end
200
+
147
201
  it 'sends a GET request with the link url' do
148
202
  link = Link.new('key', { 'href' => '/productions/1' }, entry_point)
149
203
 
@@ -266,7 +320,7 @@ module Hyperclient
266
320
  link = Link.new('key', { 'href' => '/productions/1' }, 'foo')
267
321
 
268
322
  _(link.inspect).must_include 'Link'
269
- _(link.inspect).must_include '"href"=>"/productions/1"'
323
+ _(link.inspect).must_include({ 'href' => '/productions/1' }.inspect)
270
324
  end
271
325
  end
272
326
 
@@ -313,6 +367,37 @@ module Hyperclient
313
367
 
314
368
  _(resource.next._links.next._url).must_equal 'http://api.example.org/page3'
315
369
  end
370
+
371
+ it 'iterates over paginated, embedded items across pages' do
372
+ resource = Resource.new({ '_links' => { 'orders' => { 'href' => '/orders' } } }, entry_point)
373
+
374
+ stub_request(entry_point.connection) do |stub|
375
+ stub.get('http://api.example.org/orders') do
376
+ [200, {}, { '_links' => { 'next' => { 'href' => 'http://api.example.org/orders?page=2' } },
377
+ '_embedded' => { 'orders' => [{ 'id' => 1 }] } }]
378
+ end
379
+ stub.get('http://api.example.org/orders?page=2') do
380
+ [200, {}, { '_embedded' => { 'orders' => [{ 'id' => 2 }] } }]
381
+ end
382
+ end
383
+
384
+ ids = resource._links.orders.map(&:id)
385
+
386
+ _(ids).must_equal [1, 2]
387
+ end
388
+
389
+ it 'returns an Enumerator when called without a block' do
390
+ resource = Resource.new({ '_links' => { 'orders' => { 'href' => '/orders' } } }, entry_point)
391
+
392
+ stub_request(entry_point.connection) do |stub|
393
+ stub.get('http://api.example.org/orders') { [200, {}, { '_embedded' => { 'orders' => [{ 'id' => 1 }] } }] }
394
+ end
395
+
396
+ enum = resource._links.orders.each
397
+
398
+ _(enum).must_be_kind_of Enumerator
399
+ _(enum.to_a.first.id).must_equal 1
400
+ end
316
401
  end
317
402
 
318
403
  describe 'resource' do
@@ -341,13 +426,55 @@ module Hyperclient
341
426
  it 'responds to missing methods' do
342
427
  resource.expects(:respond_to?).with('orders').returns(false)
343
428
  resource.expects(:respond_to?).with('embedded').returns(true)
429
+
344
430
  _(link.respond_to?(:embedded)).must_equal true
345
431
  end
346
432
 
433
+ it 'responds to missing methods that delegate to another resource' do
434
+ delegate = mock('Delegate')
435
+ resource.expects(:respond_to?).with('orders').returns(true)
436
+ resource.expects(:send).with('orders').returns(delegate)
437
+ delegate.expects(:respond_to?).with('foo').returns(true)
438
+
439
+ _(link.respond_to?(:foo)).must_equal true
440
+ end
441
+
347
442
  it 'does not delegate to_ary to resource' do
348
443
  resource.expects(:to_ary).never
444
+
349
445
  _([[link, link]].flatten).must_equal [link, link]
350
446
  end
447
+
448
+ it 'delegates through delegate_method when the resource responds but returns nil' do
449
+ delegate = mock('Delegate')
450
+ resource.expects(:respond_to?).with('foo').returns(true)
451
+ resource.expects(:send).with(:foo).returns(nil)
452
+ resource.expects(:respond_to?).with('orders').returns(true)
453
+ resource.expects(:send).with('orders').returns(delegate)
454
+ delegate.expects(:respond_to?).with('foo').returns(true)
455
+ delegate.expects(:send).with(:foo).returns('delegated result')
456
+
457
+ _(link.foo).must_equal 'delegated result'
458
+ end
459
+
460
+ it 'returns nil from delegate_method when the resource has no matching key' do
461
+ keyless_link = Link.new(nil, { 'href' => 'http://myapi.org/orders' }, entry_point)
462
+ resource.expects(:respond_to?).with('foo').returns(true)
463
+ resource.expects(:send).with(:foo).returns(nil)
464
+
465
+ _(keyless_link.foo).must_be_nil
466
+ end
467
+
468
+ it 'returns nil from delegate_method when the delegate does not respond to the method' do
469
+ delegate = mock('Delegate')
470
+ resource.expects(:respond_to?).with('foo').returns(true)
471
+ resource.expects(:send).with(:foo).returns(nil)
472
+ resource.expects(:respond_to?).with('orders').returns(true)
473
+ resource.expects(:send).with('orders').returns(delegate)
474
+ delegate.expects(:respond_to?).with('foo').returns(false)
475
+
476
+ _(link.foo).must_be_nil
477
+ end
351
478
  end
352
479
  end
353
480
  end
@@ -13,7 +13,7 @@ module Hyperclient
13
13
  end
14
14
 
15
15
  it 'initializes its attributes' do
16
- Attributes.expects(:new).with(foo: :bar)
16
+ Attributes.expects(:new).with({ foo: :bar })
17
17
 
18
18
  Resource.new({ foo: :bar }, entry_point)
19
19
  end
@@ -62,6 +62,7 @@ module Hyperclient
62
62
  it '_expand' do
63
63
  resource = Resource.new({ '_links' => { 'orders' => { 'href' => '/orders/{id}', 'templated' => true } } },
64
64
  entry_point)
65
+
65
66
  _(resource._links.orders._expand(id: 1)._url).must_equal '/orders/1'
66
67
  _(resource.orders._expand(id: 1)._url).must_equal '/orders/1'
67
68
  _(resource.orders(id: 1)._url).must_equal '/orders/1'
@@ -94,16 +95,19 @@ module Hyperclient
94
95
  describe 'method_missing' do
95
96
  it 'delegates to attributes' do
96
97
  resource._attributes.expects(:foo).returns('bar')
98
+
97
99
  _(resource.foo).must_equal 'bar'
98
100
  end
99
101
 
100
102
  it 'delegates to links' do
101
103
  resource._links.expects(:foo).returns('bar')
104
+
102
105
  _(resource.foo).must_equal 'bar'
103
106
  end
104
107
 
105
108
  it 'delegates to embedded' do
106
109
  resource._embedded.expects(:foo).returns('bar')
110
+
107
111
  _(resource.foo).must_equal 'bar'
108
112
  end
109
113
 
@@ -111,11 +115,13 @@ module Hyperclient
111
115
  resource._attributes.expects('respond_to?').with('foo').returns(false)
112
116
  resource._links.expects('respond_to?').with('foo').returns(false)
113
117
  resource._embedded.expects('respond_to?').with('foo').returns(false)
118
+
114
119
  _(-> { resource.foo }).must_raise NoMethodError
115
120
  end
116
121
 
117
122
  it 'delegates []' do
118
123
  resource._attributes.expects(:foo).returns('bar')
124
+
119
125
  _(resource['foo']).must_equal 'bar'
120
126
  end
121
127
 
@@ -133,6 +139,7 @@ module Hyperclient
133
139
  describe 'with a default value' do
134
140
  it 'returns the value for keys that exist' do
135
141
  resource._attributes.expects(:foo).returns('bar')
142
+
136
143
  _(resource.fetch('foo', 'default value')).must_equal 'bar'
137
144
  end
138
145
 
@@ -144,6 +151,7 @@ module Hyperclient
144
151
  describe 'with a block' do
145
152
  it 'returns the value for keys that exist' do
146
153
  resource._attributes.expects(:foo).returns('bar')
154
+
147
155
  _(resource.fetch('foo') { 'default value' }).must_equal 'bar'
148
156
  end
149
157
 
@@ -220,5 +228,30 @@ module Hyperclient
220
228
  end
221
229
  end
222
230
  end
231
+
232
+ describe 'inspect' do
233
+ it 'outputs a custom-friendly output' do
234
+ resource = Resource.new({ '_links' => { 'self' => { 'href' => '/orders/523' } }, 'title' => 'Order' },
235
+ entry_point)
236
+
237
+ _(resource.inspect).must_include 'Resource'
238
+ _(resource.inspect).must_include 'self_link:'
239
+ _(resource.inspect).must_include 'attributes:'
240
+ end
241
+ end
242
+
243
+ describe '#to_h and #to_hash' do
244
+ let(:resource) do
245
+ Resource.new({ '_links' => {}, 'title' => 'Order', 'total' => 42 }, entry_point)
246
+ end
247
+
248
+ it 'returns the attributes as a Hash from #to_hash' do
249
+ _(resource.to_hash).must_equal('title' => 'Order', 'total' => 42)
250
+ end
251
+
252
+ it 'returns the same result from #to_h as #to_hash' do
253
+ _(resource.to_h).must_equal(resource.to_hash)
254
+ end
255
+ end
223
256
  end
224
257
  end
@@ -31,6 +31,7 @@ describe Hyperclient do
31
31
 
32
32
  it 'creates a Faraday connection with the default block plus any additional handlers' do
33
33
  handlers = client.connection.builder.handlers
34
+
34
35
  _(handlers).must_include Faraday::Request::Instrumentation
35
36
  _(handlers).must_include Faraday::Response::RaiseError
36
37
  _(handlers).must_include Faraday::FollowRedirects::Middleware
@@ -0,0 +1,52 @@
1
+ require 'test_helper'
2
+ require 'hyperclient'
3
+ require 'webmock'
4
+ require 'stringio'
5
+ require_relative '../examples/oxford/app'
6
+
7
+ describe OxfordExample::App do
8
+ let(:app) { OxfordExample::App.new }
9
+
10
+ before do
11
+ WebMock.enable!
12
+ WebMock.stub_request(:any, %r{\Ahttp://127\.0\.0\.1:9292/}).to_return do |request|
13
+ status, headers, body = app.call('REQUEST_METHOD' => request.method.to_s.upcase,
14
+ 'PATH_INFO' => request.uri.path)
15
+ { status: status, headers: headers, body: body.join }
16
+ end
17
+ end
18
+
19
+ after do
20
+ WebMock.reset!
21
+ WebMock.disable!
22
+ end
23
+
24
+ it 'runs the documented client without external requests' do
25
+ output, = capture_io { load File.expand_path('../examples/oxford/client.rb', __dir__) }
26
+
27
+ _(output).must_include 'Balliol College: Broad Street OX1 3BJ'
28
+ _(output).must_include 'Followed primary_place: Balliol College'
29
+ _(output).must_include 'Archived depiction URL (not fetched):'
30
+ _(output).must_include 'CURIE: /docs/places'
31
+ end
32
+
33
+ it 'supports explicit navigation, hash attributes, and embedded resources' do
34
+ api = Hyperclient.new('http://127.0.0.1:9292/')
35
+ place = api._links.places._embedded.places.first
36
+
37
+ _(place[:name]).must_equal 'Balliol College'
38
+ _(place.to_h['id']).must_equal 'oxpoints:23232339'
39
+ _(place._embedded.files.first.type).must_equal 'depiction'
40
+ _(api._links['oxford:places']._embedded.places.first.name).must_equal 'Balliol College'
41
+ end
42
+
43
+ it 'serves HAL, rejects writes, and reports missing resources' do
44
+ status, headers, = app.call('REQUEST_METHOD' => 'GET', 'PATH_INFO' => '/')
45
+
46
+ _(status).must_equal 200
47
+ _(headers['content-type']).must_equal 'application/hal+json'
48
+ _(app.call('REQUEST_METHOD' => 'POST', 'PATH_INFO' => '/').first).must_equal 405
49
+ _(app.call('REQUEST_METHOD' => 'GET', 'PATH_INFO' => '/missing').first).must_equal 404
50
+ _(app.call('REQUEST_METHOD' => 'HEAD', 'PATH_INFO' => '/').last).must_equal []
51
+ end
52
+ end
data/test/test_helper.rb CHANGED
@@ -17,14 +17,13 @@ end
17
17
 
18
18
  require 'minitest/autorun'
19
19
  require 'minitest/pride'
20
- require 'minitest/unit'
21
20
  require 'mocha/minitest'
22
21
  require 'json'
23
22
 
24
23
  require 'faraday'
25
24
  require 'faraday/request/instrumentation'
26
25
 
27
- MiniTest::Assertions.class_eval do
26
+ Minitest::Assertions.class_eval do
28
27
  def stub_request(conn, adapter_class = Faraday::Adapter::Test, &stubs_block)
29
28
  adapter_handler = conn.builder.handlers.find { |h| h.klass < Faraday::Adapter }
30
29
  if adapter_handler
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hyperclient
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oriol Gual
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-02-01 00:00:00.000000000 Z
11
+ date: 2026-10-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: addressable
@@ -73,9 +73,9 @@ executables: []
73
73
  extensions: []
74
74
  extra_rdoc_files: []
75
75
  files:
76
- - ".DS_Store"
77
76
  - ".github/dependabot.yml"
78
77
  - ".github/workflows/coverage.yml"
78
+ - ".github/workflows/danger-comment.yml"
79
79
  - ".github/workflows/danger.yml"
80
80
  - ".github/workflows/lint.yml"
81
81
  - ".github/workflows/test.yml"
@@ -83,6 +83,7 @@ files:
83
83
  - ".rubocop.yml"
84
84
  - ".rubocop_todo.yml"
85
85
  - ".yardopts"
86
+ - AGENTS.md
86
87
  - CHANGELOG.md
87
88
  - CONTRIBUTING.md
88
89
  - Dangerfile
@@ -93,7 +94,11 @@ files:
93
94
  - RELEASING.md
94
95
  - Rakefile
95
96
  - UPGRADING.md
96
- - examples/splines_api.rb
97
+ - examples/oxford/README.md
98
+ - examples/oxford/app.rb
99
+ - examples/oxford/client.rb
100
+ - examples/oxford/places.json
101
+ - examples/oxford/server.rb
97
102
  - features/api_navigation.feature
98
103
  - features/default_config.feature
99
104
  - features/steps/api_navigation.rb
@@ -124,6 +129,7 @@ files:
124
129
  - test/hyperclient/resource_collection_test.rb
125
130
  - test/hyperclient/resource_test.rb
126
131
  - test/hyperclient_test.rb
132
+ - test/oxford_example_test.rb
127
133
  - test/test_helper.rb
128
134
  homepage: https://github.com/codegram/hyperclient/
129
135
  licenses: []
@@ -144,7 +150,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
144
150
  - !ruby/object:Gem::Version
145
151
  version: '0'
146
152
  requirements: []
147
- rubygems_version: 3.1.4
153
+ rubygems_version: 3.5.16
148
154
  signing_key:
149
155
  specification_version: 4
150
156
  summary: ''
data/.DS_Store DELETED
Binary file
@@ -1,39 +0,0 @@
1
- require 'hyperclient'
2
-
3
- puts "Using Hyperclient #{Hyperclient::VERSION} ..."
4
-
5
- # create a new client
6
- api = Hyperclient.new('https://grape-with-roar.herokuapp.com/api')
7
-
8
- # enumerate splines
9
- api.splines.each do |spline|
10
- puts spline.uuid.to_s
11
- puts " reticulated: #{spline.reticulated ? 'yes' : 'no'}"
12
- puts " thumbnail: #{spline['images:thumbnail']}"
13
- end
14
-
15
- api._links.splines._embedded.splines.each do |_spline|
16
- # ... equivalent to the above
17
- end
18
-
19
- puts '*' * 10
20
-
21
- # retrieve an existing spline
22
- spline = api.spline(uuid: 123)
23
- puts "Spline #{spline.uuid} is #{spline.reticulated ? 'reticulated' : 'not reticulated'}."
24
-
25
- # puts api._links.spline._expand(uuid: 'uuid')._resource._attributes.reticulated
26
-
27
- # spline.to_h
28
-
29
- # create a new spline
30
- spline = api.splines._post(spline: { reticulated: true })
31
- puts "Created a #{spline.reticulated ? 'reticulated' : 'unreticulated'} spline #{spline.uuid}."
32
-
33
- # update an existing spline
34
- spline = api.spline(uuid: 123)._put(spline: { reticulated: true })
35
- puts "Updated spline #{spline.uuid}, now #{spline.reticulated ? 'reticulated' : 'not reticulated'}."
36
-
37
- # delete an existing spline
38
- spline = api.spline(uuid: 123)._delete
39
- puts "Deleted spline #{spline.uuid}."