hal-client 4.2.0 → 4.3.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: 8007fc0fdf2984a6cdc35d90dfbdc73a020686d4
4
- data.tar.gz: a9b99d9d95c268e8be81324cf98ea76427f72c8e
3
+ metadata.gz: 77b2bd2a2ee75f106ff5dfc1ca7af3bb49c53ac7
4
+ data.tar.gz: 5e2abf209a2e898da64afb88f0118a58a61a4c47
5
5
  SHA512:
6
- metadata.gz: bc49c5224b0824b7d5da339467028ce9cb0b61162d3b9d4a18303e3af37fe493d832afa1cfbc65490647cd3c9441580059d5bf43b6247fa10479624685135cd6
7
- data.tar.gz: 5eb75a205939f44e074c55ad18d55275c4d9672287e2973b28557e965241fcd562af9611874cddb1c71cdbff2f13b5756e4f3416c36ac7d15661612a7c7247f7
6
+ metadata.gz: 8bbbb2faa0fcc3cc2f072b9fff4e0acf582509cd554cd0884aaeaa95ddcba4a81fc800970139a37a66550189f7b07eee9d5c4a80a95a301d9fa26b358fa02725
7
+ data.tar.gz: 25982fec801a367a71d163ce0830e7cbaae826531a1d4d56505d2464a43c3264fb7865768adf740e0aeab7ad05363c5774c838fa1b9ca5d039614d99fdbbe8f1
data/README.md CHANGED
@@ -136,6 +136,21 @@ doc.put(improved_doc) # save c
136
136
 
137
137
  This removes the obsolete link to "John Doe" from the documents list of authors and replaces it with the correct link then performs an HTTP PUT request with the updated document.
138
138
 
