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 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