hyperclient 1.0.1 → 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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +18 -0
  3. data/.github/workflows/coverage.yml +21 -0
  4. data/.github/workflows/danger-comment.yml +11 -0
  5. data/.github/workflows/danger.yml +13 -0
  6. data/.github/workflows/lint.yml +16 -0
  7. data/.github/workflows/test.yml +28 -0
  8. data/.gitignore +1 -0
  9. data/.rubocop.yml +5 -0
  10. data/.rubocop_todo.yml +40 -31
  11. data/AGENTS.md +26 -0
  12. data/CHANGELOG.md +21 -3
  13. data/CONTRIBUTING.md +11 -1
  14. data/Dangerfile +2 -0
  15. data/Gemfile +10 -7
  16. data/README.md +49 -44
  17. data/RELEASING.md +47 -41
  18. data/Rakefile +0 -8
  19. data/examples/oxford/README.md +42 -0
  20. data/examples/oxford/app.rb +48 -0
  21. data/examples/oxford/client.rb +13 -0
  22. data/examples/oxford/places.json +21 -0
  23. data/examples/oxford/server.rb +13 -0
  24. data/features/steps/api_navigation.rb +3 -3
  25. data/features/steps/default_config.rb +2 -2
  26. data/features/support/api.rb +22 -11
  27. data/features/support/env.rb +15 -1
  28. data/hyperclient.gemspec +4 -4
  29. data/lib/hyperclient/entry_point.rb +3 -3
  30. data/lib/hyperclient/link.rb +13 -4
  31. data/lib/hyperclient/resource.rb +13 -3
  32. data/lib/hyperclient/resource_collection.rb +1 -3
  33. data/lib/hyperclient/version.rb +1 -1
  34. data/test/hyperclient/attributes_test.rb +9 -0
  35. data/test/hyperclient/collection_test.rb +22 -0
  36. data/test/hyperclient/curie_test.rb +7 -0
  37. data/test/hyperclient/entry_point_test.rb +35 -13
  38. data/test/hyperclient/link_collection_test.rb +10 -0
  39. data/test/hyperclient/link_test.rb +128 -1
  40. data/test/hyperclient/resource_test.rb +36 -2
  41. data/test/hyperclient_test.rb +6 -5
  42. data/test/oxford_example_test.rb +52 -0
  43. data/test/test_helper.rb +19 -1
  44. metadata +25 -33
  45. data/.travis.yml +0 -24
  46. data/examples/splines_api.rb +0 -39
data/RELEASING.md CHANGED
@@ -1,69 +1,75 @@
1
- Releasing Hyperclient
2
- =====================
1
+ # Releasing Hyperclient
3
2
 
4
- There're no particular rules about when to release hyperclient. Release bug fixes frequenty, features not so frequently and breaking API changes rarely.
3
+ Release bug fixes frequently, features less frequently, and breaking API changes rarely.
5
4
 
6
- ### Release
5
+ ## Prepare the Release
7
6
 
8
- Run tests, check that all tests succeed locally.
7
+ Start with a clean, up-to-date `master` branch. Check that CI has succeeded for all supported platforms.
9
8
 
