excon-hypermedia 0.1.0 → 0.2.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
2
  SHA1:
3
- metadata.gz: cb36e9f12e5909b7e4d4321184f39582882da139
4
- data.tar.gz: 3b6c39402e531bd8055c75a032aba0db380c6635
3
+ metadata.gz: 375d919cb0b7562e0664ae44c30d3bdd7b71acf3
4
+ data.tar.gz: 5d48e0eff3d6f00c2968bb32a330949833a81745
5
5
  SHA512:
6
- metadata.gz: 759eedec55a1da3d4aced37c4385dcafb38c44ba1e4636222398251808ac6972b52dc5962d471ee3547032605d204f7c2bff3e12d3395f1f6dfd82b3e41f08dc
7
- data.tar.gz: 08371cb0d025c67159f85481d2e2ef61cea5b8cb08e0dbf28917cd434f3fccdfb84ceaa0cef2af56d1a2f818c7b9f426f0fdd005731abb4b6ca66fb6b9b65e97
6
+ metadata.gz: cc9e8bfd092cef7babc130b59e8debd50eb84680a37d91da238ec84888ebdae22ea9808035cc324a52ba81833eb67daffb4a5cdd68856fe8c4209bb5d0ca5d77
7
+ data.tar.gz: 080f93835dba3d7648c37377293428dadaf1e60281511f8d014debf4dacb5f65555cb96de919461229c718fa0fa253481275a4beff9f9b383d0888184d9c0c56
data/.rubocop.yml CHANGED
@@ -5,3 +5,6 @@ AllCops:
5
5
 
6
6
  Metrics/LineLength:
7
7
  Max: 100
8
+
9
+ Lint/EndAlignment:
10
+ AlignWith: variable
data/README.md CHANGED
@@ -30,19 +30,20 @@ relations. It returns raw response bodies in string format.
30
30
 
31
31
  This gem adds a thin layer on top of [Excon][excon] to make it talk with an
32
32
  HyperMedia-enabled API. To let Excon know the connection supports HyperMedia,
33
- simply add the `hypermedia: true` option.
33
+ simply enable the correct middleware (either globally, or per-connection):
34
34
 
35
35
  ```ruby
36
- conn = Excon.new('http://www.example.com/api.json', hypermedia: true)
37
- conn.class # => Excon::Connection
36
+ Excon.defaults[:middlewares].push(Excon::HyperMedia::Middleware)
37
+
38
+ api = Excon.get('http://www.example.com/api.json')
39
+ api.class # => Excon::Response
38
40
  ```
39
41
 
40
- From that point on, you can use this single connection to make all requests. The
41
- `hypermedia` option will be passed on to all subsequent connection objects, as
42
- long as you keep chaining the requests from the original top-level connection.
42
+ Using the `HyperMedia` middleware, the `Excon::Response` object now knows how
43
+ to handle the HyperMedia aspect of the API:
43
44
 
44
45
  ```ruby
45
- product = conn.product(expand: { uid: 'hello' })
46
+ product = api.product(expand: { uid: 'hello' })
46
47
  product.class # => Excon::Connection
47
48
 
48
49
  response = product.get
@@ -53,16 +54,9 @@ response.body.class # => String
53
54
  As seen above, you can expand URI Template variables using the `expand` option,
54
55
  provided by the [`excon-addressable` library][excon-addressable].
55
56
 
56
- You can mark any connection object as hypermedia-aware not just the top-level
57
- entrypoint by passing in the `hypermedia: true` option:
58
-
59
- ```ruby
60
- user = Excon.new('http://www.example.com/users/jeanmertz', hypermedia: true)
61
- user.orders.class # => Excon::Connection
62
- ```
63
-
64
- Since each new resource is simply an `Excon::Connection` object, all
65
- [Excon-provided options][options] are available as well:
57
+ Since each new resource is simply an `Excon::Response` object, accessed through
58
+ the default `Excon::Connection` object, all [Excon-provided options][options]
59
+ are available as well:
66
60
 
67
61
  ```ruby
68
62
  product.get(idempotent: true, retry_limit: 6)