139
+ ### Forms
140
+
141
+ HalClient supports [Dwolla HAL forms](https://github.com/Dwolla/hal-forms). For example, given a collection with a [`create-form`](https://tools.ietf.org/html/rfc6861#section-3.1) link to a resource with a [`default`](https://github.com/Dwolla/hal-forms#properties) form that creates new members of the collection, the following code would create a new member of `http://example.com/somecollection`.
142
+
143
+
144
+ ```ruby
145
+ collection = HalClient.get("http://example.com/somecollection")
146
+ create_form = collection.related("create-form").form
147
+ create_form.submit(
148
+ name: "my item",
149
+ author_url: URI("http://example.com/john-doe")
150
+ description: "super duper!"
151
+ )
152
+
153
+ ```
139
154
  ### Custom media types
140
155
 
141
156
  If the API uses one or more a custom mime types we can specify that they be included in the `Accept` header field of each request.
data/hal-client.gemspec CHANGED
@@ -21,6 +21,7 @@ Gem::Specification.new do |spec|
21
21
  spec.add_dependency "http", ">= 0.8.4", "< 3.0"
22
22
  spec.add_dependency "addressable", "~> 2.3"
23
23
  spec.add_dependency "multi_json", "~> 1.9"
24
+ spec.add_dependency "hana", "~> 1.3"
24
25
 
25
26
  spec.add_development_dependency "bundler", "~> 1.5"
26
27
  spec.add_development_dependency "rake", "~> 10.1"
@@ -0,0 +1,67 @@
1
+ class HalClient
2
+ class Form
3
+ # A single field in a form.
4
+ #
5
+ # Current implementation is very basic. It only understands
6
+ # `hidden` and `string` field types. All other field types are
7
+ # treated as `string` per the spec.
8
+ class Field
9
+
10
+ # Initializes a new field.
11
+ #
12
+ # parsed_json - the parsed JSON of the field
13
+ def initialize(parsed_json)
14
+ @aliases = extract_aliases(parsed_json)
15
+ @value = extract_value(parsed_json)
16
+ @type = extract_type(parsed_json)
17
+ @path = extract_path(parsed_json)
18
+ end
19
+
20
+ # Returns the path to which this field should be encoded in JSON documents, if any.
21
+ attr_reader :path
22
+
23
+ def extract_answer(answers)
24
+ return value if :hidden == type
25
+
26
+ key = aliases.find{|maybe_key| answers.has_key?(maybe_key) }
27
+
28
+ coerce_value(answers.fetch(key, value))
29
+ end
30
+
31
+ protected
32
+
33
+ attr_reader :aliases, :value, :type
34
+
35
+ def coerce_value(val)
36
+ return nil if val.nil?
37
+
38
+ val.to_s
39
+ end
40
+
41
+ def extract_aliases(parsed_json)
42
+ name = parsed_json.fetch("name") {
43
+ raise ArgumentError, "field doesn't have a name"
44
+ }
45
+
46
+ [name, name.to_sym]
47
+ end
48
+
49
+ def extract_value(parsed_json)
50
+ parsed_json.fetch("value", nil)
51
+ end
52
+
53
+ def extract_type(parsed_json)
54
+ case parsed_json["type"]
55
+ when /hidden/i
56
+ :hidden
57
+ else
58
+ :string
59
+ end
60
+ end
61
+
62
+ def extract_path(parsed_json)
63
+ parsed_json["path"]
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,108 @@
1
+ require 'hal_client/form/field'
2
+ require 'hana'
3
+
4
+ class HalClient
5
+
6
+ # A single [Dwolla HAL
7
+ # form](https://github.com/Dwolla/hal-forms). Instances of this
8
+ # class allow clients to complete and submit individual forms.
9
+ #
10
+ # This is an incomplete implementation of the spec. Thus far it
11
+ # supports string and hidden fields with JSON encoding. Enhancements
12
+ # are requested.
13
+ class Form
14
+
15
+ # Initializes a newly created form
16
+ #
17
+ # parsed_json - a Hash create by parsing the JSON.
18
+ # hal_client - the `HalClient` with which to submit the new form.
19
+ def initialize(parsed_json, hal_client)
20
+ @hal_client = hal_client
21
+ @target_tmpl = extract_target_tmpl(parsed_json)
22
+ @method = extract_method(parsed_json)
23
+ @content_type = extract_content_type(parsed_json)
24
+ @fields = extract_fields(parsed_json)
25
+ end
26
+
27
+
28
+ # Returns the `Addressable::URI` to which this form is targeted
29
+ def target_url(answers={})
30
+ target_tmpl.expand(answers)
31
+ end
32
+
33
+ # Returns the `HalClient::Representation` returned from submitting
34
+ # the form.
35
+ #
36
+ # answers - `Hash` containing the answer key to submit. Keys are
37
+ # the field names; values are the values to submit.
38
+ def submit(answers={})
39
+ if :get == method
40
+ hal_client.get(target_url(answers))
41
+ else
42
+ hal_client.public_send(method, target_url(answers), body(answers), "Content-Type" => content_type)
43
+ end
44
+ end
45
+
46
+ protected
47
+
48
+ attr_reader :target_tmpl, :method, :hal_client, :content_type, :fields
49
+
50
+ def extract_target_tmpl(parsed_json)
51
+ tmpl_str = parsed_json
52
+ .fetch("_links")
53
+ .fetch("target")
54
+ .fetch("href")
55
+
56
+ Addressable::Template.new(tmpl_str)
57
+
58
+ rescue KeyError
59
+ raise ArgumentError, "form has no target href"
60
+ end
61
+
62
+ def extract_method(parsed_json)
63
+ parsed_json
64
+ .fetch("method")
65
+ .downcase
66
+ .to_sym
67
+
68
+ rescue KeyError
69
+ raise ArgumentError, "form doesn't specify a method"
70
+ end
71
+
72
+ def extract_content_type(parsed_json)
73
+ return nil if :get == method
74
+
75
+ parsed_json
76
+ .fetch("contentType") { raise ArgumentError, "form doesn't specify a content type" }
77
+ end
78
+
79
+ def extract_fields(parsed_json)
80
+ parsed_json
81
+ .fetch("fields") { raise ArgumentError, "form doesn't have a field member" }
82
+ .map { |field_json| Field.new(field_json) }
83
+ end
84
+
85
+ def body(answers)
86
+ case content_type
87
+ when /json$/i
88
+ build_json_body(answers)
89
+ else
90
+ raise NotImplementedError, "#{content_type} is not a supported content type"
91
+ end
92
+ end
93
+
94
+ def build_json_body(answers)
95
+ fields.reduce({}) { |body_thus_far, field|
96
+ json_inject_answer(body_thus_far, field.extract_answer(answers), field.path)
97
+ }
98
+ end
99
+
100
+ def json_inject_answer(body, answer, path)
101
+ patch = Hana::Patch.new [
102
+ { 'op' => 'add', 'path' => path, 'value' => answer }
103
+ ]
104
+
105
+ patch.apply(body)
106
+ end
107
+ end
108
+ end
@@ -5,6 +5,7 @@ require 'hal_client'
5
5
  require 'hal_client/representation_set'
6
6
  require 'hal_client/interpreter'
7
7
  require 'hal_client/anonymous_resource_locator'
8
+ require 'hal_client/form'
8
9
 
9
10
  class HalClient
10
11
  # HAL representation of a single resource. Provides access to
@@ -262,6 +263,18 @@ class HalClient
262
263
  }
