excon-hypermedia 0.3.0 → 0.4.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: 6c0a3189fb4185c1db3c29a86b699ea1de7549db
4
- data.tar.gz: b4d04af297a0ea267d0a2dce0765385bfd5d8483
3
+ metadata.gz: 38a442095c082e7251b1a72d8e88084833357a3d
4
+ data.tar.gz: f0ff7d16042fa3d35bfca84b216b6e145f55db2d
5
5
  SHA512:
6
- metadata.gz: f7576259b25677acc8f9b23171705babafff2bd0510c055723bee712e1f77a329355b090be9544631aabdd199c0e4c068f0818765ffcf1c4abdd5f03a94b004a
7
- data.tar.gz: eb29ba745ea6cf03434f3232fd459e7e9bf3db791fba56fc88772e95a10800510f54e5c793e8771df9b77bae240522fd024b305f8bbf5186236d7a0681c815fd
6
+ metadata.gz: 6ce27164e467d58bd426538b59cb917761c246d36dcb744d43e807169415ca225280cbd9965d41a631c907b2abeed49794090e3800a78e51a05b2f0811f781f1
7
+ data.tar.gz: 4670c0806fa665a012b0238598cc724296e5252b79735f116954aba80649b116773560d07aeebad932cbfd0f39550bd1a21248376d2ab25ccc6f49f1615fdf9e
data/README.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  Teaches [Excon][] how to talk to [HyperMedia APIs][hypermedia].
4
4
 
