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.
@@ -21,14 +21,10 @@ module Excon
21
21
  def handle(method_name, *params)
22
22
  return false unless enabled?
23
23
 
24
- if method_name == :rel
25
- handle_link(params.shift, params)
26
- elsif resource.type?(method_name) == :link
27
- handle_link(method_name, params)
28
- elsif resource.respond_to?(method_name, false)
29
- resource.send(method_name, *params)
30
- else
31
- false
24
+ case method_name
25
+ when :resource then resource
26
+ when :rel then rel(params.shift, params)
27
+ else false
32
28
  end
33
29
  end
34
30
 
@@ -37,15 +33,26 @@ module Excon
37
33
  attr_reader :response
38
34
 
39
35
  def resource
40
- @resource ||= Resource.new(response.body)
36
+ @resource ||= ResourceObject.new(body_to_hash)
37
+ end
38
+
39
+ def body_to_hash
40
+ content_type.include?('application/hal+json') ? JSON.parse(response.body) : {}
41
+ end
42
+
43
+ def content_type
44
+ response.headers['Content-Type'].to_s
41
45
  end
42
46
 
43
47
  def enabled?
44
48
  response.data[:hypermedia] == true
45
49
  end
46
50
 
47
- def handle_link(name, params)
48
- Excon.new(resource.link(name).href, params.first.to_h.merge(hypermedia: true))
51
+ def rel(name, params)
52
+ link = resource._links.send(name)
53
+ options = params.first.to_h.merge(hypermedia: true)
54
+
55
+ link.respond_to?(:to_ary) ? link.map { |l| l.rel(options) } : link.rel(options)
49
56
  end
50
57
  end
51
58
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Excon
4
4
  module HyperMedia
5
- VERSION = '0.3.0'
5
+ VERSION = '0.4.0'
6
6
  end
7
7
  end
@@ -2,8 +2,10 @@
2
2
 
3
3
  require 'excon'
4
4
  require 'excon/addressable'
5
- require 'excon/hypermedia/link'
5
+
6
+ require 'excon/hypermedia/helpers/collection'
7
+ require 'excon/hypermedia/link_object'
6
8
  require 'excon/hypermedia/middleware'
7
- require 'excon/hypermedia/resource'
9
+ require 'excon/hypermedia/resource_object'
8
10
  require 'excon/hypermedia/response'
