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.
- checksums.yaml +4 -4
- data/.github/dependabot.yml +18 -0
- data/.github/workflows/coverage.yml +21 -0
- data/.github/workflows/danger-comment.yml +11 -0
- data/.github/workflows/danger.yml +13 -0
- data/.github/workflows/lint.yml +16 -0
- data/.github/workflows/test.yml +28 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +5 -0
- data/.rubocop_todo.yml +40 -31
- data/AGENTS.md +26 -0
- data/CHANGELOG.md +21 -3
- data/CONTRIBUTING.md +11 -1
- data/Dangerfile +2 -0
- data/Gemfile +10 -7
- data/README.md +49 -44
- data/RELEASING.md +47 -41
- data/Rakefile +0 -8
- data/examples/oxford/README.md +42 -0
- data/examples/oxford/app.rb +48 -0
- data/examples/oxford/client.rb +13 -0
- data/examples/oxford/places.json +21 -0
- data/examples/oxford/server.rb +13 -0
- data/features/steps/api_navigation.rb +3 -3
- data/features/steps/default_config.rb +2 -2
- data/features/support/api.rb +22 -11
- data/features/support/env.rb +15 -1
- data/hyperclient.gemspec +4 -4
- data/lib/hyperclient/entry_point.rb +3 -3
- data/lib/hyperclient/link.rb +13 -4
- data/lib/hyperclient/resource.rb +13 -3
- data/lib/hyperclient/resource_collection.rb +1 -3
- data/lib/hyperclient/version.rb +1 -1
- data/test/hyperclient/attributes_test.rb +9 -0
- data/test/hyperclient/collection_test.rb +22 -0
- data/test/hyperclient/curie_test.rb +7 -0
- data/test/hyperclient/entry_point_test.rb +35 -13
- data/test/hyperclient/link_collection_test.rb +10 -0
- data/test/hyperclient/link_test.rb +128 -1
- data/test/hyperclient/resource_test.rb +36 -2
- data/test/hyperclient_test.rb +6 -5
- data/test/oxford_example_test.rb +52 -0
- data/test/test_helper.rb +19 -1
- metadata +25 -33
- data/.travis.yml +0 -24
- 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
|
-
|
|
3
|
+
Release bug fixes frequently, features less frequently, and breaking API changes rarely.
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
## Prepare the Release
|
|
7
6
|
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
13
|
+
Run the full suite, then commit and push the release preparation:
|
|
23
14
|
|
|
24
|
-
```
|
|
25
|
-
|
|
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
|
-
|
|
23
|
+
Wait for CI on the release preparation commit to succeed before publishing.
|
|
30
24
|
|
|
31
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
59
|
+
## Prepare the Next Version
|
|
51
60
|
|
|
52
|
-
|
|
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
|
|
56
|
-
============
|
|
63
|
+
```markdown
|
|
64
|
+
### 2.1.1 (Next)
|
|
57
65
|
|
|
58
66
|
* Your contribution here.
|
|
59
67
|
```
|
|
60
68
|
|
|
61
|
-
|
|
69
|
+
Commit and push:
|
|
62
70
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
```
|
|
71
|
+
```sh
|
|
66
72
|
git add CHANGELOG.md lib/hyperclient/version.rb
|
|
67
|
-
git commit -m "Preparing for next release,
|
|
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
|
|
47
|
-
@post._attributes.body
|
|
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
|
|
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
|
|
29
|
-
@posts._attributes['total_posts'].to_i
|
|
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
|
data/features/support/api.rb
CHANGED
|
@@ -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,
|
|
11
|
-
|
|
12
|
-
stub_request(:get, 'api.example.org
|
|
13
|
-
|
|
14
|
-
stub_request(:get, 'api.example.org/
|
|
15
|
-
|
|
16
|
-
stub_request(:get, 'api.example.org/posts
|
|
17
|
-
|
|
18
|
-
stub_request(:get, 'api.example.org/posts
|
|
19
|
-
|
|
20
|
-
stub_request(:get, 'api.example.org/
|
|
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
|
data/features/support/env.rb
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
|
-
require 'minitest
|
|
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', '>=
|
|
18
|
-
gem.add_dependency '
|
|
19
|
-
gem.add_dependency '
|
|
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 '
|
|
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::
|
|
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
|
|
141
|
+
connection.use Faraday::FollowRedirects::Middleware
|
|
142
142
|
connection.request :hal_json
|
|
143
143
|
connection.response :hal_json, content_type: /\bjson$/
|
|
144
144
|
|
data/lib/hyperclient/link.rb
CHANGED
|
@@ -190,10 +190,19 @@ module Hyperclient
|
|
|
190
190
|
end
|
|
191
191
|
|
|
192
192
|
def http_method(method, body = nil)
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
data/lib/hyperclient/resource.rb
CHANGED
|
@@ -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
|
data/lib/hyperclient/version.rb
CHANGED
|
@@ -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
|
|
30
|
+
entry_point.connection.use Faraday::Request::Instrumentation
|
|
30
31
|
handlers = entry_point.connection.builder.handlers
|
|
31
|
-
|
|
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
|
|
39
|
-
_(handlers).must_include
|
|
40
|
-
_(handlers).must_include
|
|
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
|
|
141
|
-
_(handlers).must_include
|
|
142
|
-
_(handlers).must_include
|
|
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::
|
|
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::
|
|
187
|
+
_(handlers).must_include Faraday::Request::Instrumentation
|
|
172
188
|
_(handlers).must_include Faraday::Response::RaiseError
|
|
173
|
-
_(handlers).must_include
|
|
174
|
-
_(handlers).must_include
|
|
175
|
-
_(handlers).must_include
|
|
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
|