@@ -74,10 +68,8 @@ The gem is available as open source under the terms of the [MIT License](http://
74
68
 
75
69
  ## TODO
76
70
 
77
- * use Excon's Middleware system
78
71
  * make it easy to access attributes in response objects
79
72
  * properly handle curied-links and/or non-valid Ruby method name links
80
- * work correctly with Excon.get/post/delete shortcut methods
81
73
 
82
74
  [excon]: https://github.com/excon/excon
83
75
  [hypermedia]: https://en.wikipedia.org/wiki/HATEOAS
@@ -6,7 +6,7 @@ require 'excon/hypermedia/version'
6
6
 
7
7
  Gem::Specification.new do |spec|
8
8
  spec.name = 'excon-hypermedia'
9
- spec.version = Excon::Hypermedia::VERSION
9
+ spec.version = Excon::HyperMedia::VERSION
10
10
  spec.authors = %w(Jean Mertz)
11
11
  spec.email = %w(jean@mertz.fm)
12
12
 
@@ -22,6 +22,7 @@ Gem::Specification.new do |spec|
22
22
  spec.add_development_dependency 'minitest', '~> 5.0'
23
23
  spec.add_development_dependency 'rubocop', '~> 0.40'
24
24
  spec.add_development_dependency 'pry', '~> 0.10'
25
+ spec.add_development_dependency 'm', '~> 1.5.0'
25
26
 
26
27
  spec.add_dependency 'excon', '~> 0.49'
27
28
  spec.add_dependency 'excon-addressable', '~> 0.1'
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Excon
4
+ module HyperMedia
5
+ module Ext
6
+ # Ext::Response
7
+ #
8
+ # Overloads the default `Excon::Response` to add a thin HyperMedia layer
9
+ # on top.
10
+ #
11
+ module Response
12
+ def method_missing(method_name, *params)
13
+ hypermedia_response.handle(method_name, *params) || super
14
+ end
15
+
16
+ def respond_to_missing?(method_name, include_private = false)
17
+ hypermedia_response.handle(method_name, *params) != false || super
18
+ end
19
+
20
+ def hypermedia_response
21
+ @hypermedia_response ||= HyperMedia::Response.new(self)
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ # :nodoc:
28
+ class Response
29
+ prepend HyperMedia::Ext::Response
30
+ end
31
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Excon
4
+ module HyperMedia
5
+ # Link
6
+ #
7
+ # This HyperMedia::Link object encapsulates a link pointing to a resource.
8
+ #
9
+ class Link
10
+ attr_reader :name
11
+
12
+ def initialize(name:, hash:)
13
+ @hash = hash
14
+ @name = name
15
+ end
16
+
17
+ def valid?
18
+ link_data.keys.any?
19
+ end
20
+
21
+ def invalid?
22
+ !valid?
23
+ end
24
+
25
+ def uri
26
+ ::Addressable::URI.parse(href)
27
+ end
28
+
29
+ def href
30
+ link_data['href']
31
+ end
32
+
33
+ private
34
+
35
+ def link_data
36
+ @hash.dig('_links', name.to_s) || {}
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ Excon.defaults[:middlewares].unshift(Excon::Addressable::Middleware)
4
+
5
+ module Excon
6
+ module HyperMedia
7
+ # Middleware
8
+ #
9
+ # This middleware sets the `hypermedia` datum to `true`, if the returned
10
+ # `Content-Type` header contains `hal+json`.
11
+ #
12
+ # If the `hypermedia` attribute is already set for the connection, it
13
+ # will be left alone by this middleware.
14
+ #
15
+ class Middleware < Excon::Middleware::Base
16
+ def request_call(datum)
17
+ return super unless (content_type = datum.dig(:response, :headers, 'Content-Type').to_s)
18
+
19
+ datum[:response][:hypermedia] = if datum[:hypermedia].nil?
20
+ content_type.include?('hal+json')
21
+ else
22
+ datum[:hypermedia]
23
+ end
24
+
25
+ super
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Excon
6
+ module HyperMedia
7
+ # Resource
8
+ #
9
+ # This HyperMedia::Resource object encapsulates the returned JSON and
10
+ # makes it easy to access the links and attributes.
11
+ #
12
+ class Resource
13
+ attr_reader :data
14
+
15
+ def initialize(body)
16
+ @body = body
17
+ end
18
+
19
+ def links
20
+ data.fetch('_links', {}).keys.map { |name| link(name) }
21
+ end
22
+
23
+ def link(link_name)
24
+ Link.new(name: link_name, hash: data)
25
+ end
26
+
27
+ def attributes
28
+ attributes = data.reject do |k, _|
29
+ k == '_links'
30
+ end
31
+
32
+ Struct.new(*attributes.keys.map(&:to_sym)).new(*attributes.values)
33
+ end
34
+
35
+ def type?(name)
36
+ return :link if link(name).valid?
37
+ return :attribute if attributes.respond_to?(name.to_s)
38
+
39
+ :unknown
40
+ end
41
+
42
+ def data
43
+ @data ||= JSON.parse(@body)
44
+ rescue JSON::ParserError
45
+ {}
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'excon/hypermedia/ext/response'
4
+
5
+ module Excon
6
+ module HyperMedia
7
+ # Response
8
+ #
9
+ # This HyperMedia::Response object helps determine valid subsequent
10
+ # requests and attribute values.
11
+ #
12
+ class Response
13
+ attr_reader :response
14
+
15
+ def initialize(response)
16
+ @response = response
17
+ end
18
+
19
+ # handle
20
+ #
21
+ # Correctly handle the hypermedia request.
22
+ #
23
+ def handle(method_name, *params)
24
+ return false if disabled?
25
+
26
+ case resource.type?(method_name)
27
+ when :link then return handle_link(method_name, params)
28
+ when :attribute then return handle_attribute(method_name)
29
+ end
30
+
31
+ respond_to?(method_name) ? send(method_name) : false
32
+ end
33
+
34
+ def links
35
+ resource.links
36
+ end
37
+
38
+ def attributes
39
+ resource.attributes
40
+ end
41
+
42
+ def resource
43
+ @resource ||= Resource.new(response.body)
44
+ end
45
+
46
+ def enabled?
47
+ response.data[:hypermedia] == true
48
+ end
49
+
50
+ def disabled?
51
+ !enabled?
52
+ end
53
+
54
+ private
55
+
56
+ def handle_link(name, params)
57
+ Excon.new(resource.link(name).href, params.first.to_h.merge(hypermedia: true))
58
+ end
59
+
60
+ def handle_attribute(name)
61
+ attributes[name.to_s]
62
+ end
63
+ end
64
+ end
65
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Excon
3
- module Hypermedia
4
- VERSION = '0.1.0'
4
+ module HyperMedia
5
+ VERSION = '0.2.0'
5
6
  end
6
7
  end
@@ -1,17 +1,9 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'excon'
3
4
  require 'excon/addressable'
4
- require 'excon/hypermedia/hyper_media'
5
-
6
- # :nodoc:
7
- module Excon
8
- # HyperMedia addition to Excon.
9
- #
10
- module Hypermedia
11
- def new(url, params = {})
12
- params[:hypermedia] ? super.extend(HyperMedia) : super
13
- end
14
- end
15
-
16
- singleton_class.prepend Hypermedia
17
- end
5
+ require 'excon/hypermedia/link'
6
+ require 'excon/hypermedia/middleware'
7
+ require 'excon/hypermedia/resource'
8
+ require 'excon/hypermedia/response'
9
+ require 'excon/hypermedia/version'
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
- # rubocop:disable Metrics/AbcSize, Metrics/LineLength
2
+ # rubocop:disable Metrics/LineLength
3
3
  require_relative '../test_helper'
4
4
 
5
5
  module Excon
@@ -9,75 +9,81 @@ module Excon
9
9
  #
10
10
  class HypermediaTest < Minitest::Test
11
11
  def entrypoint
12
- <<~EOF
13
- {
14
- "_links": {
15
- "hello": {
16
- "href":"http://www.example.com/hello/{location}"
17
- }
18
- }
19
- }
20
- EOF
12
+ '{ "_links": { "hello": { "href":"http://www.example.com/hello/{location}" } } }'
21
13
  end
22
14
 
23
- def hello_world
15
+ def hello_world # rubocop:disable Metrics/MethodLength
24
16
  <<~EOF
25
17
  {
26
18
  "_links": {
27
19
  "goodbye": {
28
20
  "href":"http://www.example.com/hello/world/goodbye{?message}"
29
21
  }
30
- }
31
- }
32
- EOF
33
- end
34
-
35
- def hello_universe
36
- <<~EOF
37
- {
38
- "_links": {
39
- "goodbye": {
40
- "href":"http://www.example.com/hello/universe/goodbye{?message}"
41
- }
42
- }
22
+ },
23
+ "uid": "hello",
24
+ "message": "goodbye!"
43
25
  }