10
- ```
11
- bundle install
12
- rake
13
- ```
14
-
15
- Check that the last build succeeded in [Travis CI](https://travis-ci.org/codegram/hyperclient) for all supported platforms.
16
-
17
- Increment the version, modify [lib/hyperclient/version.rb](lib/hyperclient/version.rb).
9
+ Set the release version in [lib/hyperclient/version.rb](lib/hyperclient/version.rb). Use a patch version for backward-compatible fixes, a minor version for backward-compatible features, and a major version for breaking changes.
18
10
 
19
- * Increment the third number if the release has bug fixes and/or very minor features, only (eg. change `0.5.1` to `0.5.2`).
20
- * Increment the second number if the release contains major features or breaking API changes (eg. change `0.5.1` to `0.4.0`).
11
+ Replace the next-version heading in [CHANGELOG.md](CHANGELOG.md) with the release version and date, for example `### 2.1.0 (2026/10/14)`. Remove the "Your contribution here." placeholder from that release.
21
12
 
22
- Change "Next Release" in [CHANGELOG.md](CHANGELOG.md) to the new version.
13
+ Run the full suite, then commit and push the release preparation:
23
14
 
24
- ```
25
- 0.4.0 (2014-01-27)
26
- ==================
15
+ ```sh
16
+ bundle install
17
+ bundle exec rake
18
+ git add CHANGELOG.md lib/hyperclient/version.rb
19
+ git commit -m "Preparing for release, 2.1.0."
20
+ git push origin master
27
21
  ```
28
22
 
29
- Remove the line with "Your contribution here.", since there will be no more contributions to this release.
23
+ Wait for CI on the release preparation commit to succeed before publishing.
30
24
 
31
- Commit your changes.
25
+ ## Publish to RubyGems and GitHub
32
26
 
27
+ Publish the gem using Bundler's release task. This builds the gem, creates and pushes the version tag, and publishes to RubyGems. Complete RubyGems MFA when prompted.
28
+
29
+ ```sh
30
+ bundle exec rake release
33
31
  ```
34
- git add CHANGELOG.md lib/hyperclient/version.rb
35
- git commit -m "Preparing for release, 0.4.0."
36
- git push origin master
32
+
33
+ Creating a Git tag does **not** create a GitHub release. After confirming that the gem was published successfully, create a GitHub release for the same tag. Put the matching changelog section's entries in a temporary file and use them as the release notes:
34
+
35
+ ```sh
36
+ version=2.1.0
37
+ notes="$(mktemp)"
38
+ awk -v version="$version" '
39
+ /^### / {
40
+ if (found) exit
41
+ if ($2 == version) found = 1
42
+ next
43
+ }
44
+ found { print }
45
+ ' CHANGELOG.md > "$notes"
46
+ gh release create "v$version" --repo codegram/hyperclient --verify-tag --title "$version" --notes-file "$notes" --latest
47
+ rm "$notes"
37
48
  ```
38
49
 
39
- Release.
50
+ Verify both publications before considering the release complete:
40
51
 
52
+ ```sh
53
+ curl --fail --silent --show-error "https://rubygems.org/api/v2/rubygems/hyperclient/versions/$version.json"
54
+ gh release view "v$version" --repo codegram/hyperclient
41
55
  ```
42
- $ rake release
43
56
 
44
- hyperclient 0.4.0 built to pkg/hyperclient-0.4.0.gem.
45
- Tagged v0.4.0.
46
- Pushed git commits and tags.
47
- Pushed hyperclient 0.4.0 to rubygems.org.
48
- ```
57
+ If publishing is interrupted, inspect the existing tag, RubyGems version, and GitHub release first. Resume only the missing step; do not move or recreate a published tag.
49
58
 
50
- ### Prepare for the Next Version
59
+ ## Prepare the Next Version
51
60
 
52
- Add the next release to [CHANGELOG.md](CHANGELOG.md).
61
+ Increment the development patch version in [lib/hyperclient/version.rb](lib/hyperclient/version.rb), for example from `2.1.0` to `2.1.1`. Add the matching unreleased section at the top of [CHANGELOG.md](CHANGELOG.md):
53
62
 
54
- ```
55
- Next Release
56
- ============
63
+ ```markdown
64
+ ### 2.1.1 (Next)
57
65
 
58
66
  * Your contribution here.
59
67
  ```
60
68
 
61
- Increment the minor version, modify [lib/hyperclient/version.rb](lib/hyperclient/version.rb).
69
+ Commit and push:
62
70
 
63
- Comit your changes.
64
-
65
- ```
71
+ ```sh
66
72
  git add CHANGELOG.md lib/hyperclient/version.rb
67
- git commit -m "Preparing for next release, 0.4.1."
73
+ git commit -m "Preparing for next release, 2.1.1."
68
74
  git push origin master
69
75
  ```
data/Rakefile CHANGED
@@ -6,14 +6,6 @@ Bundler.setup :default, :test, :development
6
6
 
7
7
  Bundler::GemHelper.install_tasks
8
8
 
9
- if ENV['COVERAGE']
10
- require 'simplecov'
11
- SimpleCov.start do
12
- add_filter '/test/'
13
- add_filter '/features/'
14
- end
15
- end
16
-
17
9
  require 'rake/testtask'
18
10
 
