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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a2e12f276257a0cbc1b66f5777f5dee67570f6e7
4
- data.tar.gz: 1fd1c93f8399581deb75ac6b2c8566b7179b9c20
3
+ metadata.gz: ba7860074fdcec96c7e93e4de881bc2acbf2654f
4
+ data.tar.gz: 99d106bfe6ec85f33979309d5c4983e21fd450ff
5
5
  SHA512:
6
- metadata.gz: 41c74b5b41ae43af541941fab2ef38a02b5d9905551d95b1b303a2cfdd0b23946ea689448c59c77ba36e5e72f45d0f8a12650703a49042ce23803a2d466b26f2
7
- data.tar.gz: 576ff03799bbeaaf6939eb649848a990085ae86df21e14cace8af6ad75a0cc0ef89ab83583b26c07b959f17de31548c8198771658f8ca6f6b517a8571a41090d
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
- extract :birthday, coercion: ->(date_str) { Date.iso8601(date_str) }
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 and existing record
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.key? :with
31
- extractor_opts[:coercion] = opts[:coercion] if opts.key? :coercion
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
@@ -1,3 +1,3 @@
1
1
  module HalInterpretation
2
- VERSION = "1.4.1"
2
+ VERSION = "1.5.0"
3
3
  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
- extract :up, with: ->(hal_repr){hal_repr.related_hrefs("up").first}, from: "/_links/up"
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 matching(%r(/geo/latitude\b)).and(match(/\binvalid value\b/i)) }
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.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-05 00:00:00.000000000 Z
11
+ date: 2014-12-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: hal-client