44
- EOF
26
+ EOF
45
27
  end
46
28
 
47
29
  def setup
48
30
  Excon.defaults[:mock] = true
31
+ Excon.defaults[:middlewares].push(Excon::HyperMedia::Middleware)
32
+
33
+ response = { headers: { 'Content-Type' => 'application/hal+json' } }
34
+ Excon.stub({ method: :get, path: '/api' }, response.merge(body: entrypoint))
35
+ Excon.stub({ method: :get, path: '/hello/world' }, response.merge(body: hello_world))
36
+ Excon.stub({ method: :get, path: '/hello/world/goodbye', query: nil }, response.merge(body: 'bye!'))
37
+ Excon.stub({ method: :get, path: '/hello/world/goodbye', query: 'message=farewell' }, response.merge(body: 'farewell'))
38
+ end
39
+
40
+ def client
41
+ Excon.get('http://www.example.com/api')
42
+ end
43
+
44
+ def test_request
45
+ response = client.hello(expand: { location: 'world' }).get
46
+
47
+ assert response.body.include?('http://www.example.com/hello/world/goodbye{?message}')
48
+ end
49
+
50
+ def test_nested_request
51
+ hello = client.hello(expand: { location: 'world' })
52
+ response = hello.get.goodbye.get
49
53
 