9
11
  require 'excon/hypermedia/version'
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+ # rubocop:disable Metrics/AbcSize
3
+ # rubocop:disable Metrics/LineLength
4
+
5
+ require_relative '../test_helper'
6
+
7
+ module Excon
8
+ # IntegrationTest
9
+ #
10
+ # Verifies the Excon connection consuming HyperMedia APIs.
11
+ #
12
+ class IntegrationTest < HyperMediaTest
13
+ def api
14
+ Excon.get('https://www.example.org/api.json')
15
+ end
16
+
17
+ def test_request
18
+ response = api.rel('product', expand: { uid: 'bicycle' }).get
19
+
20
+ assert response.body.include?('https://www.example.org/product/bicycle')
21
+ end
22
+
23
+ def test_request_using_link_rel
24
+ response = api.resource._links.product.rel(expand: { uid: 'bicycle' }).get
25
+
26
+ assert response.body.include?('https://www.example.org/product/bicycle')
27
+ end
28
+
29
+ def test_nested_request
30
+ bicycle = api.rel('product', expand: { uid: 'bicycle' }).get
31
+ response = bicycle.rel('handlebar').get
32
+
33
+ assert_equal data(:handlebar)['material'], response.resource.material
34
+ end
35
+
36
+ def test_collection_request
37
+ bicycle = api.rel('product', expand: { uid: 'bicycle' }).get
38
+ wheels = bicycle.rel('wheels')
39
+ responses = wheels.map(&:get)
40
+
41
+ assert Array, wheels.class
42
+ assert_equal data(:front_wheel)['position'], responses.first.resource.position
43
+ end
44
+
45
+ def test_expand_in_get
46
+ response = api.rel('product').get(expand: { uid: 'bicycle' })
47
+
48
+ assert response.body.include?('https://www.example.org/product/bicycle')
49
+ end
50
+
51
+ def test_invalid_relation
52
+ assert_raises(NoMethodError) { api.rel('invalid') }
53
+ end
54
+
55
+ def test_link
56
+ response = api.rel('product', expand: { uid: 'bicycle' }).get
57
+
58
+ assert_equal data(:bicycle)['_links']['handlebar']['href'], response.resource._links.handlebar.href
59
+ end
60
+
61
+ def test_link_collection
62
+ response = api.rel('product', expand: { uid: 'bicycle' }).get
63
+
64
+ assert_equal Array, response.resource._links.wheels.class
65
+ assert_equal data(:bicycle)['_links']['wheels'][0]['href'], response.resource._links.wheels.first.href
66
+ end
67
+
68
+ def test_nested_attributes
69
+ response = api.rel('product', expand: { uid: 'bicycle' }).get
70
+
71
+ assert_equal 7, response.resource.derailleurs.back
72
+ end
73
+
74
+ def test_invalid_attribute
75
+ response = api.rel('product', expand: { uid: 'bicycle' }).get
76
+
77
+ assert_equal 'Mountain Bike', response.resource['bike-type']
78
+ assert_equal false, response.resource.bmx
79
+ end
80
+
81
+ def test_embedded_resource
82
+ response = api.rel('product', expand: { uid: 'bicycle' }).get
83
+
84
+ assert_equal Excon::HyperMedia::ResourceObject, response.resource._embedded.pump.class
85
+ assert_equal '2kg', response.resource._embedded.pump.weight
86
+ end
87
+
88
+ def test_embedded_resource_collection
89
+ response = api.rel('product', expand: { uid: 'bicycle' }).get
90
+
91
+ assert_equal Array, response.resource._embedded.wheels.class
92
+ assert_equal data(:front_wheel)['position'], response.resource._embedded.wheels.first.position
93
+ end
94
+
95
+ def teardown
96
+ Excon.stubs.clear
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+ # rubocop:disable Metrics/MethodLength
3
+
4
+ require_relative '../test_helper'
5
+
6
+ module Excon
7
+ # LinkTest
8
+ #
9
+ # Validate the workings of `Excon::HyperResource::LinkObject`.
10
+ #
11
+ class LinkTest < Minitest::Test
12
+ def self
13
+ '{ "href": "https://www.example.org/hello" }'
14
+ end
15
+
16
+ def templated
17
+ '{ "href": "https://www.example.org/hello/{receiver}", "templated": "true" }'
18
+ end
19
+
20
+ def full
21
+ <<~EOF
22
+ {
23
+ "href": "https://www.example.org/goodbye/{receiver}",
24
+ "templated": "true",
25
+ "type": "json",
26
+ "deprecation": true,
27
+ "name": "goodbye",
28
+ "profile": "https://profile.example.org",
29
+ "title": "Goodbye!",
30
+ "hreflang": "en-gb"
31
+ }
32
+ EOF
33
+ end
34
+
35
+ def data(name)
36
+ JSON.parse(send(name))
37
+ end
38
+
39
+ def link(name)
40
+ Excon::HyperMedia::LinkObject.new(data(name))
41
+ end
42
+
43
+ def test_link
44
+ assert_equal data(:self), link(:self).to_h
45
+ end
46
+
47
+ def test_missing_property
48
+ assert_raises(NoMethodError) { data(:self).name }
49
+ end
50
+
51
+ def test_href
52
+ assert_equal data(:self)['href'], link(:self).href
53
+ end
54
+
55
+ def test_templated
56
+ assert link(:templated).templated
57
+ end
58
+
59
+ def test_templated_returns_false_if_undefined
60
+ refute link(:self).templated
61
+ end
62
+
63
+ def test_type
64
+ assert_equal data(:full)['type'], link(:full).type
65
+ end
66
+
67
+ def test_deprecation
68
+ assert_equal data(:full)['deprecation'], link(:full).deprecation
69
+ end
70
+
71
+ def test_name
72
+ assert_equal data(:full)['name'], link(:full).name
73
+ end
74
+
75
+ def test_profile
76
+ assert_equal data(:full)['profile'], link(:full).profile
77
+ end
78
+
79
+ def test_title
80
+ assert_equal data(:full)['title'], link(:full).title
81
+ end
82
+
83
+ def test_hreflang
84
+ assert_equal data(:full)['hreflang'], link(:full).hreflang
85
+ end
86
+
87
+ def test_uri
88
+ assert_equal data(:self)['href'], link(:self).uri.to_s
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+ # rubocop:disable Metrics/MethodLength
3
+
4
+ require_relative '../test_helper'
5
+
6
+ module Excon
7
+ # LinksTest
8
+ #
9
+ # Validate the workings of `Excon::HyperResource::Resource::Links`.
10
+ #
11
+ class LinksTest < Minitest::Test
12
+ def body
13
+ <<~EOF
14
+ {
15
+ "_links": {
16
+ "self": {
17
+ "href": "https://www.example.org/product/bicycle"
18
+ },
19
+ "parts": {
20
+ "href": "https://www.example.org/product/bicycle/parts"
21
+ }
22
+ }
23
+ }
24
+ EOF
25
+ end
26
+
27
+ def data
28
+ JSON.parse(body)
29
+ end
30
+
31
+ def links
32
+ @links ||= Excon::HyperMedia::ResourceObject::Links.new(data['_links'])
33
+ end
34
+
35
+ def test_links
36
+ assert_equal Excon::HyperMedia::ResourceObject::Links, links.class
37
+ end
38
+
39
+ def test_link_properties
40
+ assert_equal %w(self parts), links.to_h.keys
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+ # rubocop:disable Metrics/MethodLength
3
+
4
+ require_relative '../test_helper'
5
+
6
+ module Excon
7
+ # PropertiesTest
8
+ #
9
+ # Validate the workings of `Excon::HyperResource::Resource::Properties`.
10
+ #
11
+ class PropertiesTest < Minitest::Test
12
+ def body
13
+ <<~EOF
14
+ {
15
+ "size": "49CM",
16
+ "bike-type": "Mountain Bike",
17
+ "derailleurs": {
18
+ "front": 3,
19
+ "back": 7
20
+ },
21
+ "reflectors": true,
22
+ "BMX": false
23
+ }
24
+ EOF
25
+ end
26
+
27
+ def data
28
+ JSON.parse(body)
29
+ end
30
+
31
+ def properties
32
+ @properties ||= Excon::HyperMedia::ResourceObject::Properties.new(data)
33
+ end
34
+
35
+ def test_properties
36
+ assert_equal data, properties.to_h
37
+ end
38
+
39
+ def test_attribute
40
+ assert_equal properties.size, '49CM'
41
+ end
42
+
43
+ def test_boolean_attribute
44
+ assert_equal properties.reflectors, true
45
+ end
46
+
47
+ def test_uppercase_attribute_names
48
+ assert_equal properties.bmx, false
49
+ assert_equal properties['BMX'], false
50
+ end
51
+
52
+ def test_invalid_attribute_names
53
+ refute properties.respond_to?('bike-type')
54
+ assert_equal properties['bike-type'], 'Mountain Bike'
55
+ end
56
+
57
+ def test_nested_attribute
58
+ assert_equal properties.derailleurs.front, 3
59
+ assert_equal properties.derailleurs.back, 7
60
+ assert_equal properties['derailleurs'].back, 7
61
+ end
62
+
63
+ def test_nested_attribute_hash
64
+ assert_equal properties.derailleurs.to_h, data['derailleurs']
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+ # rubocop:disable Metrics/MethodLength
3
+
4
+ require_relative '../test_helper'
5
+
6
+ module Excon
7
+ # ResourceObjectTest
8
+ #
9
+ # Validate the workings of `Excon::HyperResource::ResourceObject`.
10
+ #
11
+ class ResourceObjectTest < Minitest::Test
12
+ def body
13
+ <<~EOF
14
+ {
15
+ "_links": {
16
+ "hello": {
17
+ "href": "http://www.example.com/hello/{location}"
18
+ }
19
+ },
20
+ "uid": "universe",
21
+ "hello": "world"
22
+ }
23
+ EOF
24
+ end
25
+
26
+ def data
27
+ @data ||= JSON.parse(body)
28
+ end
29
+
30
+ def resource
31
+ @resource ||= Excon::HyperMedia::ResourceObject.new(data)
32
+ end
33
+
34
+ def test_resource
35
+ assert_equal data, resource.instance_variable_get(:@data)
36
+ end
37
+
38
+ def test_links
39
+ assert_equal data['_links']['hello']['href'], resource._links.hello.href
40
+ end
41
+
42
+ def test_properties
43
+ assert_equal 'universe', resource.uid
44
+ assert_equal 'world', resource.hello
45
+ assert_equal 'world', resource['hello']
46
+ assert_equal nil, resource['invalid']
47
+ end
48
+ end
49
+ end
data/test/test_helper.rb CHANGED
@@ -1,4 +1,145 @@
1
1
  # frozen_string_literal: true