19
11
  Rake::TestTask.new(:test) do |t|
@@ -0,0 +1,42 @@
1
+ # Local Oxford HAL Demo
2
+
3
+ This read-only demo uses a small selection of public place facts from Oxford's retired mobile API. It runs entirely on localhost; neither Oxford nor the Internet Archive is contacted when running the example. The information is historical, not a current directory.
4
+
5
+ From the repository root, install dependencies and start the server:
6
+
7
+ ```sh
8
+ bundle install
9
+ bundle exec ruby examples/oxford/server.rb
10
+ ```
11
+
12
+ In a second terminal:
13
+
14
+ ```sh
15
+ bundle exec ruby -Ilib examples/oxford/client.rb
16
+ ```
17
+
18
+ The server binds only to `127.0.0.1:9292`. Set `PORT` on the server and `OXFORD_URL` on the client to use another port. Stop the server with Ctrl-C.
19
+
20
+ The client demonstrates collection traversal, attributes, a templated place link, following `primary_place`, embedded file metadata, and CURIE expansion. It prints image URLs without downloading images. Unknown paths return 404; writes return 405.
21
+
22
+ ## Provenance and Adaptations
23
+
24
+ The factual fields and original place path in `places.json` were transcribed from [Balliol College's archived response, September 10, 2020](https://web.archive.org/web/20200910141343id_/https://api.m.ox.ac.uk/places/oxpoints:23232339). The record retains the original ID, name, address, coordinates, self/primary-place paths, and one image URL. No image or descriptive prose is redistributed.
25
+
26
+ The [archived API root, July 12, 2024](https://web.archive.org/web/20240712050404id_/http://api.m.ox.ac.uk/) demonstrates the original HAL-style links. See also [Moxie's OxPoints importer documentation](https://moxie.readthedocs.io/en/latest/places/oxpoints.html).
27
+
28
+ ## Data License
29
+
30
+ The underlying OxPoints place data is dedicated to the public domain under [CC0](https://creativecommons.org/publicdomain/zero/1.0/). Oxford's own presentation, [*OxPoints and data.ox.ac.uk*, Alexander Dutton, InfoDev, OUCS (2011)](https://old.ictf.ox.ac.uk/conference/2011/presentations/wks11_oxpoints_dataox.pdf), identifies names, coordinates, addresses, and identifiers as OxPoints data (slides 2-5) and explicitly states "Public domain (CC0; no attribution required)" (slide 8). The place facts in `places.json` come from this dataset via the archived mobile API response cited above; the HAL wrappers and local links are demo adaptations.
31
+
32
+ This is not a license claim for full archived API responses or photographs. The depiction URL is retained only as a reference to an external image; the image itself is not included, fetched, or covered by this notice. Newly authored server code and documentation are under this project's license. Moxie's Apache-2.0 code license is separate from the OxPoints data license.
33
+
34
+ The root, collection wrapper, templated `place` link, `oxford` CURIE, and local relation documentation are demo additions, not a reconstruction of the original service. Responses use `application/hal+json`; the archived service returned `application/json`. Relative place links retain their original paths and resolve locally. Unavailable child links, images, and unrelated API endpoints are not mirrored.
35
+
36
+ ## Tests
37
+
38
+ ```sh
39
+ bundle exec ruby -Itest test/oxford_example_test.rb
40
+ ```
41
+
42
+ The tests exercise the same app and client using local HTTP stubs, without requiring a running server or external network access. They are also included in `bundle exec rake test`.
@@ -0,0 +1,48 @@
1
+ require 'json'
2
+
3
+ module OxfordExample
4
+ # A read-only HAL demo, independent of Oxford's retired API.
5
+ class App
6
+ def initialize
7
+ @places = JSON.parse(File.read(File.join(__dir__, 'places.json')))
8
+ end
9
+
10
+ def call(env)
11
+ return response(405, { 'error' => 'Read-only example' }) unless %w[GET HEAD].include?(env['REQUEST_METHOD'])
12
+
13
+ status, body = representation(env['PATH_INFO'])
14
+ result = response(status, body)
15
+ result[2] = [] if env['REQUEST_METHOD'] == 'HEAD'
16
+ result
17
+ end
18
+
19
+ private
20
+
21
+ def representation(path)
22
+ case path
23
+ when '/'
24
+ [200, {
25
+ '_links' => {
26
+ 'self' => { 'href' => '/' },
27
+ 'places' => { 'href' => '/places/' },
28
+ 'place' => { 'href' => '/places/{id}', 'templated' => true },
29
+ 'curies' => [{ 'name' => 'oxford', 'href' => '/docs/{rel}', 'templated' => true }],
30
+ 'oxford:places' => { 'href' => '/places/' }
31
+ }
32
+ }]
33
+ when '/places/'
34
+ [200, { '_links' => { 'self' => { 'href' => '/places/' } },
35
+ '_embedded' => { 'places' => @places } }]
36
+ when '/docs/places'
37
+ [200, { 'description' => 'Archived Oxford place facts in a local HAL collection.' }]
38
+ else
39
+ place = @places.find { |item| item['_links']['self']['href'] == path }
40
+ place ? [200, place] : [404, { 'error' => 'Not included in this example' }]
41
+ end
42
+ end
43
+
44
+ def response(status, body)
45
+ [status, { 'content-type' => 'application/hal+json' }, [JSON.generate(body)]]
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,13 @@
1
+ require 'hyperclient'
2
+
3
+ api = Hyperclient.new(ENV.fetch('OXFORD_URL', 'http://127.0.0.1:9292/'))
4
+ api.places.each do |place|
5
+ puts "#{place.name}: #{place.address}"
6
+ end
7
+
8
+ place = api.place(id: 'oxpoints:23232339')
9
+ puts "Followed primary_place: #{place.primary_place.name}"
10
+ place._embedded.files.each do |file|
11
+ puts "Archived #{file.type} URL (not fetched): #{file.url}"
12
+ end
13
+ puts "CURIE: #{api._links._curies['oxford'].expand('places')}"
@@ -0,0 +1,21 @@
1
+ [
2
+ {
3
+ "_links": {
4
+ "self": { "href": "/places/oxpoints:23232339" },
5
+ "primary_place": { "href": "/places/oxpoints:23232339" }
6
+ },
7
+ "_embedded": {
8
+ "files": [
9
+ {
10
+ "type": "depiction",
11
+ "url": "//mox-static-files.oucs.ox.ac.uk/oxpoints/23232339/depiction/original/ball.jpg"
12
+ }
13
+ ]
14
+ },
15
+ "id": "oxpoints:23232339",
16
+ "name": "Balliol College",
17
+ "address": "Broad Street OX1 3BJ",
18
+ "lat": "51.754425",
19
+ "lon": "-1.257216"
20
+ }
21
+ ]
@@ -0,0 +1,13 @@
1
+ require 'webrick'
2
+ require_relative 'app'
3
+
4
+ app = OxfordExample::App.new
5
+ server = WEBrick::HTTPServer.new(BindAddress: '127.0.0.1', Port: Integer(ENV.fetch('PORT', '9292')))
6
+ server.mount_proc('/') do |request, response|
7
+ status, headers, body = app.call('REQUEST_METHOD' => request.request_method, 'PATH_INFO' => request.path)
8
+ response.status = status
9
+ headers.each { |key, value| response[key] = value }
10
+ response.body = body.join
11
+ end
12
+ %w[INT TERM].each { |signal| trap(signal) { server.shutdown } }
13
+ server.start
@@ -43,13 +43,13 @@ class Spinach::Features::ApiNavigation < Spinach::FeatureSteps
43
43
  end
44
44
 
45
45
  step 'I should be able to access it\'s title and body' do
46
- @post._attributes.title.wont_equal nil
47
- @post._attributes.body.wont_equal nil
46
+ assert_kind_of String, @post._attributes.title
47
+ assert_kind_of String, @post._attributes.body
48
48
  end
49
49
 
50
50
  step 'I should also be able to access it\'s embedded comments' do
51
51
  comment = @post._embedded.comments.first
52
- comment._attributes.title.wont_equal nil
52
+ assert_kind_of String, comment._attributes.title
53
53
  end
54
54
 
55
55
  step 'I should be able to navigate to next page' do
@@ -25,7 +25,7 @@ class Spinach::Features::DefaultConfig < Spinach::FeatureSteps
25
25
  end
26
26
 
27
27
  step 'it should have been parsed as JSON' do
28
- @posts._attributes.total_posts.to_i.must_equal 4
29
- @posts._attributes['total_posts'].to_i.must_equal 4
28
+ assert_equal 4, @posts._attributes.total_posts.to_i
29
+ assert_equal 4, @posts._attributes['total_posts'].to_i
30
30
  end
31
31
  end
@@ -7,17 +7,28 @@ module API
7
7
  before do
8
8
  WebMock::Config.instance.query_values_notation = :flat_array
9
9
 
10
- stub_request(:any, /api.example.org*/).to_return(body: root_response, headers: { 'Content-Type' => 'application/hal+json' })
11
- stub_request(:get, 'api.example.org').to_return(body: root_response, headers: { 'Content-Type' => 'application/hal+json' })
12
- stub_request(:get, 'api.example.org/authors').to_return(body: authors_response, headers: { 'Content-Type' => 'application/hal+json' })
13
- stub_request(:get, 'api.example.org/posts').to_return(body: posts_response, headers: { 'Content-Type' => 'application/hal+json' })
14
- stub_request(:get, 'api.example.org/posts?page=2').to_return(body: posts_page2_response, headers: { 'Content-Type' => 'application/hal+json' })
15
- stub_request(:get, 'api.example.org/posts?page=3').to_return(body: posts_page3_response, headers: { 'Content-Type' => 'application/hal+json' })
16
- stub_request(:get, 'api.example.org/posts/1').to_return(body: post1_response, headers: { 'Content-Type' => 'application/hal+json' })
17
- stub_request(:get, 'api.example.org/posts/2').to_return(body: post2_response, headers: { 'Content-Type' => 'application/hal+json' })
18
- stub_request(:get, 'api.example.org/posts/3').to_return(body: post3_response, headers: { 'Content-Type' => 'application/hal+json' })
19
- stub_request(:get, 'api.example.org/page2').to_return(body: page2_response, headers: { 'Content-Type' => 'application/hal+json' })
20
- stub_request(:get, 'api.example.org/page3').to_return(body: page3_response, headers: { 'Content-Type' => 'application/hal+json' })
10
+ stub_request(:any, /api.example.org*/).to_return(body: root_response,
11
+ headers: { 'Content-Type' => 'application/hal+json' })
12
+ stub_request(:get, 'api.example.org').to_return(body: root_response,
13
+ headers: { 'Content-Type' => 'application/hal+json' })
14
+ stub_request(:get, 'api.example.org/authors').to_return(body: authors_response,
15
+ headers: { 'Content-Type' => 'application/hal+json' })
16
+ stub_request(:get, 'api.example.org/posts').to_return(body: posts_response,
17
+ headers: { 'Content-Type' => 'application/hal+json' })
18
+ stub_request(:get, 'api.example.org/posts?page=2').to_return(body: posts_page2_response,
19
+ headers: { 'Content-Type' => 'application/hal+json' })
20
+ stub_request(:get, 'api.example.org/posts?page=3').to_return(body: posts_page3_response,
21
+ headers: { 'Content-Type' => 'application/hal+json' })
22
+ stub_request(:get, 'api.example.org/posts/1').to_return(body: post1_response,
23
+ headers: { 'Content-Type' => 'application/hal+json' })
24
+ stub_request(:get, 'api.example.org/posts/2').to_return(body: post2_response,
25
+ headers: { 'Content-Type' => 'application/hal+json' })
26
+ stub_request(:get, 'api.example.org/posts/3').to_return(body: post3_response,
27
+ headers: { 'Content-Type' => 'application/hal+json' })
28
+ stub_request(:get, 'api.example.org/page2').to_return(body: page2_response,
29
+ headers: { 'Content-Type' => 'application/hal+json' })
30
+ stub_request(:get, 'api.example.org/page3').to_return(body: page3_response,
31
+ headers: { 'Content-Type' => 'application/hal+json' })
21
32
  end
22
33
 
23
34
  def api
@@ -1,4 +1,18 @@
1
- require 'minitest/spec'
1
+ require 'minitest'
2
2
  require 'webmock'
3
3
  WebMock.enable!
4
4
  require 'hyperclient'
5
+
6
+ Minitest::Spec.new nil if defined?(Minitest::Spec)
7
+ Spinach.config[:failure_exceptions] << Minitest::Assertion
8
+
9
+ class Spinach::FeatureSteps
10
+ include Minitest::Assertions
11
+
12
+ attr_accessor :assertions
13
+
14
+ def initialize(*args)
15
+ super
16
+ self.assertions = 0
17
+ end
18
+ end
data/hyperclient.gemspec CHANGED
@@ -8,13 +8,13 @@ Gem::Specification.new do |gem|
8
8
  gem.homepage = 'https://github.com/codegram/hyperclient/'
9
9
  gem.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
10
10
  gem.files = `git ls-files`.split("\n")
11
- gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
12
11
  gem.name = 'hyperclient'
13
12
  gem.require_paths = ['lib']
14
13
  gem.version = Hyperclient::VERSION
15
14
 
16
15
  gem.add_dependency 'addressable'
17
- gem.add_dependency 'faraday', '>= 0.9.0'
18
- gem.add_dependency 'faraday_hal_middleware'
19
- gem.add_dependency 'faraday_middleware'
16
+ gem.add_dependency 'faraday', '>= 2'
17
+ gem.add_dependency 'faraday-follow_redirects'
18
+ gem.add_dependency 'faraday_hal_middleware', '>= 0.2'
19
+ gem.metadata['rubygems_mfa_required'] = 'true'
20
20
  end
@@ -1,4 +1,4 @@
1
- require 'faraday_middleware'
1
+ require 'faraday/follow_redirects'
2
2
  require 'faraday_hal_middleware'
3
3
 
4
4
  module Hyperclient
@@ -20,7 +20,7 @@ module Hyperclient
20
20
  #
21
21
  # client = Hyperclient::EntryPoint.new('http://my.api.org') do |entry_point|
22
22
  # entry_point.connection do |conn|
23
- # conn.use Faraday::Request::OAuth
23
+ # conn.use Faraday::Request::Instrumentation
24
24
  # end
25
25
  # entry_point.headers['Access-Token'] = 'token'
26
26
  # end
@@ -138,7 +138,7 @@ module Hyperclient
138
138
  def default_faraday_block
139
139
  lambda do |connection, &block|
140
140
  connection.use Faraday::Response::RaiseError
141
- connection.use FaradayMiddleware::FollowRedirects
141
+ connection.use Faraday::FollowRedirects::Middleware
142
142
  connection.request :hal_json
143
143
  connection.response :hal_json, content_type: /\bjson$/
144
144
 
@@ -190,10 +190,19 @@ module Hyperclient
190
190
  end
191
191
 
192
192
  def http_method(method, body = nil)
193
- @resource = begin
194
- response = @entry_point.connection.run_request(method, _url, body, nil)
195
- Resource.new(response.body, @entry_point, response)
196
- end
193
+ response = @entry_point.connection.run_request(method, _url, body, nil)
194
+ resource = Resource.new(response.body, @entry_point, response)
195
+
196
+ # Only cache GET responses. Caching the response of a mutating
197
+ # request (POST/PUT/PATCH/DELETE) as `@resource` would make
198
+ # subsequent delegated calls (e.g. `#each`, `method_missing`) see
199
+ # the stale mutation response instead of fetching a fresh
200
+ # resource, requiring users to create a new client between a
201
+ # `_post` and a `_get`. See #107.
202
+ @resource = resource if method == :get
203
+ @delegate = nil
204
+
205
+ resource
197
206
  end
198
207
  end
199
208
  end
@@ -66,6 +66,18 @@ module Hyperclient
66
66
  send(name) if respond_to?(name)
67
67
  end
68
68
 
69
+ # Public: Returns the attributes of the Resource as a Hash.
70
+ #
71
+ # Delegates explicitly (rather than relying on method_missing) since
72
+ # Array also defines #to_h (but not #to_hash), which meant the
73
+ # `Array.method_defined?(method)` guard in method_missing let #to_hash
74
+ # through but silently swallowed #to_h, returning nil instead of the
75
+ # attributes hash.
76
+ def to_h
77
+ _attributes.to_h
78
+ end
79
+ alias to_hash to_h
80
+
69
81
  def fetch(key, *args)
70
82
  return self[key] if respond_to?(key)
71
83
 
@@ -111,9 +123,7 @@ module Hyperclient
111
123
  elsif !Array.method_defined?(method)
112
124
  %i[_attributes _embedded _links].each do |target|
113
125
  target = send(target)
114
- if target.respond_to?(method.to_s)
115
- return target.send(method, *args, &block)
116
- end
126
+ return target.send(method, *args, &block) if target.respond_to?(method.to_s)
117
127
  end
118
128
  super
119
129
  end
@@ -23,9 +23,7 @@ module Hyperclient
23
23
  private
24
24
 
25
25
  def build_resource(representation)
26
- if representation.is_a?(Array)
27
- return representation.map(&method(:build_resource))
28
- end
26
+ return representation.map(&method(:build_resource)) if representation.is_a?(Array)
29
27
 
30
28
  Resource.new(representation, @entry_point)
31
29
  end
@@ -1,3 +1,3 @@
1
1
  module Hyperclient
2
- VERSION = '1.0.1'.freeze
2
+ VERSION = '2.1.0'.freeze
3
3
  end
@@ -36,5 +36,14 @@ module Hyperclient
36
36
  it 'is a collection' do
37
37
  _(Attributes.ancestors).must_include Collection
38
38
  end
39
+
40
+ describe 'when the representation is not a Hash' do
41
+ it 'wraps the representation as-is instead of filtering reserved keys' do
42
+ representation = 'not a hash'
43
+ attributes = Attributes.new(representation)
44
+
45
+ _(attributes.instance_variable_get(:@collection)).must_equal representation
46
+ end
47
+ end
39
48
  end
40
49
  end
@@ -41,6 +41,28 @@ module Hyperclient
41
41
  end
42
42
  end
43
43
 
44
+ describe '#to_h' do
45
+ it 'returns the wrapped collection as a hash' do
46
+ _(collection.to_h).must_be_kind_of Hash
47
+ end
48
+
49
+ it 'returns the same result as #to_hash' do
50
+ _(collection.to_h).must_equal(collection.to_hash)
51
+ end
52
+ end
53
+
54
+ describe '#to_s' do
55
+ it 'returns the wrapped collection as a hash' do
56
+ _(collection.to_s).must_be_kind_of Hash
57
+ end
58
+ end
59
+
60
+ describe '#method_missing' do
61
+ it 'raises an error for missing keys' do
62
+ _(proc { collection.missing_key }).must_raise RuntimeError
63
+ end
64
+ end
65
+
44
66
  describe 'include?' do
45
67
  it 'returns true for keys that exist' do
46
68
  _(collection.include?('_links')).must_equal true
@@ -34,5 +34,12 @@ module Hyperclient
34
34
  _(curie.expand('thumbnail')).must_equal '/images/thumbnail'
35
35
  end
36
36
  end
37
+
38
+ describe 'inspect' do
39
+ it 'outputs a custom-friendly output' do
40
+ _(curie.inspect).must_include 'Curie'
41
+ _(curie.inspect).must_include({ 'name' => 'image', 'href' => '/images/{rel}', 'templated' => true }.to_s)
42
+ end
43
+ end
37
44
  end
38
45
  end
@@ -21,23 +21,25 @@ module Hyperclient
21
21
  it 'can update headers after a connection has been constructed' do
22
22
  _(entry_point.connection).must_be_kind_of Faraday::Connection
23
23
  entry_point.headers.update('Content-Type' => 'application/foobar')
24
+
24
25
  _(entry_point.headers['Content-Type']).must_equal 'application/foobar'
25
26
  end
26
27
 
27
28
  it 'can insert additional middleware after a connection has been constructed' do
28
29
  _(entry_point.connection).must_be_kind_of Faraday::Connection
29
- entry_point.connection.use :instrumentation
30
+ entry_point.connection.use Faraday::Request::Instrumentation
30
31
  handlers = entry_point.connection.builder.handlers
31
- _(handlers).must_include FaradayMiddleware::Instrumentation
32
+
33
+ _(handlers).must_include Faraday::Request::Instrumentation
32
34
  end
33
35
 
34
36
  it 'creates a Faraday connection with the default block' do
35
37
  handlers = entry_point.connection.builder.handlers
36
38
 
37
39
  _(handlers).must_include Faraday::Response::RaiseError
38
- _(handlers).must_include FaradayMiddleware::FollowRedirects
39
- _(handlers).must_include FaradayMiddleware::EncodeHalJson
40
- _(handlers).must_include FaradayMiddleware::ParseHalJson
40
+ _(handlers).must_include Faraday::FollowRedirects::Middleware
41
+ _(handlers).must_include Faraday::HalJson::Request
42
+ _(handlers).must_include Faraday::HalJson::Response
41
43
 
42
44
  _(entry_point.connection.options.params_encoder).must_equal Faraday::FlatParamsEncoder
43
45
  end
@@ -51,6 +53,19 @@ module Hyperclient
51
53
  _(entry_point.connection).must_be_kind_of Faraday::Connection
52
54
  _(-> { entry_point.connection {} }).must_raise ConnectionAlreadyInitializedError
53
55
  end
56
+
57
+ it 'can set the faraday block before a connection has been constructed' do
58
+ block = proc { |conn| conn.use Faraday::Request::Instrumentation }
59
+ entry_point.faraday_block = block
60
+
61
+ _(entry_point.faraday_block).must_equal block
62
+ end
63
+
64
+ it 'raises a ConnectionAlreadyInitializedError if attempting to modify ' \
65
+ 'the faraday block after a connection has been constructed' do
66
+ _(entry_point.connection).must_be_kind_of Faraday::Connection
67
+ _(-> { entry_point.faraday_block = proc {} }).must_raise ConnectionAlreadyInitializedError
68
+ end
54
69
  end
55
70
 
56
71
  describe 'initialize' do
@@ -136,10 +151,11 @@ module Hyperclient
136
151
 
137
152
  it 'creates a Faraday connection with the default block' do
138
153
  handlers = entry_point.connection.builder.handlers
154
+
139
155
  _(handlers).wont_include Faraday::Response::RaiseError
140
- _(handlers).wont_include FaradayMiddleware::FollowRedirects
141
- _(handlers).must_include FaradayMiddleware::EncodeJson
142
- _(handlers).must_include FaradayMiddleware::ParseJson
156
+ _(handlers).wont_include Faraday::FollowRedirects
157
+ _(handlers).must_include Faraday::Request::Json
158
+ _(handlers).must_include Faraday::Response::Json
143
159
  end
144
160
  end
145
161
  end
@@ -148,7 +164,7 @@ module Hyperclient
148
164
  let(:entry_point) do
149
165
  EntryPoint.new 'http://my.api.org' do |entry_point|
150
166
  entry_point.connection do |conn|
151
- conn.use Faraday::Request::OAuth
167
+ conn.use Faraday::Request::Instrumentation
152
168
  end
153
169
  entry_point.headers['Access-Token'] = 'token'
154
170
  end
@@ -168,15 +184,21 @@ module Hyperclient
168
184
  it 'creates a Faraday connection with the default block plus any additional handlers' do
169
185
  handlers = entry_point.connection.builder.handlers
170
186
 
171
- _(handlers).must_include Faraday::Request::OAuth
187
+ _(handlers).must_include Faraday::Request::Instrumentation
172
188
  _(handlers).must_include Faraday::Response::RaiseError
173
- _(handlers).must_include FaradayMiddleware::FollowRedirects
174
- _(handlers).must_include FaradayMiddleware::EncodeHalJson
175
- _(handlers).must_include FaradayMiddleware::ParseHalJson
189
+ _(handlers).must_include Faraday::FollowRedirects::Middleware
190
+ _(handlers).must_include Faraday::HalJson::Request
191
+ _(handlers).must_include Faraday::HalJson::Response
176
192
 
177
193
  _(entry_point.connection.options.params_encoder).must_equal Faraday::FlatParamsEncoder
178
194
  end
179
195
  end
180
196
  end
197
+
198
+ describe ConnectionAlreadyInitializedError do
199
+ it 'has a descriptive message' do
200
+ _(ConnectionAlreadyInitializedError.new.message).must_equal 'The connection has already been initialized.'
201
+ end
202
+ end
181
203
  end
182
204
  end