263
264
  end
264
265
 
266
+ # Returns the specified `Form`
267
+ #
268
+ # form_id - the string or symbol id of the form of interest. Default: `"default"`
269
+ #
270
+ # Raises `KeyError` if the specified form doesn't exist.
271
+ def form(form_id="default")
272
+ parsed_form_json = property("_forms").fetch(form_id.to_s)
273
+
274
+ Form.new(parsed_form_json, hal_client)
275
+ end
276
+
277
+
265
278
  # Resets this representation such that it will be requested from
266
279
  # the upstream on it's next use.
267
280
  def reset
@@ -72,6 +72,27 @@ class HalClient
72
72
  first.patch(data, options)
73
73
  end
74
74
 
75
+
76
+ # Returns the specified `Form`
77
+ #
78
+ # form_id - the string or symbol id of the form of interest. Default: `"default"`
79
+ #
80
+ # Raises `KeyError` if the specified form doesn't exist, or if there are duplicates.
81
+ def form(form_id="default")
82
+ self
83
+ .map { |r|
84
+ begin
85
+ r.form(form_id)
86
+ rescue KeyError
87
+ nil
88
+ end }
89
+ .compact
90
+ .tap do |fs|
91
+ raise KeyError, "Duplicate `#{form_id}` forms exist" if fs.count > 1
92
+ end
93
+ .first
94
+ end
95
+
75
96
  protected
76
97
 
77
98
  attr_reader :reprs
@@ -1,3 +1,3 @@
1
1
  class HalClient
2
- VERSION = "4.2.0"
2
+ VERSION = "4.3.0"
3
3
  end