2
+ # rubocop:disable Metrics/MethodLength
3
+ # rubocop:disable Metrics/LineLength
4
+ # rubocop:disable Metrics/ClassLength
5
+ # rubocop:disable Metrics/AbcSize
6
+
2
7
  $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
3
8
  require 'excon/hypermedia'
4
9
  require 'minitest/autorun'
10
+
11
+ module Excon
12
+ # HyperMediaTest
13
+ #
14
+ class HyperMediaTest < Minitest::Test
15
+ def setup
16
+ Excon.defaults[:mock] = true
17
+ Excon.defaults[:middlewares].push(Excon::HyperMedia::Middleware)
18
+
19
+ response = { headers: { 'Content-Type' => 'application/hal+json' } }
20
+ Excon.stub({ method: :get, path: '/api.json' }, response.merge(body: api_body))
21
+ Excon.stub({ method: :get, path: '/product/bicycle' }, response.merge(body: bicycle_body))
22
+ Excon.stub({ method: :get, path: '/product/bicycle/wheels/front' }, response.merge(body: front_wheel_body))
23
+ Excon.stub({ method: :get, path: '/product/bicycle/wheels/rear' }, response.merge(body: rear_wheel_body))
24
+ Excon.stub({ method: :get, path: '/product/pump' }, response.merge(body: pump_body))
25
+ Excon.stub({ method: :get, path: '/product/handlebar' }, response.merge(body: handlebar_body))
26
+ end
27
+
28
+ def teardown
29
+ Excon.stubs.clear
30
+ Excon.defaults[:middlewares].delete(Excon::HyperMedia::Middleware)
31
+ Excon.defaults[:mock] = true
32
+ end
33
+
34
+ def data(name)
35
+ JSON.parse(send("#{name}_body"))
36
+ end
37
+
38
+ def api_body
39
+ <<~EOF
40
+ {
41
+ "_links": {
42
+ "self": {
43
+ "href": "https://www.example.org/api.json"
44
+ },
45
+ "product": {
46
+ "href": "https://www.example.org/product/{uid}",
47
+ "templated": true
48
+ }
49
+ }
50
+ }
51
+ EOF
52
+ end
53
+
54
+ def bicycle_body
55
+ <<~EOF
56
+ {
57
+ "_links": {
58
+ "self": {
59
+ "href": "https://www.example.org/product/bicycle"
60
+ },
61
+ "handlebar": {
62
+ "href": "https://www.example.org/product/handlebar"
63
+ },
64
+ "object_id": {
65
+ "href": "https://www.example.org/product/bicycle/object_id_as_text"
66
+ },
67
+ "pump": {
68
+ "href": "https://www.example.org/product/pump"
69
+ },
70
+ "wheels": [
71
+ { "href": "https://www.example.org/product/bicycle/wheels/front" },
72
+ { "href": "https://www.example.org/product/bicycle/wheels/rear" }
73
+ ]
74
+ },
75
+ "bike-type": "Mountain Bike",
76
+ "BMX": false,
77
+ "derailleurs": {
78
+ "back": 7,
79
+ "front": 3
80
+ },
81
+ "name": "bicycle",
82
+ "reflectors": true,
83
+ "_embedded": {
84
+ "pump": #{pump_body},
85
+ "wheels": [#{front_wheel_body}, #{rear_wheel_body}]
86
+ }
87
+ }
88
+ EOF
89
+ end
90
+
91
+ def handlebar_body
92
+ <<~EOF
93
+ {
94
+ "_links": {
95
+ "self": "https://www.example.org/product/handlebar"
96
+ },
97
+ "material": "Carbon fiber",
98
+ "reach": "75mm",
99
+ "bend": "compact"
100
+ }
101
+ EOF
102
+ end
103
+
104
+ def pump_body
105
+ <<~EOF
106
+ {
107
+ "_links": {
108
+ "self": "https://www.example.org/product/pump"
109
+ },
110
+ "weight": "2kg",
111
+ "type": "Floor Pump",
112
+ "valve-type": "Presta"
113
+ }
114
+ EOF
115
+ end
116
+
117
+ def rear_wheel_body
118
+ <<~EOF
119
+ {
120
+ "_links": {
121
+ "self": {
122
+ "href": "https://www.example.org/product/bicycle/wheels/rear"
123
+ }
124
+ },
125
+ "position": "rear",
126
+ "lacing_pattern": "Radial"
127
+ }
128
+ EOF
129
+ end
130
+
131
+ def front_wheel_body
132
+ <<~EOF
133
+ {
134
+ "_links": {
135
+ "self": {
136
+ "href": "https://www.example.org/product/bicycle/wheels/front"
137
+ }
138
+ },
139
+ "position": "front",
140
+ "lacing_pattern": "Radial/2-Cross"
141
+ }
142
+ EOF
143
+ end
144
+ end
145
+ 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.3.0
4
+ version: 0.4.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-18 00:00:00.000000000 Z
12
+ date: 2016-05-20 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -141,14 +141,20 @@ files:
141
141
  - excon-hypermedia.gemspec
142
142
  - lib/excon/hypermedia.rb
143
143
  - lib/excon/hypermedia/ext/response.rb
144
- - lib/excon/hypermedia/link.rb
144
+ - lib/excon/hypermedia/helpers/collection.rb
145
+ - lib/excon/hypermedia/link_object.rb
145
146
  - lib/excon/hypermedia/middleware.rb
146
- - lib/excon/hypermedia/resource.rb
147
+ - lib/excon/hypermedia/resource_object.rb
148
+ - lib/excon/hypermedia/resource_object/embedded.rb
149
+ - lib/excon/hypermedia/resource_object/links.rb
150
+ - lib/excon/hypermedia/resource_object/properties.rb
147
151
  - lib/excon/hypermedia/response.rb
148
152
  - lib/excon/hypermedia/version.rb
149
- - test/excon/hypermedia_test.rb
150
- - test/excon/link_test.rb
151
- - test/excon/resource_test.rb
153
+ - test/excon/integration_test.rb
154
+ - test/excon/link_object_test.rb
155
+ - test/excon/links_test.rb
156
+ - test/excon/properties_test.rb
157
+ - test/excon/resource_object_test.rb
152
158
  - test/test_helper.rb
153
159
  homepage: https://github.com/JeanMertz/excon-hypermedia
154
160
  licenses:
@@ -1,40 +0,0 @@
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
@@ -1,49 +0,0 @@
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