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 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