@@ -0,0 +1,151 @@
1
+ require "hal_client/form/field"
2
+
3
+ RSpec.describe HalClient::Form::Field do
4
+ describe ".new" do
5
+ it "works for simplest legal form field" do
6
+ expect(
7
+ described_class.new({"name" => "rating", "type" => "string"})
8
+ ).to behave_like_a described_class
9
+ end
10
+
11
+ it "works for complex field" do
12
+ expect(
13
+ described_class.new({ "name" => "rating",
14
+ "type" => "string",
15
+ "path" => "/foo/bar",
16
+ "value" => "ok",
17
+ "displayText" => "Rating",
18
+ "validations" => {
19
+ "required" => true,
20
+ "regex" => "(good)|(ok)|(bad)"
21
+ },
22
+ "multiple" => true,
23
+ "accepted" => {
24
+ "groupedValues" => [
25
+ { "key" => "group1",
26
+ "displayText" => "Group 1",
27
+ "values" => [
28
+ { "value" => "val1id",
29
+ "key" => "val1",
30
+ "displayText" => "Value 1"
31
+ }
32
+ ]
33
+ }
34
+ ]
35
+ }
36
+ })
37
+ ).to behave_like_a described_class
38
+ end
39
+ end
40
+
41
+ describe "#extract_answer(answers)" do
42
+ context "no default value" do
43
+ subject { described_class.new(string_field_json) }
44
+
45
+ it "handles string answer keys" do
46
+ expect(
47
+ subject.extract_answer("rating" => "bad")
48
+ ).to eq "bad"
49
+ end
50
+
51
+ it "coerces non-string answers" do
52
+ expect(
53
+ subject.extract_answer("rating" => URI("http://example.com/ratings/good"))
54
+ ).to eq "http://example.com/ratings/good"
55
+ end
56
+
57
+ it "handles symbol answer keys" do
58
+ expect(
59
+ subject.extract_answer(rating: "bad")
60
+ ).to eq "bad"
61
+ end
62
+
63
+ it "returns nil if answer is missing" do
64
+ expect(
65
+ subject.extract_answer(foo: 1)
66
+ ).to be nil
67
+ end
68
+ end
69
+
70
+ context "with default value" do
71
+ subject {
72
+ described_class.new(field_json(name: "rating", value: "ok"))
73
+ }
74
+
75
+ it "returns default value when answer is missing" do
76
+ expect(
77
+ subject.extract_answer(foo: 1)
78
+ ).to eq "ok"
79
+ end
80
+
81
+ it "returns answer when available" do
82
+ expect(
83
+ subject.extract_answer(rating: "bad")
84
+ ).to eq "bad"
85
+ end
86
+
87
+ end
88
+
89
+ context "hidden field" do
90
+ subject {
91
+ described_class.new(field_json(type: "hidden", name: "rating", value: "ok"))
92
+ }
93
+
94
+ it "ignore answer and use default" do
95
+ expect(
96
+ subject.extract_answer(rating: "bad")
97
+ ).to eq "ok"
98
+ end
99
+ end
100
+ end
101
+
102
+ describe "#path" do
103
+ context "path explicitly specified" do
104
+ subject { described_class.new(field_json(path: "/rating/value")) }
105
+
106
+ it "returns the path" do
107
+ expect(
108
+ subject.path
109
+ ).to eq "/rating/value"
110
+ end
111
+ end
112
+
113
+ context "path omitted" do
114
+ subject { described_class.new(field_json(path: MISSING)) }
115
+
116
+ it "returns the path" do
117
+ expect(
118
+ subject.path
119
+ ).to be nil
120
+ end
121
+ end
122
+ end
123
+
124
+ MISSING = Object.new
125
+
126
+ def field_json(type: "string", name: "rating", path: MISSING, value: MISSING)
127
+ { "name" => name,
128
+ "type" => type }
129
+ .tap{|f|
130
+ f["path"] = path if provided?(path)
131
+ f["value"] = value if provided?(value)
132
+ }
133
+ end
134
+
135
+ def provided?(thing)
136
+ MISSING != thing
137
+ end
138
+
139
+ def string_field_json
140
+ field_json(type: "string")
141
+ end
142
+
143
+ def string_field_json_with_default
144
+ field_json(type: "string", value: "ok")
145
+ end
146
+
147
+ def hidden_field_json
148
+ field_json(type: "hidden", value: "ok")
149
+ end
150
+
151
+ end
@@ -0,0 +1,113 @@
1
+ require "hal_client/form"
2
+ require "hal-client"
3
+
4
+ RSpec.describe HalClient::Form do
5
+ describe ".new" do
6
+ specify { expect( HalClient::Form.new(fieldless_get_form, a_client) ).to behave_like_a HalClient::Form }
7
+ end
8
+
9
+ describe "#target_url" do
10
+ context "vanilla target" do
11
+ subject { HalClient::Form.new(fieldless_get_form, a_client) }
12
+
13
+ specify { expect( subject.target_url ).to eq(Addressable::URI.parse("http://example.com")) }
14
+ end
15
+
16
+ context "templated target" do
17
+ subject { HalClient::Form.new(get_form_with_fields_and_templated_target, a_client) }
18
+
19
+ specify { expect( subject.target_url(rating: 4, description: "pretty good" ) )
20
+ .to eq(Addressable::URI.parse("http://example.com/?description=pretty%20good&rating=4")) }
21
+
22
+ end
23
+ end
24
+
25
+ describe "#submit" do
26
+ context "fieldless GET form" do
27
+ subject { HalClient::Form.new(fieldless_get_form, a_client) }
28
+ let!(:get_request) { stub_request(:get, subject.target_url).to_return(body: "") }
29
+
30
+ specify { expect{ subject.submit }.to make_http_request(get_request) }
31
+ end
32
+
33
+ context "GET form w/ fields and templated target" do
34
+ subject { HalClient::Form.new(get_form_with_fields_and_templated_target, a_client) }
35
+
36
+ specify {
37
+ expect{
38
+ subject.submit(rating: 4, description: "pretty good")
39
+ }.to make_http_request(
40
+ stub_request(:get, subject.target_url(rating: 4, description: "pretty good"))
41
+ )
42
+ }
43
+ end
44
+
45
+ context "POST form w/ fields and json content type" do
46
+ subject { HalClient::Form.new(post_form_with_fields_and_json_content_type, a_client) }
47
+
48
+ specify {
49
+ expect{
50
+ subject.submit(rating: "ok", description: "pretty good")
51
+ }.to make_http_request(
52
+ stub_request(:post, subject.target_url)
53
+ .with(body: { rating: "ok", description: "pretty good" },
54
+ headers: { "Content-Type" => "application/json" })
55
+ )
56
+ }
57
+ end
58
+
59
+ end
60
+
61
+ # Background
62
+
63
+ def fieldless_get_form
64
+ { "_links" => {
65
+ "target" => {
66
+ "href" => "http://example.com"
67
+ }
68
+ },
69
+ "method" => "GET",
70
+ "fields" => []
71
+ }
72
+ end
73
+
74
+ def get_form_with_fields_and_templated_target
75
+ { "_links" => {
76
+ "target" => {
77
+ "href" => "http://example.com/{?description,rating}",
78
+ "templated" => true
79
+ }
80
+ },
81
+ "method" => "GET",
82
+ "fields" => [
83
+ { "name" => "rating",
84
+ "type" => "string" },
85
+ { "name" => "description",
86
+ "type" => "text" }
87
+ ]
88
+ }
89
+ end
90
+
91
+ def post_form_with_fields_and_json_content_type
92
+ { "_links" => {
93
+ "target" => {
94
+ "href" => "http://example.com/"
95
+ }
96
+ },
97
+ "method" => "POST",
98
+ "contentType" => "application/json",
99
+ "fields" => [
100
+ { "name" => "rating",
101
+ "type" => "number",
102
+ "path" => "/rating" },
103
+ { "name" => "description",
104
+ "type" => "text",
105
+ "path" => "/description" }
106
+ ]
107
+ }
108
+ end
109
+
110
+ def a_client
111
+ HalClient.new
112
+ end
113
+ end
@@ -115,6 +115,124 @@ RSpec.describe HalClient::RepresentationSet do
115
115
  end