50
- Excon.stub({ method: :get, path: '/api' }, body: entrypoint, status: 200)
51
- Excon.stub({ method: :get, path: '/hello/world' }, body: hello_world, status: 200)
52
- Excon.stub({ method: :get, path: '/hello/world/goodbye', query: nil }, body: 'bye!', status: 200)
53
- Excon.stub({ method: :get, path: '/hello/world/goodbye', query: 'message=farewell' }, body: 'farewell', status: 200)
54
- Excon.stub({ method: :get, path: '/hello/universe' }, body: hello_universe, status: 200)
54
+ assert_equal 'bye!', response.body
55
55
  end
56
56
 
57
- def test_hypermedia_request
58
- conn = Excon.new('http://www.example.com/api', hypermedia: true)
59
- conn2 = conn.hello(expand: { location: 'world' })
60
- conn3 = conn.hello(expand: { location: 'universe' })
57
+ def test_nested_query_parameters
58
+ hello = client.hello(expand: { location: 'world' })
59
+ response = hello.get.goodbye(expand: { message: 'farewell' }).get
61
60
 
62
- assert_equal '/hello/world', conn2.data[:path]
63
- assert conn2.get.body.include?('http://www.example.com/hello/world/goodbye{?message}')
61
+ assert_equal 'farewell', response.body
62
+ end
63
+
64
+ def test_expand_in_get
65
+ response = client.hello.get(expand: { location: 'world' })
64
66
 
65
- assert_equal '/hello/universe', conn3.data[:path]
66
- assert conn3.get.body.include?('http://www.example.com/hello/universe/goodbye{?message}')
67
+ assert response.body.include?('http://www.example.com/hello/world/goodbye{?message}')
67
68
  end
68
69
 
69
- def test_nested_hypermedia_request
70
- conn = Excon.new('http://www.example.com/api', hypermedia: true)
71
- conn2 = conn.hello(expand: { location: 'world' }).goodbye
72
- conn3 = conn.hello(expand: { location: 'world' }).goodbye(expand: { message: 'farewell' })
70
+ def test_attribute
71
+ response = client.hello(expand: { location: 'world' }).get
72
+
73
+ assert_equal response.uid, 'hello'
74
+ assert_equal response.message, 'goodbye!'
75
+ end
76
+
77
+ def test_links
78
+ response = client.hello(expand: { location: 'world' }).get
79
+
80
+ assert_equal response.links.first.name, 'goodbye'
81
+ end
73
82
 
74
- assert_equal '/hello/world/goodbye', conn2.data[:path]
75
- assert_nil conn2.data[:query]
76
- assert_equal 'bye!', conn2.get.body
83
+ def test_attributes
84
+ response = client.hello(expand: { location: 'world' }).get
77
85
 
78
- assert_equal '/hello/world/goodbye', conn3.data[:path]
79
- assert_equal 'message=farewell', conn3.data[:query]
80
- assert_equal 'farewell', conn3.get.body
86
+ assert_equal response.attributes.uid, 'hello'
81
87
  end
82
88
 
