excon-hypermedia 0.5.0 → 0.7.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 +5 -5
- data/.rubocop.yml +4 -1
- data/.wercker.yml +8 -0
- data/Gemfile +1 -0
- data/README.md +7 -4
- data/Rakefile +3 -2
- data/excon-hypermedia.gemspec +12 -10
- data/lib/excon/hypermedia/helpers/collection.rb +12 -1
- data/lib/excon/hypermedia/middleware.rb +18 -3
- data/lib/excon/hypermedia/middlewares/hypertext_cache_pattern.rb +49 -24
- data/lib/excon/hypermedia/resource_object.rb +6 -2
- data/lib/excon/hypermedia/response.rb +2 -5
- data/lib/excon/hypermedia/version.rb +1 -1
- data/test/excon/edgecase_test.rb +14 -36
- data/test/excon/hcp_test.rb +15 -10
- data/test/excon/integration_test.rb +9 -14
- data/test/excon/link_object_test.rb +5 -6
- data/test/excon/links_test.rb +5 -6
- data/test/excon/middleware_test.rb +58 -0
- data/test/excon/properties_test.rb +2 -3
- data/test/excon/resource_object_test.rb +13 -5
- data/test/excon/response_test.rb +1 -1
- data/test/support/responses.rb +144 -0
- data/test/support/server.rb +35 -0
- data/test/test_helper.rb +32 -144
- metadata +44 -28
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: e5194b922b1a5cfda77304bd8800408c6c4bf8c43fd43e94a61cc46c45370860
|
4
|
+
data.tar.gz: 50adb4314587f29da3120b62087afddbd850fb33d62f75d770b225f99c21d482
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 25688b9b666becd230dcc00c638e336a9b86eefcd13fd5f56e596b2040318a1786e14c8ed6115a3fc0645062a658758431b45dcb5bd5f46b8489ea45ee35af36
|
7
|
+
data.tar.gz: 177a1f114fa7696aff96cafdab91a433d3743369cd849182aeda3823329f0343f24398117a04219bf787a3cb881afb214ff2c07ed6950fa7b49e47bda4ece0d1
|
data/.rubocop.yml
CHANGED
data/.wercker.yml
CHANGED
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -280,9 +280,12 @@ pump.resource.weight # => '2kg'
|
|
280
280
|
```
|
281
281
|
|
282
282
|
This feature only works if you are sure the embedded resource is equal to the
|
283
|
-
resource returned by the link relation.
|
284
|
-
|
285
|
-
|
283
|
+
resource returned by the link relation. Also, the embedded resource needs to
|
284
|
+
have a `self` link in order to stub the correct endpoint.
|
285
|
+
|
286
|
+
Because of these requirement, the default configuration has `hcp` disabled, you
|
287
|
+
can either enable it per request (which also enables it for future requests in
|
288
|
+
the chain), or enable it globally:
|
286
289
|
|
287
290
|
```ruby
|
288
291
|
Excon.defaults[:hcp] = true
|
@@ -291,7 +294,7 @@ Excon.defaults[:hcp] = true
|
|
291
294
|
### shortcuts
|
292
295
|
|
293
296
|
While the above examples shows the clean separation between the different
|
294
|
-
concepts like `response`, `resource`, `links`, `properties` and `
|
297
|
+
concepts like `response`, `resource`, `links`, `properties` and `embeds`.
|
295
298
|
|
296
299
|
Traversing these objects always starts from the response object. To make moving
|
297
300
|
around a bit faster, there are several methods available on the
|
data/Rakefile
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'bundler/gem_tasks'
|
3
4
|
require 'rake/testtask'
|
4
5
|
require 'rubocop/rake_task'
|
5
6
|
|
6
7
|
RuboCop::RakeTask.new do |t|
|
7
|
-
t.options = %w
|
8
|
+
t.options = %w[--display-cop-names --extra-details --display-style-guide]
|
8
9
|
end
|
9
10
|
|
10
11
|
Rake::TestTask.new(:test) do |t|
|
@@ -13,4 +14,4 @@ Rake::TestTask.new(:test) do |t|
|
|
13
14
|
t.test_files = FileList['test/**/*_test.rb']
|
14
15
|
end
|
15
16
|
|
16
|
-
task default: %i
|
17
|
+
task default: %i[test rubocop]
|
data/excon-hypermedia.gemspec
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
# encoding: utf-8
|
3
|
+
|
3
4
|
lib = File.expand_path('../lib', __FILE__)
|
4
5
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
6
|
require 'excon/hypermedia/version'
|
@@ -7,24 +8,25 @@ require 'excon/hypermedia/version'
|
|
7
8
|
Gem::Specification.new do |spec|
|
8
9
|
spec.name = 'excon-hypermedia'
|
9
10
|
spec.version = Excon::HyperMedia::VERSION
|
10
|
-
spec.authors = %w
|
11
|
-
spec.email = %w
|
11
|
+
spec.authors = %w[Jean Mertz]
|
12
|
+
spec.email = %w[jean@mertz.fm]
|
12
13
|
|
13
14
|
spec.summary = 'Excon, with Hypermedia traversing baked in.'
|
14
15
|
spec.description = 'Excon, with Hypermedia traversing baked in.'
|
15
16
|
spec.homepage = 'https://github.com/JeanMertz/excon-hypermedia'
|
16
17
|
spec.license = 'MIT'
|
17
18
|
spec.files = `git ls-files -z`.split("\x0")
|
18
|
-
spec.require_paths = %w
|
19
|
+
spec.require_paths = %w[lib]
|
19
20
|
|
20
|
-
spec.add_development_dependency 'bundler', '
|
21
|
-
spec.add_development_dependency 'rake', '~> 10.0'
|
22
|
-
spec.add_development_dependency 'minitest', '~> 5.0'
|
23
|
-
spec.add_development_dependency 'rubocop', '~> 0.40'
|
24
|
-
spec.add_development_dependency 'pry', '~> 0.10'
|
21
|
+
spec.add_development_dependency 'bundler', '>= 1.15'
|
25
22
|
spec.add_development_dependency 'm', '~> 1.5'
|
23
|
+
spec.add_development_dependency 'minitest', '~> 5.11'
|
24
|
+
spec.add_development_dependency 'open4', '~> 1.3'
|
25
|
+
spec.add_development_dependency 'pry', '~> 0.11'
|
26
|
+
spec.add_development_dependency 'rake', '~> 12.3'
|
27
|
+
spec.add_development_dependency 'rubocop', '~> 0.52'
|
26
28
|
|
27
29
|
spec.add_dependency 'backport_dig' if RUBY_VERSION < '2.3'
|
28
|
-
spec.add_dependency 'excon', '~> 0.
|
29
|
-
spec.add_dependency 'excon-addressable', '~> 0.
|
30
|
+
spec.add_dependency 'excon', '~> 0.60'
|
31
|
+
spec.add_dependency 'excon-addressable', '~> 0.4'
|
30
32
|
end
|
@@ -45,10 +45,21 @@ module Excon
|
|
45
45
|
# The second notation returns `nil` on missing keys, the first should do
|
46
46
|
# as well.
|
47
47
|
#
|
48
|
-
def method_missing(_)
|
48
|
+
def method_missing(_) # rubocop:disable Style/MethodMissing
|
49
49
|
nil
|
50
50
|
end
|
51
51
|
|
52
|
+
# respond_to_missing?
|
53
|
+
#
|
54
|
+
# Checking if a key exists should be possible using `respond_to?`:
|
55
|
+
#
|
56
|
+
# collection.respond_to?(:hello_world)
|
57
|
+
# # => false
|
58
|
+
#
|
59
|
+
def respond_to_missing?(_, _ = false)
|
60
|
+
super
|
61
|
+
end
|
62
|
+
|
52
63
|
def to_properties
|
53
64
|
collection.each do |key, value|
|
54
65
|
key = key.downcase
|
@@ -5,7 +5,15 @@ require 'backport_dig' if RUBY_VERSION < '2.3'
|
|
5
5
|
Excon.defaults[:middlewares].delete(Excon::Addressable::Middleware)
|
6
6
|
Excon.defaults[:middlewares].unshift(Excon::Addressable::Middleware)
|
7
7
|
|
8
|
+
# Excon
|
9
|
+
#
|
10
|
+
# We inject the `expand` key to the allowed lists of keys to be used when
|
11
|
+
# creating a request, or connection object. Excon does not enforce this yet, but
|
12
|
+
# it does print a warning, so this makes things future-proof.
|
8
13
|
module Excon
|
14
|
+
VALID_REQUEST_KEYS.push(:hcp, :embedded, :hypermedia)
|
15
|
+
VALID_CONNECTION_KEYS.push(:hcp, :embedded, :hypermedia)
|
16
|
+
|
9
17
|
module HyperMedia
|
10
18
|
# Middleware
|
11
19
|
#
|
@@ -17,13 +25,20 @@ module Excon
|
|
17
25
|
#
|
18
26
|
class Middleware < Excon::Middleware::Base
|
19
27
|
def request_call(datum)
|
20
|
-
|
21
|
-
|
28
|
+
# if `hcp` is enabled, insert the `HypertextCachePattern` middleware in
|
29
|
+
# the middleware stack right after this one.
|
30
|
+
if datum[:hcp]
|
31
|
+
orig_stack = @stack
|
32
|
+
@stack = Excon::HyperMedia::Middlewares::HypertextCachePattern.new(orig_stack)
|
33
|
+
end
|
34
|
+
|
22
35
|
super
|
23
36
|
end
|
24
37
|
|
25
38
|
def response_call(datum)
|
26
|
-
return super unless (
|
39
|
+
return super unless (headers = datum.dig(:response, :headers))
|
40
|
+
return super unless (match = headers.find { |k, v| k.downcase == 'content-type' })
|
41
|
+
content_type = match[1].to_s
|
27
42
|
|
28
43
|
datum[:response][:hypermedia] = if datum[:hypermedia].nil?
|
29
44
|
content_type.include?('hal+json')
|
@@ -15,43 +15,68 @@ module Excon
|
|
15
15
|
def request_call(datum)
|
16
16
|
@datum = datum
|
17
17
|
|
18
|
-
|
18
|
+
if stubs.any?
|
19
|
+
# We've created new stubs. The request should be marked as `mocked`
|
20
|
+
# to make sure the stubbed response is returned.
|
21
|
+
datum[:mock] = true
|
19
22
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
23
|
+
# The requested resource might not be part of the embedded resources
|
24
|
+
# so we allow external requests.
|
25
|
+
# datum[:allow_unstubbed_requests] = true
|
26
|
+
|
27
|
+
# Make sure Excon's `Mock` middleware runs after this middleware, as
|
28
|
+
# it might have already triggered in the middleware chain.
|
29
|
+
orig_stack = @stack
|
30
|
+
@stack = Excon::Middleware::Mock.new(orig_stack)
|
31
|
+
end
|
32
|
+
|
33
|
+
super
|
34
|
+
rescue StandardError => e
|
35
|
+
raise unless e.class == Excon::Errors::StubNotFound
|
27
36
|
|
37
|
+
# If a request was made to a non-stubbed resource, don't use the Mock
|
38
|
+
# middleware, but simply send the request to the server.
|
39
|
+
@stack = orig_stack
|
28
40
|
super
|
29
41
|
end
|
30
42
|
|
31
|
-
|
43
|
+
def response_call(datum)
|
44
|
+
@datum = datum
|
32
45
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
end
|
46
|
+
# After the response is returned, remove any request-specific stubs
|
47
|
+
# from Excon, so they can't be accidentally re-used anymore.
|
48
|
+
embedded.each { |r| (match = matcher(r)) && Excon.unstub(match) }
|
37
49
|
|
38
|
-
|
50
|
+
super
|
39
51
|
end
|
40
52
|
|
41
|
-
|
42
|
-
uri = ::Addressable::URI.new(datum.tap { |h| h.delete(:port) })
|
53
|
+
private
|
43
54
|
|
44
|
-
|
45
|
-
|
46
|
-
elsif object.dig('_links', 'self', 'href') == uri.to_s
|
47
|
-
object
|
48
|
-
end
|
55
|
+
def stubs
|
56
|
+
embedded.each { |r| (match = matcher(r)) && Excon.stub(match, response(r)) }.compact
|
49
57
|
end
|
50
58
|
|
51
|
-
def
|
52
|
-
return
|
59
|
+
def matcher(resource)
|
60
|
+
return unless (uri = ::Addressable::URI.parse(resource.dig('_links', 'self', 'href')))
|
61
|
+
|
62
|
+
{
|
63
|
+
scheme: uri.scheme,
|
64
|
+
host: uri.host,
|
65
|
+
path: uri.path,
|
66
|
+
query: uri.query
|
67
|
+
}
|
68
|
+
end
|
69
|
+
|
70
|
+
def response(resource)
|
71
|
+
{
|
72
|
+
body: resource.to_json,
|
73
|
+
hcp: true,
|
74
|
+
headers: { 'Content-Type' => 'application/hal+json', 'X-HCP' => 'true' }
|
75
|
+
}
|
76
|
+
end
|
53
77
|
|
54
|
-
|
78
|
+
def embedded
|
79
|
+
datum[:embedded].to_h.values.flatten
|
55
80
|
end
|
56
81
|
end
|
57
82
|
end
|
@@ -11,7 +11,7 @@ module Excon
|
|
11
11
|
# Represents a resource.
|
12
12
|
#
|
13
13
|
class ResourceObject
|
14
|
-
RESERVED_PROPERTIES = %w
|
14
|
+
RESERVED_PROPERTIES = %w[_links _embedded].freeze
|
15
15
|
|
16
16
|
def initialize(data)
|
17
17
|
@data = data
|
@@ -58,7 +58,11 @@ module Excon
|
|
58
58
|
end
|
59
59
|
|
60
60
|
def method_missing(method_name, *_)
|
61
|
-
_properties.send(method_name)
|
61
|
+
_properties.respond_to?(method_name) ? _properties.send(method_name) : super
|
62
|
+
end
|
63
|
+
|
64
|
+
def respond_to_missing?(method_name, _ = false)
|
65
|
+
_properties.respond_to?(method_name) || super
|
62
66
|
end
|
63
67
|
end
|
64
68
|
end
|
@@ -65,12 +65,9 @@ module Excon
|
|
65
65
|
|
66
66
|
def rel_params(params)
|
67
67
|
params.merge(
|
68
|
-
hypermedia: true,
|
69
68
|
hcp: (params[:hcp].nil? ? response.data[:hcp] : params[:hcp]),
|
70
|
-
|
71
|
-
|
72
|
-
embedded: resource._embedded
|
73
|
-
}
|
69
|
+
embedded: resource._embedded.to_h,
|
70
|
+
hypermedia: true
|
74
71
|
)
|
75
72
|
end
|
76
73
|
end
|
data/test/excon/edgecase_test.rb
CHANGED
@@ -8,22 +8,8 @@ module Excon
|
|
8
8
|
# Validate edge cases (or: non-happy path)
|
9
9
|
#
|
10
10
|
class EdgeCaseTest < HyperMediaTest
|
11
|
-
def
|
12
|
-
|
13
|
-
Excon.defaults[:middlewares].push(Excon::HyperMedia::Middleware)
|
14
|
-
|
15
|
-
response = { headers: { 'Content-Type' => 'application/hal+json' } }
|
16
|
-
Excon.stub({ path: '/api.json' }, response.merge(body: api_body))
|
17
|
-
Excon.stub({ path: '/empty_json' }, response.merge(body: '{}'))
|
18
|
-
end
|
19
|
-
|
20
|
-
def teardown
|
21
|
-
Excon.stubs.clear
|
22
|
-
Excon.defaults[:middlewares].delete(Excon::HyperMedia::Middleware)
|
23
|
-
end
|
24
|
-
|
25
|
-
def api
|
26
|
-
Excon.get('https://www.example.org/api.json')
|
11
|
+
def empty_json_resource
|
12
|
+
empty_json_response.resource
|
27
13
|
end
|
28
14
|
|
29
15
|
def test_missing_middleware
|
@@ -47,42 +33,34 @@ module Excon
|
|
47
33
|
end
|
48
34
|
|
49
35
|
def test_missing_links
|
50
|
-
|
51
|
-
|
52
|
-
assert_equal({}, resource._links.to_h)
|
36
|
+
assert_equal({}, empty_json_resource._links.to_h)
|
53
37
|
end
|
54
38
|
|
55
39
|
def test_missing_embedded
|
56
|
-
|
57
|
-
|
58
|
-
assert_equal({}, resource._embedded.to_h)
|
40
|
+
assert_equal({}, empty_json_resource._embedded.to_h)
|
59
41
|
end
|
60
42
|
|
61
43
|
def test_missing_properties
|
62
|
-
|
63
|
-
|
64
|
-
assert_equal({}, resource._properties.to_h)
|
44
|
+
assert_equal({}, empty_json_resource._properties.to_h)
|
65
45
|
end
|
66
46
|
|
67
47
|
def test_unknown_property
|
68
|
-
|
48
|
+
assert_nil api.resource._properties.invalid
|
49
|
+
assert_nil api.resource._properties['invalid']
|
50
|
+
end
|
69
51
|
|
70
|
-
|
71
|
-
assert_equal
|
52
|
+
def test_unknown_property_respond_to
|
53
|
+
assert_equal false, api.resource._properties.respond_to?(:invalid)
|
72
54
|
end
|
73
55
|
|
74
56
|
def test_unknown_link
|
75
|
-
|
76
|
-
|
77
|
-
assert_equal nil, resource._links.invalid
|
78
|
-
assert_equal nil, resource._links['invalid']
|
57
|
+
assert_nil empty_json_resource._links.invalid
|
58
|
+
assert_nil empty_json_resource._links['invalid']
|
79
59
|
end
|
80
60
|
|
81
61
|
def test_unknown_embed
|
82
|
-
|
83
|
-
|
84
|
-
assert_equal nil, resource._embedded.invalid
|
85
|
-
assert_equal nil, resource._embedded['invalid']
|
62
|
+
assert_nil api.resource._embedded.invalid
|
63
|
+
assert_nil api.resource._embedded['invalid']
|
86
64
|
end
|
87
65
|
end
|
88
66
|
end
|
data/test/excon/hcp_test.rb
CHANGED
@@ -9,28 +9,37 @@ module Excon
|
|
9
9
|
#
|
10
10
|
class HCPTest < HyperMediaTest
|
11
11
|
def response
|
12
|
-
|
12
|
+
bicycle
|
13
13
|
end
|
14
14
|
|
15
15
|
def test_non_hcp_response
|
16
|
-
|
16
|
+
assert_nil response[:hcp]
|
17
17
|
end
|
18
18
|
|
19
19
|
def test_hcp_response
|
20
20
|
assert response.rel('pump', hcp: true).get[:hcp]
|
21
21
|
end
|
22
22
|
|
23
|
+
def test_hcp_response_without_existing_response
|
24
|
+
assert Excon.get(url('/product/bicycle'), hcp: true)
|
25
|
+
end
|
26
|
+
|
23
27
|
def test_hcp_response_with_missing_embedding
|
24
|
-
|
25
|
-
response = api.rel('product', expand: { uid: 'bicycle' }, rel: true).get
|
28
|
+
response = api.rel('product', expand: { uid: 'bicycle' }, hcp: true).get
|
26
29
|
|
27
|
-
|
30
|
+
assert_nil response[:hcp]
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_hcp_response_with_embedding_but_missing_embed_for_request
|
34
|
+
handlebar = response.rel('handlebar', hcp: true).get
|
35
|
+
|
36
|
+
assert_nil handlebar[:hcp]
|
28
37
|
end
|
29
38
|
|
30
39
|
def test_hcp_response_with_embedded_array
|
31
40
|
wheels = response.rel('wheels', hcp: true)
|
32
41
|
|
33
|
-
assert
|
42
|
+
assert(wheels.map(&:get).all? { |res| res[:hcp] })
|
34
43
|
end
|
35
44
|
|
36
45
|
def test_nested_hcp_responses
|
@@ -40,10 +49,6 @@ module Excon
|
|
40
49
|
assert response[:hcp]
|
41
50
|
end
|
42
51
|
|
43
|
-
def test_hcp_not_working_for_non_get_requests
|
44
|
-
assert_equal nil, response.rel('pump', hcp: true).post[:hcp]
|
45
|
-
end
|
46
|
-
|
47
52
|
def test_hcp_resource
|
48
53
|
resource = response.rel('pump', hcp: true).get.resource
|
49
54
|
|