116
116
  end
117
117
 
118
+ describe "#form" do
119
+ context "single representation with default form" do
120
+ subject(:repr_single_set) { described_class.new([repr_with_default_form]) }
121
+
122
+ specify { expect(
123
+ subject.form
124
+ ).to behave_like_a HalClient::Form }
125
+
126
+ specify { expect(
127
+ subject.form
128
+ ).to target "default-form" }
129
+
130
+ specify { expect(
131
+ subject.form("default")
132
+ ).to behave_like_a HalClient::Form }
133
+
134
+ specify { expect(
135
+ subject.form("default")
136
+ ).to target "default-form" }
137
+
138
+ end
139
+
140
+ context "multiple representations with default form" do
141
+ subject(:repr_single_set) { described_class.new([repr_with_default_form,
142
+ repr_with_default_form]) }
143
+
144
+ specify { expect{
145
+ subject.form
146
+ }.to raise_error KeyError }
147
+
148
+ specify { expect{
149
+ subject.form("default")
150
+ }.to raise_error KeyError }
151
+
152
+ specify { expect{
153
+ subject.form(:default)
154
+ }.to raise_error KeyError }
155
+ end
156
+
157
+ context "multiple representations with unique forms" do
158
+ subject(:repr_single_set) { described_class.new([repr_with_default_form,
159
+ repr_with_foo_form]) }
160
+
161
+ specify { expect(
162
+ subject.form
163
+ ).to behave_like_a HalClient::Form }
164
+
165
+ specify { expect(
166
+ subject.form
167
+ ).to target "default-form" }
168
+
169
+ specify { expect(
170
+ subject.form("default")
171
+ ).to behave_like_a HalClient::Form }
172
+
173
+ specify { expect(
174
+ subject.form("default")
175
+ ).to target "default-form" }
176
+
177
+
178
+ specify { expect(
179
+ subject.form("foo")
180
+ ).to target "foo-form" }
181
+
182
+ specify { expect(
183
+ subject.form("foo")
184
+ ).to behave_like_a HalClient::Form }
185
+
186
+ specify { expect(
187
+ subject.form(:foo)
188
+ ).to target "foo-form" }
189
+
190
+ specify { expect(
191
+ subject.form(:foo)
192
+ ).to behave_like_a HalClient::Form }
193
+
194
+ end
195
+
196
+ matcher :target do |expected_target_url|
197
+ match do |actual_form|
198
+ expect(actual_form.target_url.to_s).to eq expected_target_url
199
+ end
200
+ end
201
+
202
+ let(:repr_with_default_form) {
203
+ HalClient::Representation.new(
204
+ hal_client: a_client,
205
+ parsed_json: inject_form(form_json(target: "default-form"), as: "default",
206
+ into: {}))
207
+ }
208
+
209
+ let(:repr_with_foo_form) {
210
+ HalClient::Representation.new(
211
+ hal_client: a_client,
212
+ parsed_json: inject_form(form_json(target: "foo-form"), as: "foo",
213
+ into: {}))
214
+ }
215
+
216
+ def inject_form(form_json, as:, into: )
217
+ into["_forms"] ||= {}
218
+ into["_forms"][as] = form_json
219
+
220
+ into
221
+ end
222
+
223
+ def form_json(target:)
224
+ { "_links" => {
225
+ "target" => {
226
+ "href" => target
227
+ }
228
+ },
229
+ "method" => "GET",
230
+ "fields" => []
231
+ }
232
+ end
233
+ end
234
+
235
+
118
236
  let(:a_client) { HalClient.new }
