excon-hypermedia 0.5.0 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
|