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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 6bc648cfd5761dfb1236aada3be47b061620a3ad
4
- data.tar.gz: f0fc534b110a5de92cbe548a4dbe5eb4bcb6f5e6
2
+ SHA256:
3
+ metadata.gz: e5194b922b1a5cfda77304bd8800408c6c4bf8c43fd43e94a61cc46c45370860
4
+ data.tar.gz: 50adb4314587f29da3120b62087afddbd850fb33d62f75d770b225f99c21d482
5
5
  SHA512:
6
- metadata.gz: 83c3ec47d43ce609a789f2bf0ae0281822ae0b4ba25b00596516ef7ed54e9ecc8ab4f3e834e642172b4a1560da4c8184aa4f6a6ac611f63dc84f22e19b4ee226
7
- data.tar.gz: 74fb5d0362acce99b511533c487d18612c81f0c8b455d62633c6fd52e74eb8a262429e6873913e5a5f60e5efae9fdda15132c1602b38a3597e02e0573bf929ea
6
+ metadata.gz: 25688b9b666becd230dcc00c638e336a9b86eefcd13fd5f56e596b2040318a1786e14c8ed6115a3fc0645062a658758431b45dcb5bd5f46b8489ea45ee35af36
7
+ data.tar.gz: 177a1f114fa7696aff96cafdab91a433d3743369cd849182aeda3823329f0343f24398117a04219bf787a3cb881afb214ff2c07ed6950fa7b49e47bda4ece0d1
@@ -3,8 +3,11 @@ AllCops:
3
3
  Exclude:
4
4
  - '.wercker/**/*'
5
5
 
6
+ Metrics/MethodLength:
7
+ Max: 15
8
+
6
9
  Metrics/LineLength:
7
10
  Max: 100
8
11
 
9
12
  Lint/EndAlignment:
10
- AlignWith: variable
13
+ EnforcedStyleAlignWith: variable
@@ -6,6 +6,14 @@ ruby-latest:
6
6
  name: tests
7
7
  code: bundle exec rake
8
8
 
9
+ ruby-25:
10
+ box: ruby:2.5
11
+ steps:
12
+ - bundle-install
13
+ - script:
14
+ name: tests
15
+ code: bundle exec rake
16
+
9
17
  ruby-23:
10
18
  box: ruby:2.3
11
19
  steps:
data/Gemfile CHANGED
@@ -1,3 +1,4 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  source 'https://rubygems.org'
3
4
  gemspec
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. Because of this requirement, the default
284
- configuration has `hcp` disabled, you can either enable it per request (which
285
- also enables it for future requests in the chain), or enable it globally:
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 `emeds`.
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(--display-cop-names --extra-details --display-style-guide)
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(test rubocop)
17
+ task default: %i[test rubocop]
@@ -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(Jean Mertz)
11
- spec.email = %w(jean@mertz.fm)
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(lib)
19
+ spec.require_paths = %w[lib]
19
20
 
20
- spec.add_development_dependency 'bundler', '~> 1.9'
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.49'
29
- spec.add_dependency 'excon-addressable', '~> 0.3'
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
- orig_stack = @stack
21
- @stack = Excon::HyperMedia::Middlewares::HypertextCachePattern.new(orig_stack)
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 (content_type = datum.dig(:response, :headers, 'Content-Type').to_s)
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
- return super unless datum[:hcp] == true && datum[:method] == :get && find_embedded
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
- datum[:response] = {
21
- body: @embedded.to_json,
22
- hcp: true,
23
- headers: content_type_header,
24
- remote_ip: '127.0.0.1',
25
- status: 200
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
- private
43
+ def response_call(datum)
44
+ @datum = datum
32
45
 
33
- def find_embedded
34
- datum.dig(:hcp_params, :embedded).to_h.each do |_, object|
35
- break if (@embedded = object_to_embedded(object))
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
- @embedded
50
+ super
39
51
  end
40
52
 
41
- def object_to_embedded(object)
42
- uri = ::Addressable::URI.new(datum.tap { |h| h.delete(:port) })
53
+ private
43
54
 
44
- if object.respond_to?(:to_ary)
45
- object.find { |hash| hash.dig('_links', 'self', 'href') == uri.to_s }
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 content_type_header
52
- return {} unless (header = datum.dig(:hcp_params, :content_type))
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
- { 'Content-Type' => header }
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(_links _embedded).freeze
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
- hcp_params: {
71
- content_type: response.headers['Content-Type'],
72
- embedded: resource._embedded
73
- }
69
+ embedded: resource._embedded.to_h,
70
+ hypermedia: true
74
71
  )
75
72
  end
76
73
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Excon
4
4
  module HyperMedia
5
- VERSION = '0.5.0'
5
+ VERSION = '0.7.0'
6
6
  end
7
7
  end
@@ -8,22 +8,8 @@ module Excon
8
8
  # Validate edge cases (or: non-happy path)
9
9
  #
10
10
  class EdgeCaseTest < HyperMediaTest
11
- def setup
12
- Excon.defaults[:mock] = true
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
- resource = Excon.get('https://www.example.org/empty_json').resource
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
- resource = Excon.get('https://www.example.org/empty_json').resource
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
- resource = Excon.get('https://www.example.org/empty_json').resource
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
- resource = Excon.get('https://www.example.org/api.json').resource
48
+ assert_nil api.resource._properties.invalid
49
+ assert_nil api.resource._properties['invalid']
50
+ end
69
51
 
70
- assert_equal nil, resource._properties.invalid
71
- assert_equal nil, resource._properties['invalid']
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
- resource = Excon.get('https://www.example.org/empty_json').resource
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
- resource = Excon.get('https://www.example.org/api.json').resource
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
@@ -9,28 +9,37 @@ module Excon
9
9
  #
10
10
  class HCPTest < HyperMediaTest
11
11
  def response
12
- @response ||= Excon.get('https://example.org/product/bicycle')
12
+ bicycle
13
13
  end
14
14
 
15
15
  def test_non_hcp_response
16
- assert_equal nil, response[:hcp]
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
- api = Excon.get('https://www.example.org/api.json')
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
- assert_equal nil, response[:hcp]
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 wheels.map(&:get).all? { |res| res[:hcp] }
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