119
237
 
120
238
  let(:foo_repr) { HalClient::Representation.new hal_client: a_client, parsed_json: MultiJson.load(foo_hal)}
@@ -119,6 +119,98 @@ HAL
119
119
  end
120
120
  end
121
121
 
122
+ describe "#form" do
123
+ context "default form" do
124
+ subject(:repr) {
125
+ HalClient::Representation.new(
126
+ hal_client: a_client,
127
+ parsed_json: inject_form(form_json(target: "default-form"), as: "default",
128
+ into: {}))
129
+ }
130
+
131
+ specify { expect(
132
+ repr.form
133
+ ).to target "default-form" }
134
+
135
+ specify { expect(
136
+ repr.form
137
+ ).to behave_like_a HalClient::Form }
138
+
139
+ specify { expect(
140
+ repr.form("default")
141
+ ).to target "default-form" }
142
+
143
+ specify { expect(
144
+ repr.form("default")
145
+ ).to behave_like_a HalClient::Form }
146
+ end
147
+
148
+ context "non-default form" do
149
+ subject(:repr) {
150
+ HalClient::Representation.new(
151
+ hal_client: a_client,
152
+ parsed_json: inject_form(form_json(target: "foo-form"), as: "foo", into: {}))
153
+ }
154
+
155
+ specify { expect(
156
+ repr.form("foo")
157
+ ).to target "foo-form" }
158
+
159
+ specify { expect(
160
+ repr.form(:foo)
161
+ ).to target "foo-form"
162
+ }
163
+
164
+ specify { expect{
165
+ repr.form("nonexistent")
166
+ }.to raise_error KeyError }
167
+ end
168
+
169
+ context "multiple forms" do
170
+ subject(:repr) {
171
+ HalClient::Representation.new(
172
+ hal_client: a_client,
173
+ parsed_json: {}.tap { |hal|
174
+ inject_form(form_json(target: "foo-form"), as: "foo", into: hal)
175
+ inject_form(form_json(target: "bar-form"), as: "bar", into: hal)
176
+ }
177
+ )
178
+ }
179
+
180
+ specify { expect(
181
+ repr.form("foo")
182
+ ).to target "foo-form" }
183
+
184
+ specify { expect(
185
+ repr.form("bar")
186
+ ).to target "bar-form" }
187
+ end
188
+
189
+ matcher :target do |expected_target_url|
190
+ match do |actual_form|
191
+ expect(actual_form.target_url.to_s).to eq expected_target_url
192
+ end
193
+ end
194
+
195
+ def inject_form(form_json, as:, into: )
196
+ into["_forms"] ||= {}
197
+ into["_forms"][as] = form_json
198
+
199
+ into
200
+ end
201
+
202
+ def form_json(target:)
203
+ { "_links" => {
204
+ "target" => {
205
+ "href" => target
206
+ }
207
+ },
208
+ "method" => "GET",
209
+ "fields" => []
210
+ }
211
+ end
212
+ end
213
+
122
214
  context "equality and hash" do