5
+ * [Installation](#installation)
6
+ * [Quick Start](#quick-start)
7
+ * [Usage](#usage)
8
+ * [resources](#resources)
9
+ * [links](#links)
10
+ * [relations](#relations)
11
+ * [properties](#properties)
12
+ * [embedded](#embedded)
13
+ * [License](#license)
14
+
5
15
  ## Installation
6
16
 
7
17
  Add this line to your application's Gemfile:
@@ -22,6 +32,22 @@ Or install it yourself as:
22
32
  gem install excon-hypermedia
23
33
  ```
24
34
 
35
+ ## Quick Start
36
+
37
+ ```ruby
38
+ Excon.defaults[:middlewares].push(Excon::HyperMedia::Middleware)
39
+
40
+ api = Excon.get('https://www.example.org/api.json')
41
+ api.class # => Excon::Response
42
+
43
+ product = api.rel('product', expand: { uid: 'bicycle' })
44
+ product.class # => Excon::Connection
45
+
46
+ response = product.get
47
+ response.class # => Excon::Response
48
+ response.resource.name # => 'bicycle'
49
+ ```
50
+
25
51
  ## Usage
26
52
 
27
53
  To let Excon know the API supports HyperMedia, simply enable the correct
@@ -34,65 +60,202 @@ api = Excon.get('https://www.example.org/api.json')
34
60
  api.class # => Excon::Response
35
61
  ```
36
62
 
37
- Using the `HyperMedia` middleware, the `Excon::Response` object now knows how
38
- to handle the HyperMedia aspect of the API:
63
+ > NOTE: we'll use the following JSON response body in the below examples:
64
+ >
65
+ > **https://www.example.org/api.json**
66
+ >
67
+ > ```json
68
+ > {
69
+ > "_links": {
70
+ > "self": {
71
+ > "href": "https://www.example.org/api.json"
72
+ > },
73
+ > "product": {
74
+ > "href": "https://www.example.org/product/{uid}",
75
+ > "templated": true
76
+ > }
77
+ > }
78
+ > }
79
+ > ```
80
+ >
81
+ > **https://www.example.org/product/bicycle**
82
+ >
83
+ > ```json
84
+ > {
85
+ > "_links": {
86
+ > "self": {
87
+ > "href": "https://www.example.org/product/bicycle"
88
+ > }
89
+ > },
90
+ > "bike-type": "Mountain Bike",
91
+ > "BMX": false,
92
+ > "derailleurs": {
93
+ > "back": 7,
94
+ > "front": 3
95
+ > },
96
+ > "name": "bicycle",
97
+ > "reflectors": true,
98
+ > "_embedded": {
99
+ > "pump": {
100
+ > "_links": {
101
+ > "self": "https://www.example.org/product/pump"
102
+ > },
103
+ > "weight": "2kg",
104
+ > "type": "Floor Pump",
105
+ > "valve-type": "Presta"
106
+ > }
107
+ > }
108
+ > }
109
+
110
+ With this middleware injected in the stack, Excon's model is now expanded with
111
+ several key concepts:
112
+
113
+ ### resources
114
+
115
+ A **resource** is the representation of the object returned by the API. Almost
116
+ all other concepts and methods originate from this object.
117
+
118
+ Use the newly available `Excon::Response#resource` method to access the resource
119
+ object:
39
120
 
40
121
  ```ruby
41
- product = api.product(expand: { uid: 'bicycle' })
42
- product.class # => Excon::Connection
122
+ api.resource.class # => Excon::HyperMedia::ResourceObject
123
+ ```
43
124
 
44
- response = product.get
45
- response.class # => Excon::Response
46
- response.body.class # => String
125
+ A resource has several methods exposed:
126
+
127
+ ```ruby
128
+ api.resource.public_methods(false) # => [:_links, :_properties, :_embedded]
129
+ ```
130
+
131
+ Each of these methods represents one of the following HyperMedia concepts.
132
+
133
+ ### links
134
+
135
+ A resource has links, that point to related resources (and itself), these can be
136
+ accessed as well:
137
+
138
+ ```ruby
139
+ api.resource._links.class # => Excon::HyperMedia::ResourceObject::Links
140
+ ```
141
+
142
+ You can get a list of valid links using `keys`:
143
+
144
+ ```ruby
145
+ api.resource._links.keys # => ['self', 'product']
146
+ ```
147
+
148
+ Each links is represented by a `LinkObject` instance:
149
+
150
+ ```ruby
151
+ api.resource._links.product.class # => Excon::HyperMedia::LinkObject
152
+ api.resource._links.product.href # => 'https://www.example.org/product/{uid}'
153
+ api.resource._links.product.templated # => true
154
+ ```
155
+
156
+ ### relations
157
+
158
+ Links are the primary way to traverse between relations. This is what makes a
159
+ HyperMedia-based API "self-discoverable".
160
+
161
+ To go from one resource, to the next, you use the `rel` (short for relation)
162
+ method. This method is available on any `LinkObject` instance.
163
+
164
+ Using `rel`, returns an `Excon::Connection` object, the same as if you where to
165
+ call `Excon.new`:
166
+
167
+ ```ruby
168
+ relation = api.resource._links.self.rel
169
+ relation.class # => Excon::Connection
170
+ ```
171
+
172
+ Since the returned object is of type `Excon::Connection`, all
173
+ [Excon-provided options][options] are available as well:
174
+
175
+ ```ruby
176
+ relation.get(idempotent: true, retry_limit: 6)
177
+ ```
178
+
179
+ `Excon::Response` also has a convenient delegation to `LinkObject#rel`:
180
+
181
+ ```ruby
182
+ relation = api.rel('self').get
183
+ ```
184
+
185
+ Once you call `get` (or `post`, or any other valid Excon request method), you
186
+ are back where you started, with a new `Excon::Response` object, imbued with
187
+ HyperMedia powers:
188
+
189
+ ```ruby
190
+ relation.resource._links.keys # => ['self', 'product']
191
+ ```
192
+
193
+ In this case, we ended up back with the same type of object as before. To go
194
+ anywhere meaningful, we want to use the `product` rel:
195
+
196
+ ```ruby
197
+ product = api.rel('product', expand: { uid: 'bicycle' }).get
47
198
  ```
48
199
 
49
200
  As seen above, you can expand URI Template variables using the `expand` option,
50
201
  provided by the [`excon-addressable` library][excon-addressable].
51
202
 
52
- Since each new resource is simply an `Excon::Response` object, accessed through
53
- the default `Excon::Connection` object, all [Excon-provided options][options]
54
- are available as well:
203
+ ### properties
204
+
205
+ Properties are what make a resource unique, they tell us more about the state of
206
+ the resource, they are the key/value pairs that define the resource.
207
+
208
+ In HAL/JSON terms, this is everything returned by the response body, excluding
209
+ the `_links` and `_embedded` sections:
55
210
 
56
211
  ```ruby
57
- product.get(idempotent: true, retry_limit: 6)
212
+ product.resource.name # => "bicycle"
213
+ product.resource.reflectors # => true
58
214
  ```
59
215
 
60
- ### Links
61
-
62
- You can access all links in a resource using the `links` method:
216
+ Nested properties are supported as well:
63
217
 
64
218
  ```ruby
65
- api.links.first.class # => Excon::HyperMedia::Link
66
- api.links.first.name # => 'product'
67
- api.links.first.href # => 'https://www.example.org/product/{uid}'
219
+ product.resource.derailleurs.class # => Excon::HyperMedia::ResourceObject::Properties
220
+ product.resource.derailleurs.front # => 3
221
+ product.resource.derailleurs.back # => 7
68
222
  ```
69
223
 
70
- You can also access a link directly, using its name:
224
+ Property names that aren't valid method names can always be accessed using the
225
+ hash notation:
71
226
 
72
227
  ```ruby
73
- api.link('product').href # => 'https://www.example.org/product/{uid}'
228
+ product.resource['bike-type'] # => 'Mountain Bike'
229
+ product.resource['BMX'] # => false
230
+ product.resource.bmx # => false
74
231
  ```
75
232
 
76
- If you want to access a relation through its link, but can't use the implicit
77
- naming, you can use the `rel` method:
233
+ Properties should be implicitly accessed on a resource, but are internally
234
+ accessed via the `_properties` method:
78
235
 
79
236
  ```ruby
80
- api.links.last.name # => 'customer-orders'
237
+ product.resource._properties.class # => Excon::HyperMedia::ResourceObject::Properties
238
+ ```
81
239
 
82
- # won't work, due to invalid ruby method name:
83
- api.customer-orders(expand: { sort: 'desc' }) # => NoMethodError: undefined method `customer'
240
+ The `Properties` object inherits its logics from `Enumerable`:
84
241
 
85
- # works
86
- api.rel('customer-orders', expand: { sort: 'desc' })
242
+ ```ruby
243
+ product.resource._properties.to_h.class # => Hash
244
+ product.resource._properties.first # => ['name', 'bicycle']
87
245
  ```
88
246
 
89
- ### Attributes
247
+ ### embedded
248
+
249
+ Embedded resources are resources that are available through link relations, but
250
+ embedded in the current resource for easier access.
251
+
252
+ For more information on this concept, see the [formal specification][_embedded].
90
253
 
91
- Attributes are available through the `attributes` method:
254
+ Embedded resources work the same as the top-level resource:
92
255
 
93
256
  ```ruby
94
- product.attributes.to_h # => { uid: 'bicycle', stock: 5 }
95
- product.attributes.uid # => 'bicycle'
257
+ product._embedded.pump.class # => Excon::HyperMedia::ResourceObject
258
+ product._embedded.pump.weight # => '2kg'
96
259
  ```
97
260
 
98
261
  ## License
@@ -103,3 +266,4 @@ The gem is available as open source under the terms of the [MIT License](http://
103
266
  [hypermedia]: https://en.wikipedia.org/wiki/HATEOAS
104
267
  [excon-addressable]: https://github.com/JeanMertz/excon-addressable
105
268
  [options]: https://github.com/excon/excon#options
269
+ [_embedded]: https://tools.ietf.org/html/draft-kelly-json-hal-08#section-4.1.2
@@ -9,6 +9,8 @@ module Excon
9
9
  # on top.
10
10
  #
11
11
  module Response
12
+ private
13
+
12
14
  def method_missing(method_name, *params)
13
15
  hypermedia_response.handle(method_name, *params) || super
14
16
  end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Excon
6
+ module HyperMedia
7
+ # Collection
8
+ #
9
+ # Given a `Hash`, provides dot-notation properties and other helper methods.
10
+ #
11
+ module Collection
12
+ include Enumerable
13
+
14
+ def initialize(collection = {})
15
+ @collection ||= collection
16
+ to_properties
17
+ end
18
+
19
+ def each(&block)
20
+ collection.each(&block)
21
+ end
22
+
23
+ def keys
24
+ @collection.keys
25
+ end
26
+
27
+ def key?(key)
28
+ collection.key?(key.to_s)
29
+ end
30
+
31
+ def [](key)
32
+ to_property(key)
33
+ end
34
+
35
+ private
36
+
37
+ def to_properties
38
+ collection.each do |key, value|
39
+ key = key.downcase
40
+ next unless /[@$"]/ !~ key.to_sym.inspect
41
+
42
+ singleton_class.class_eval { attr_reader key }
43
+ instance_variable_set("@#{key}", property(value))
44
+ end
45
+ end
46
+
47
+ def property(value)
48
+ value.respond_to?(:keys) ? self.class.new(value) : value
49
+ end
50
+
51
+ def to_property(key)
52
+ key?(key) ? property(collection[key]) : nil
53
+ end
54
+
55
+ def to_property!(key)
56
+ key?(key) ? to_property(key) : method_missing(key)
57
+ end
58
+
59
+ attr_reader :collection
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Excon
4
+ module HyperMedia
5
+ # Link
6
+ #
7
+ # Encapsulates a link pointing to a resource.
8
+ #
9
+ # @see https://tools.ietf.org/html/draft-kelly-json-hal-08#section-5
10
+ #
11
+ class LinkObject
12
+ include Collection
13
+
14
+ # href
15
+ #
16
+ # The "href" property is REQUIRED.
17
+ #
18
+ # Its value is either a URI [RFC3986] or a URI Template [RFC6570].
19
+ #
20
+ # If the value is a URI Template then the Link Object SHOULD have a
21
+ # "templated" attribute whose value is true.
22
+ #
23
+ # @see https://tools.ietf.org/html/draft-kelly-json-hal-08#section-5.1
24
+ #
25
+ def href
26
+ to_property!(:href)
27
+ end
28
+
29
+ # The "templated" property is OPTIONAL.
30
+ #
31
+ # Its value is boolean and SHOULD be true when the Link Object's "href"
32
+ # property is a URI Template.
33
+ #
34
+ # Its value SHOULD be considered false if it is undefined or any other
35
+ # value than true.
36
+ #
37
+ # @see: https://tools.ietf.org/html/draft-kelly-json-hal-08#section-5.2
38
+ #
39
+ def templated
40
+ to_property(__method__) || false
41
+ end
42
+
43
+ # type
44
+ #
45
+ # The "type" property is OPTIONAL.
46
+ #
47
+ # Its value is a string used as a hint to indicate the media type
48
+ # expected when dereferencing the target resource.
49
+ #
50
+ # @see: https://tools.ietf.org/html/draft-kelly-json-hal-08#section-5.3
51
+ #
52
+ def type
53
+ to_property!(__method__)
54
+ end
55
+
56
+ # deprecation
57
+ #
58
+ # The "deprecation" property is OPTIONAL.
59
+ #
60
+ # Its presence indicates that the link is to be deprecated (i.e.
61
+ # removed) at a future date. Its value is a URL that SHOULD provide
62
+ # further information about the deprecation.
63
+ #
64
+ # A client SHOULD provide some notification (for example, by logging a
65
+ # warning message) whenever it traverses over a link that has this
66
+ # property. The notification SHOULD include the deprecation property's
67
+ # value so that a client manitainer can easily find information about
68
+ # the deprecation.
69
+ #
70
+ # @see https://tools.ietf.org/html/draft-kelly-json-hal-08#section-5.4
71
+ #
72
+ def deprecation
73
+ to_property!(__method__)
74
+ end
75
+
76
+ # name
77
+ #
78
+ # The "name" property is OPTIONAL.
79
+ #
80
+ # Its value MAY be used as a secondary key for selecting Link Objects
81
+ # which share the same relation type.
82
+ #
83
+ # @see https://tools.ietf.org/html/draft-kelly-json-hal-08#section-5.5
84
+ #
85
+ def name
86
+ to_property!(__method__)
87
+ end
88
+
89
+ # profile
90
+ #
91
+ # The "profile" property is OPTIONAL.
92
+ #
93
+ # Its value is a string which is a URI that hints about the profile (as
94
+ # defined by [I-D.wilde-profile-link]) of the target resource.
95
+ #
96
+ # @see https://tools.ietf.org/html/draft-kelly-json-hal-08#section-5.6
97
+ #
98
+ def profile
99
+ to_property!(__method__)
100
+ end
101
+
102
+ # title
103
+ #
104
+ # The "title" property is OPTIONAL.
105
+ #
106
+ # Its value is a string and is intended for labelling the link with a
107
+ # human-readable identifier (as defined by [RFC5988]).
108
+ #
109
+ # @see https://tools.ietf.org/html/draft-kelly-json-hal-08#section-5.7
110
+ #
111
+ def title
112
+ to_property!(__method__)
113
+ end
114
+
115
+ # hreflang
116
+ #
117
+ # The "hreflang" property is OPTIONAL.
118
+ #
119
+ # Its value is a string and is intended for indicating the language of
120
+ # the target resource (as defined by [RFC5988]).
121
+ #
122
+ # @see https://tools.ietf.org/html/draft-kelly-json-hal-08#section-5.8
123
+ #
124
+ def hreflang
125
+ to_property!(__method__)
126
+ end
127
+
128
+ # uri
129
+ #
130
+ # Returns a URI representation of the provided "href" property.
131
+ #
132
+ # @return [URI] URI object of the "href" property
133
+ #
134
+ def uri
135
+ ::Addressable::URI.parse(href)
136
+ end
137
+
138
+ # rel
139
+ #
140
+ # Returns an `Excon::Connection` instance, based on the current link.
141
+ #
142
+ # @return [Excon::Connection] Connection object based on current link
143
+ #
144
+ def rel(params = {})
145
+ Excon.new(href, params)
146
+ end
147
+
148
+ private def property(value)
149
+ value
150
+ end
151
+ end
152
+ end
153
+ end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ Excon.defaults[:middlewares].delete(Excon::Addressable::Middleware)
3
4
  Excon.defaults[:middlewares].unshift(Excon::Addressable::Middleware)
4
5
 
5
6
  module Excon
@@ -16,6 +17,7 @@ module Excon
16
17
  def request_call(datum)
17
18
  return super unless (content_type = datum.dig(:response, :headers, 'Content-Type').to_s)
18
19
 
20
+ datum[:response] ||= {}
19
21
  datum[:response][:hypermedia] = if datum[:hypermedia].nil?
20
22
  content_type.include?('hal+json')
21
23
  else
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Excon
4
+ module HyperMedia
5
+ class ResourceObject
6
+ # Links
7
+ #
8
+ # Represents a collection of links part of a resource.
9
+ #
10
+ class Embedded
11
+ include Collection
12
+
13
+ private def property(value)
14
+ if value.respond_to?(:to_ary)
15
+ value.map { |v| ResourceObject.new(v) }
16
+ else
17
+ ResourceObject.new(value)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Excon
4
+ module HyperMedia
5
+ class ResourceObject
6
+ # Links
7
+ #
8
+ # Represents a collection of links part of a resource.
9
+ #
10
+ class Links
11
+ include Collection
12
+
13
+ private def property(value)
14
+ value.respond_to?(:to_ary) ? value.map { |v| LinkObject.new(v) } : LinkObject.new(value)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Excon
6
+ module HyperMedia
7
+ class ResourceObject
8
+ # Properties
9
+ #
10
+ # Provides consistent access to resource properties.
11
+ #
12
+ class Properties
13
+ include Collection
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'excon/hypermedia/resource_object/embedded'
4
+ require 'excon/hypermedia/resource_object/links'
5
+ require 'excon/hypermedia/resource_object/properties'
6
+
7
+ module Excon
8
+ module HyperMedia
9
+ # ResourceObject
10
+ #
11
+ # Represents a resource.
12
+ #
13
+ class ResourceObject
14
+ RESERVED_PROPERTIES = %w(_links _embedded).freeze
15
+
16
+ def initialize(data)
17
+ @data = data
18
+
19
+ _properties.each do |key, value|
20
+ key = key.downcase
21
+ next unless /[@$"]/ !~ key.to_sym.inspect
22
+
23
+ singleton_class.class_eval { attr_reader key }
24
+ instance_variable_set("@#{key}", value.respond_to?(:keys) ? Properties.new(value) : value)
25
+ end
26
+ end
27
+
28
+ def _properties
29
+ @_properties ||= Properties.new(@data.reject { |k, _| RESERVED_PROPERTIES.include?(k) })
30
+ end
31
+
32
+ # _links
33
+ #
34
+ # The reserved "_links" property is OPTIONAL.
35
+ #
36
+ # It is an object whose property names are link relation types (as
37
+ # defined by [RFC5988]) and values are either a Link Object or an array
38
+ # of Link Objects. The subject resource of these links is the Resource
39
+ # Object of which the containing "_links" object is a property.
40
+ #
41
+ # @see https://tools.ietf.org/html/draft-kelly-json-hal-08#section-4.1.1
42
+ #
43
+ def _links
44
+ @_links ||= Links.new(@data['_links'])
45
+ end
46
+
47
+ # _embedded
48
+ #
49
+ # The reserved "_embedded" property is OPTIONAL
50
+ #
51
+ # It is an object whose property names are link relation types (as
52
+ # defined by [RFC5988]) and values are either a Resource Object or an
53
+ # array of Resource Objects.
54
+ #
55
+ # Embedded Resources MAY be a full, partial, or inconsistent version of
56
+ # the representation served from the target URI.
57
+ #
58
+ # @see https://tools.ietf.org/html/draft-kelly-json-hal-08#section-4.1.2
59
+ #
60
+ def _embedded
61
+ @_embedded ||= Embedded.new(@data['_embedded'])
62
+ end
63
+
64
+ def [](key)
65
+ _properties[key]
66
+ end
67
+ end
68
+ end
69
+ end