hal-interpretation 1.4.1 → 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|