hal-interpretation 1.4.1 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +81 -2
- data/lib/hal_interpretation/dsl.rb +90 -3
- data/lib/hal_interpretation/version.rb +1 -1
- data/spec/hal_interpretation_spec.rb +46 -4
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ba7860074fdcec96c7e93e4de881bc2acbf2654f
|
4
|
+
data.tar.gz: 99d106bfe6ec85f33979309d5c4983e21fd450ff
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d550101e729d0997a634ca46e9141d19d8cd9023c3b9c156172205e764031674ab235ecb2061ab78d90e403b36b9b3d7e6984d51f09b956bc8d5a219441a5eb4
|
7
|
+
data.tar.gz: b1e21c3e017494d6532adf460fea15edb442d4dbab9f2c55c07689b379ac8bc51785b88bf123e9d9a268a5f5a69b7546d94607a8f8dc09c50b9f94ecce0e7b47
|
data/README.md
CHANGED
@@ -17,10 +17,31 @@ class UserHalInterpreter
|
|
17
17
|
|
18
18
|
item_class User
|
19
19
|
|
20
|
+
# Extract value of the name member of the JSON object and assign it to
|
21
|
+
# the `name` attribute of the model.
|
20
22
|
extract :name
|
23
|
+
|
24
|
+
# Extract the value of the line1 member of the address member of the JSON
|
25
|
+
# object and assign it to the `address_line` attribute of the model.
|
21
26
|
extract :address_line, from: "address/line1"
|
27
|
+
|
28
|
+
# Assign the `seq` attribute of the model a newly generated sequence number.
|
22
29
|
extract :seq, with: ->(_hal_repr) { next_seq_num }
|
23
|
-
|
30
|
+
|
31
|
+
# Extract the birthday member of the JSON object, convert it to a ruby date
|
32
|
+
# and assign it to the `birthday` attribute of the model.
|
33
|
+
extract :birthday, coercion: ->(date_str) { Date.iso8601(date_str) }
|
34
|
+
|
35
|
+
# Extract the targets of the .../knows links, extract the ids from each and
|
36
|
+
# assign those ids to the `friend_ids` attribute of the model.
|
37
|
+
extract_links :friend_ids, coercion: ->(urls) { urls.map{|u| u.split("/").last} },
|
38
|
+
rel: "http://xmlns.com/foaf/0.1/knows"
|
39
|
+
|
40
|
+
# Extract the target of the up link and assign the full url to the up
|
41
|
+
# attribute of the model. Reports a problem if more than one link of this
|
42
|
+
# type is present.
|
43
|
+
extract_link :up
|
44
|
+
|
24
45
|
|
25
46
|
def initialize
|
26
47
|
@cur_seq_num = 0
|
@@ -34,6 +55,61 @@ class UserHalInterpreter
|
|
34
55
|
end
|
35
56
|
```
|
36
57
|
|
58
|
+
This interpreter will work for documents that look like the following
|
59
|
+
|
60
|
+
```json
|
61
|
+
{ "name": "Bob",
|
62
|
+
"address": {
|
63
|
+
"line1": "123 Main St",
|
64
|
+
"city": "Denver"
|
65
|
+
},
|
66
|
+
"birthday": "1980-08-31",
|
67
|
+
"_links": {
|
68
|
+
"http://xmlns.com/foaf/0.1/knows": [
|
69
|
+
{ "href": "http://example.com/alice" },
|
70
|
+
{ "href": "http://example.com/mallory" }
|
71
|
+
],
|
72
|
+
"up": { "href": "http://example.com/vips" }
|
73
|
+
} }
|
74
|
+
```
|
75
|
+
|
76
|
+
or
|
77
|
+
|
78
|
+
```json
|
79
|
+
{ "_embedded": {
|
80
|
+
"item": [
|
81
|
+
{ "name": "Bob",
|
82
|
+
"address": {
|
83
|
+
"line1": "123 Main St",
|
84
|
+
"city": "Denver"
|
85
|
+
},
|
86
|
+
"birthday": "1980-08-31",
|
87
|
+
"_links": {
|
88
|
+
"http://xmlns.com/foaf/0.1/knows": [
|
89
|
+
{ "href": "http://example.com/alice" },
|
90
|
+
{ "href": "http://example.com/mallory" }
|
91
|
+
],
|
92
|
+
"up": { "href": "http://example.com/vips" }
|
93
|
+
} },
|
94
|
+
|
95
|
+
{ "name": "Alice",
|
96
|
+
"address": {
|
97
|
+
"line1": "123 Main St",
|
98
|
+
"city": "Denver"
|
99
|
+
},
|
100
|
+
"birthday": "1979-02-16",
|
101
|
+
"_links": {
|
102
|
+
"http://xmlns.com/foaf/0.1/knows": [
|
103
|
+
{ "href": "http://example.com/bob" },
|
104
|
+
{ "href": "http://example.com/mallory" }
|
105
|
+
],
|
106
|
+
"up": { "href": "http://example.com/vips" }
|
107
|
+
} }
|
108
|
+
]
|
109
|
+
} }
|
110
|
+
|
111
|
+
```
|
112
|
+
|
37
113
|
#### Create
|
38
114
|
|
39
115
|
To interpret a HAL document simply create a new interpreter from the
|
@@ -56,7 +132,7 @@ The `items` method returns an `Enumerable` of valid `item_class` objects.
|
|
56
132
|
|
57
133
|
#### Update
|
58
134
|
|
59
|
-
To update
|
135
|
+
To update an existing record
|
60
136
|
|
61
137
|
```ruby
|
62
138
|
|
@@ -75,6 +151,9 @@ class Users < ApplicationController
|
|
75
151
|
end
|
76
152
|
```
|
77
153
|
|
154
|
+
This approach with produce an error if the JSON contains more than one
|
155
|
+
representation.
|
156
|
+
|
78
157
|
### Errors
|
79
158
|
|
80
159
|
If the JSON being interpreted is invalid or malformed
|
@@ -27,10 +27,97 @@ module HalInterpretation
|
|
27
27
|
attr: attr_name,
|
28
28
|
location: opts.fetch(:from) { "/#{attr_name}" }
|
29
29
|
}
|
30
|
-
extractor_opts[:extraction_proc] = opts.fetch(:with) if opts
|
31
|
-
extractor_opts[:coercion] = opts[:coercion] if opts
|
30
|
+
extractor_opts[:extraction_proc] = opts.fetch(:with) if opts[:with]
|
31
|
+
extractor_opts[:coercion] = opts[:coercion] if opts[:coercion]
|
32
32
|
|
33
33
|
extractors << Extractor.new(extractor_opts)
|
34
34
|
end
|
35
|
+
|
36
|
+
# Declare that an attribute should be extracted the HAL document's
|
37
|
+
# links (or embeddeds) where only one instance of that link type
|
38
|
+
# is legal.
|
39
|
+
#
|
40
|
+
# attr_name - name of the attribute on the model to extract
|
41
|
+
#
|
42
|
+
# opts - hash of named arguments
|
43
|
+
#
|
44
|
+
# :rel - rel of link to extract. Default: attr_name
|
45
|
+
#
|
46
|
+
# :coercion - callable with which the raw URL should transformed
|
47
|
+
# before being stored in the model
|
48
|
+
#
|
49
|
+
# Examples
|
50
|
+
#
|
51
|
+
# extract_link :author_website,
|
52
|
+
# rel: "http://xmlns.com/foaf/0.1/homepage"
|
53
|
+
#
|
54
|
+
# extracts the target of the `.../homepage` link and stores in the
|
55
|
+
# `author_website` attribute of the model.
|
56
|
+
#
|
57
|
+
# extract_link :parent, rel: "up",
|
58
|
+
# coercion: ->(url) {
|
59
|
+
# Blog.find id_from_url(u)
|
60
|
+
# }
|
61
|
+
#
|
62
|
+
# looks up the blog pointed to by the `up` link and stores that
|
63
|
+
# model instance in the `parent` association of the model we are
|
64
|
+
# interpreting.
|
65
|
+
def extract_link(attr_name, opts={})
|
66
|
+
orig_coercion = opts[:coercion] || IDENTITY
|
67
|
+
adjusted_opts = opts.merge coercion: ->(urls) {
|
68
|
+
fail "Too many instances (expected exactly 1, found #{urls.count})" if
|
69
|
+
urls.count > 1
|
70
|
+
|
71
|
+
instance_exec urls.first, &orig_coercion
|
72
|
+
}
|
73
|
+
|
74
|
+
extract_links attr_name, adjusted_opts
|
75
|
+
end
|
76
|
+
|
77
|
+
# Declare that an attribute should be extracted the HAL document's
|
78
|
+
# links (or embeddeds).
|
79
|
+
#
|
80
|
+
# attr_name - name of the attribute on the model to extract
|
81
|
+
#
|
82
|
+
# opts - hash of named arguments
|
83
|
+
#
|
84
|
+
# :rel - rel of link to extract. Default: attr_name
|
85
|
+
#
|
86
|
+
# :coercion - callable with which the raw URL should transformed
|
87
|
+
# before being stored in the model
|
88
|
+
#
|
89
|
+
# Examples
|
90
|
+
#
|
91
|
+
# extract_links :author_websites,
|
92
|
+
# rel: "http://xmlns.com/foaf/0.1/homepage"
|
93
|
+
#
|
94
|
+
# extracts the targets of the `.../homepage` link and stores in the
|
95
|
+
# `author_websites` attribute of the model.
|
96
|
+
#
|
97
|
+
# extract_links :parents, rel: "up",
|
98
|
+
# coercion: ->(urls) {
|
99
|
+
# urls.map { |u| Blog.find id_from_url(u) }
|
100
|
+
# }
|
101
|
+
#
|
102
|
+
# looks up the blogs pointed to by the `up` links and stores that
|
103
|
+
# collection of model instances in the `parents` association of
|
104
|
+
# the model we are interpreting.
|
105
|
+
def extract_links(attr_name, opts={})
|
106
|
+
rel = opts.fetch(:rel) { attr_name }.to_s
|
107
|
+
path = "/_links/" + json_path_escape(rel)
|
108
|
+
|
109
|
+
extract attr_name, from: path,
|
110
|
+
with: ->(r){ r.related_hrefs(rel){[]} },
|
111
|
+
coercion: opts[:coercion]
|
112
|
+
end
|
113
|
+
|
114
|
+
|
115
|
+
protected
|
116
|
+
|
117
|
+
def json_path_escape(rel)
|
118
|
+
rel.gsub('~', '~0').gsub('/', '~1')
|
119
|
+
end
|
120
|
+
|
121
|
+
IDENTITY = ->(o) { o }
|
35
122
|
end
|
36
|
-
end
|
123
|
+
end
|
@@ -11,7 +11,9 @@ describe HalInterpretation do
|
|
11
11
|
item_class test_item_class
|
12
12
|
extract :name
|
13
13
|
extract :latitude, from: "/geo/latitude"
|
14
|
-
|
14
|
+
extract_link :up
|
15
|
+
extract_links :friend_ids, rel: "http://xmlns.com/foaf/0.1/knows",
|
16
|
+
coercion: ->(urls) { urls.map{|u| u.split("/").last } }
|
15
17
|
extract :bday, coercion: ->(val){ Time.parse(val) }
|
16
18
|
extract :seq, with: ->(_) { next_seq_num }
|
17
19
|
|
@@ -37,7 +39,11 @@ describe HalInterpretation do
|
|
37
39
|
"latitude": 39.1
|
38
40
|
}
|
39
41
|
,"_links": {
|
40
|
-
"up": {"href": "/foo"}
|
42
|
+
"up": { "href": "/foo" },
|
43
|
+
"http://xmlns.com/foaf/0.1/knows": [
|
44
|
+
{ "href": "http://example.com/bob" },
|
45
|
+
{ "href": "http://example.com/alice" }
|
46
|
+
]
|
41
47
|
}
|
42
48
|
}
|
43
49
|
JSON
|
@@ -49,6 +55,8 @@ describe HalInterpretation do
|
|
49
55
|
specify { expect(interpreter.item.up).to eq "/foo" }
|
50
56
|
specify { expect(interpreter.item.bday).to eq Time.utc(2013,12,11,10,9,8) }
|
51
57
|
specify { expect(interpreter.item.seq).to eq 1 }
|
58
|
+
specify { expect(interpreter.item.friend_ids).to eq ["bob", "alice"] }
|
59
|
+
|
52
60
|
specify { expect(interpreter.problems).to be_empty }
|
53
61
|
|
54
62
|
context "for update" do
|
@@ -66,6 +74,27 @@ describe HalInterpretation do
|
|
66
74
|
specify { expect(interpreter.item.latitude).to eq 39.1 }
|
67
75
|
specify { expect(interpreter.item.bday).to eq Time.utc(2013,12,11,10,9,8) }
|
68
76
|
end
|
77
|
+
|
78
|
+
context "with embedded links" do
|
79
|
+
let(:json_doc) { <<-JSON }
|
80
|
+
{ "name": "foo"
|
81
|
+
,"bday": "2013-12-11T10:09:08Z"
|
82
|
+
,"geo": {
|
83
|
+
"latitude": 39.1
|
84
|
+
}
|
85
|
+
,"_embedded": {
|
86
|
+
"up": { "_links": { "self": { "href": "/foo" } } },
|
87
|
+
"http://xmlns.com/foaf/0.1/knows": [
|
88
|
+
{ "_links": { "self":{ "href": "http://example.com/bob" } } },
|
89
|
+
{ "_links": { "self":{ "href": "http://example.com/alice" } } }
|
90
|
+
]
|
91
|
+
}
|
92
|
+
}
|
93
|
+
JSON
|
94
|
+
|
95
|
+
specify { expect(interpreter.item.up).to eq "/foo" }
|
96
|
+
specify { expect(interpreter.item.friend_ids).to eq ["bob", "alice"] }
|
97
|
+
end
|
69
98
|
end
|
70
99
|
|
71
100
|
context "valid collection" do
|
@@ -115,22 +144,35 @@ describe HalInterpretation do
|
|
115
144
|
}
|
116
145
|
JSON
|
117
146
|
|
147
|
+
before do
|
148
|
+
test_item_class.class_eval do
|
149
|
+
validates :up, presence: true
|
150
|
+
validates :friend_ids, presence: { message: "only popular people allowed" }
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
118
154
|
specify { expect{interpreter.items}
|
119
155
|
.to raise_exception HalInterpretation::InvalidRepresentationError }
|
120
156
|
context "raised error" do
|
121
157
|
subject(:error) { interpreter.items rescue $! }
|
122
158
|
|
123
159
|
specify { expect(error.problems)
|
124
|
-
.to include matching
|
160
|
+
.to include matching(%r(/geo/latitude\b)).and(match(/\binvalid value\b/i)) }
|
125
161
|
specify { expect(error.problems)
|
126
162
|
.to include matching(%r(/name\b)).and(match(/\bblank\b/i)) }
|
127
163
|
end
|
164
|
+
|
128
165
|
specify { expect(interpreter.problems)
|
129
166
|
.to include matching(%r(/name\b)).and(match(/\bblank\b/i)) }
|
130
167
|
specify { expect(interpreter.problems)
|
131
168
|
.to include matching(%r(/geo/latitude\b)).and(match(/\binvalid value\b/i)) }
|
132
169
|
specify { expect(interpreter.problems)
|
133
170
|
.to include matching(%r(/bday\b)).and(match(/\bno time\b/i)) }
|
171
|
+
specify { expect(interpreter.problems)
|
172
|
+
.to include matching(%r(/_links/up\b)).and(match(/\bblank\b/i)) }
|
173
|
+
specify { expect(interpreter.problems)
|
174
|
+
.to include matching(%r(/_links/http:~1~1xmlns.com~1foaf~10.1~1knows\b))
|
175
|
+
.and(match(/\bpopular\b/i)) }
|
134
176
|
end
|
135
177
|
|
136
178
|
context "collection w/ invalid attributes" do
|
@@ -225,7 +267,7 @@ describe HalInterpretation do
|
|
225
267
|
let(:test_item_class) { Class.new do
|
226
268
|
include ActiveModel::Validations
|
227
269
|
|
228
|
-
attr_accessor :name, :latitude, :up, :bday, :seq, :hair
|
270
|
+
attr_accessor :name, :latitude, :up, :bday, :seq, :hair, :friend_ids
|
229
271
|
|
230
272
|
def initialize
|
231
273
|
yield self
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hal-interpretation
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.5.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: 2014-12-
|
11
|
+
date: 2014-12-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: hal-client
|