83
89
  def teardown
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+ require_relative '../test_helper'
3
+
4
+ module Excon
5
+ # LinkTest
6
+ #
7
+ # Validate the workings of `Excon::HyperResource::Link`.
8
+ #
9
+ class LinkTest < Minitest::Test
10
+ def body # rubocop:disable Metrics/MethodLength
11
+ <<~EOF
12
+ {
13
+ "_links": {
14
+ "hello": {
15
+ "href": "http://www.example.com/hello/{location}"
16
+ }
17
+ },
18
+ "uid": "universe",
19
+ "hello": "world"
20
+ }
21
+ EOF
22
+ end
23
+
24
+ def data
25
+ JSON.parse(body)
26
+ end
27
+
28
+ def link
29
+ @link ||= Excon::HyperMedia::Link.new(name: 'hello', hash: data)
30
+ end
31
+
32
+ def invalid_link
33
+ @invalid_link ||= Excon::HyperMedia::Link.new(name: 'goodbye', hash: data)
34
+ end
35
+
36
+ def test_link
37
+ assert_equal link.name, 'hello'
38
+ end
39
+
40
+ def test_valid_link
41
+ assert link.valid?
42
+ end
43
+
44
+ def test_invalid_link
45
+ refute invalid_link.valid?
46
+ end
47
+
48
+ def test_uri
49
+ assert_equal link.uri.to_s, data['_links']['hello']['href']
50
+ end
51
+
52
+ def test_href
53
+ assert_equal link.href, data['_links']['hello']['href']
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+ require_relative '../test_helper'
3
+
4
+ module Excon
5
+ # ResourceTest
6
+ #
7
+ # Validate the workings of `Excon::HyperResource::Resource`.
8
+ #
9
+ class ResourceTest < Minitest::Test
10
+ def body # rubocop:disable Metrics/MethodLength
11
+ <<~EOF
12
+ {
13
+ "_links": {
14
+ "hello": {
15
+ "href": "http://www.example.com/hello/{location}"
16
+ }
17
+ },
18
+ "uid": "universe",
19
+ "hello": "world"
20
+ }
21
+ EOF
22
+ end
23
+
24
+ def data
25
+ @data ||= JSON.parse(body)
26
+ end
27
+
28
+ def resource
29
+ @resource ||= Excon::HyperMedia::Resource.new(body)
30
+ end
31
+
32
+ def test_resource
33
+ assert_equal data, resource.data
34
+ end
35
+
36
+ def test_links
37
+ assert_equal data['_links']['hello']['href'], resource.links.first.href
38
+ end
39
+
40
+ def test_attributes
41
+ assert_equal resource.attributes.uid, 'universe'
42
+ assert_equal resource.attributes.hello, 'world'
43
+
44
+ refute resource.attributes.respond_to?('_links')
45
+ end
46
+ end
47
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: excon-hypermedia
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jean
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2016-05-17 00:00:00.000000000 Z
12
+ date: 2016-05-18 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -81,6 +81,20 @@ dependencies:
81
81
  - - "~>"
82
82
  - !ruby/object:Gem::Version
83
83
  version: '0.10'
84
+ - !ruby/object:Gem::Dependency
85
+ name: m
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - "~>"
89
+ - !ruby/object:Gem::Version
90
+ version: 1.5.0
91
+ type: :development
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - "~>"
96
+ - !ruby/object:Gem::Version
97
+ version: 1.5.0
84
98
  - !ruby/object:Gem::Dependency
85
99
  name: excon
86
100
  requirement: !ruby/object:Gem::Requirement
@@ -126,9 +140,15 @@ files:
126
140
  - Rakefile
127
141
  - excon-hypermedia.gemspec
128
142
  - lib/excon/hypermedia.rb
129
- - lib/excon/hypermedia/hyper_media.rb
143
+ - lib/excon/hypermedia/ext/response.rb
144
+ - lib/excon/hypermedia/link.rb
145
+ - lib/excon/hypermedia/middleware.rb
146
+ - lib/excon/hypermedia/resource.rb
147
+ - lib/excon/hypermedia/response.rb
130
148
  - lib/excon/hypermedia/version.rb
131
149
  - test/excon/hypermedia_test.rb
150
+ - test/excon/link_test.rb
151
+ - test/excon/resource_test.rb
132
152
  - test/test_helper.rb
133
153
  homepage: https://github.com/JeanMertz/excon-hypermedia
134
154
  licenses:
@@ -155,4 +175,3 @@ signing_key:
155
175
  specification_version: 4
156
176
  summary: Excon, with Hypermedia traversing baked in.
157
177
  test_files: []
158
- has_rdoc:
@@ -1,26 +0,0 @@
1
- # frozen_string_literal: true
2
- require 'json'
3
-
4
- module Excon
5
- # HyperMedia
6
- #
7
- module HyperMedia
8
- def method_missing(method_name, *params)
9
- return super unless (url = entrypoint.dig('_links', method_name.to_s, 'href'))
10
-
11
- Excon.new(url, params.first.to_h.merge(hypermedia: true))
12
- end
13
-
14
- def respond_to_missing?(method_name, include_private = false)
15
- entrypoint.dig('_links', method_name.to_s, 'href') ? true : super
16
- end
17
-
18
- private
19
-
20
- def entrypoint
21
- @entrypoint ||= JSON.parse(get.body)
22
- rescue JSON::ParserError
23
- {}
24
- end
25
- end
26
- end