123
215
  let(:repr_same_href) { described_class.new(hal_client: a_client,
124
216
  parsed_json: MultiJson.load(<<-HAL)) }
@@ -1,5 +1,10 @@
1
1
  module CustomMatchers
2
2
  extend RSpec::Matchers::DSL
3
+ matcher :behave_like_a do |expected_class|
4
+ match do |actual_instance|
5
+ (expected_class.instance_methods - actual_instance.class.instance_methods).empty?
6
+ end
7
+ end
3
8
 
4
9
  matcher :be_equivalent_json_to do |expected_json|
5
10
  match do |actual_json|
@@ -16,4 +21,15 @@ module CustomMatchers
16
21
  end
17
22
  end
18
23
  end
24
+
25
+ matcher :make_http_request do |expected_request|
26
+ match do |actual_code_under_test|
27
+ actual_code_under_test.call()
28
+ expect(expected_request).to have_been_made
29
+ end
30
+
31
+ def supports_block_expectations?
32
+ true
33
+ end
34
+ end
19
35
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hal-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.2.0
4
+ version: 4.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter Williams
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-10-16 00:00:00.000000000 Z
11
+ date: 2018-06-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: http
@@ -58,6 +58,20 @@ dependencies:
58
58
  - - "~>"
59
59
  - !ruby/object:Gem::Version
60
60
  version: '1.9'
61
+ - !ruby/object:Gem::Dependency
62
+ name: hana
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '1.3'
68
+ type: :runtime
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '1.3'
61
75
  - !ruby/object:Gem::Dependency
62
76
  name: bundler
63
77
  requirement: !ruby/object:Gem::Requirement
@@ -172,6 +186,8 @@ files:
172
186
  - lib/hal_client/collection.rb
173
187
  - lib/hal_client/curie_resolver.rb
174
188
  - lib/hal_client/errors.rb
189
+ - lib/hal_client/form.rb
190
+ - lib/hal_client/form/field.rb
175
191
  - lib/hal_client/interpreter.rb
176
192
  - lib/hal_client/link.rb
177
193
  - lib/hal_client/links_section.rb
@@ -183,6 +199,8 @@ files:
183
199
  - lib/hal_client/version.rb
184
200
  - spec/hal_client/collection_spec.rb
185
201
  - spec/hal_client/curie_resolver_spec.rb
202
+ - spec/hal_client/form/field_spec.rb
203
+ - spec/hal_client/form_spec.rb
186
204
  - spec/hal_client/interpreter_spec.rb
187
205
  - spec/hal_client/link_spec.rb
188
206
  - spec/hal_client/links_section_spec.rb
@@ -220,6 +238,8 @@ summary: Use HAL APIs easily
220
238
  test_files:
221
239
  - spec/hal_client/collection_spec.rb
222
240
  - spec/hal_client/curie_resolver_spec.rb
241
+ - spec/hal_client/form/field_spec.rb
242
+ - spec/hal_client/form_spec.rb
223
243
  - spec/hal_client/interpreter_spec.rb
224
244
  - spec/hal_client/link_spec.rb
225
245
  - spec/hal_client/links_section_spec.rb