yaks 0.4.4 → 0.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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/CHANGELOG.md +44 -3
  4. data/README.md +90 -33
  5. data/Rakefile +10 -0
  6. data/bench/bench.rb +0 -1
  7. data/bench/bench_1000.rb +60 -0
  8. data/lib/yaks/breaking_changes.rb +22 -0
  9. data/lib/yaks/config/dsl.rb +114 -27
  10. data/lib/yaks/config.rb +39 -54
  11. data/lib/yaks/default_policy.rb +32 -14
  12. data/lib/yaks/format/collection_json.rb +4 -4
  13. data/lib/yaks/format/hal.rb +20 -3
  14. data/lib/yaks/format/json_api.rb +3 -3
  15. data/lib/yaks/format.rb +54 -9
  16. data/lib/yaks/fp/callable.rb +9 -0
  17. data/lib/yaks/fp/hash_updatable.rb +2 -0
  18. data/lib/yaks/fp/updatable.rb +2 -0
  19. data/lib/yaks/fp.rb +8 -0
  20. data/lib/yaks/mapper/link.rb +2 -2
  21. data/lib/yaks/mapper.rb +6 -6
  22. data/lib/yaks/primitivize.rb +2 -2
  23. data/lib/yaks/resource/link.rb +0 -4
  24. data/lib/yaks/runner.rb +90 -0
  25. data/lib/yaks/util.rb +4 -0
  26. data/lib/yaks/version.rb +1 -1
  27. data/lib/yaks.rb +3 -0
  28. data/spec/acceptance/acceptance_spec.rb +6 -1
  29. data/spec/json/confucius.collection.json +5 -16
  30. data/spec/json/plant_collection.collection.json +32 -0
  31. data/spec/spec_helper.rb +2 -1
  32. data/spec/support/deep_eql.rb +14 -7
  33. data/spec/support/pet_mapper.rb +0 -2
  34. data/spec/unit/yaks/collection_mapper_spec.rb +24 -2
  35. data/spec/unit/yaks/config/dsl_spec.rb +6 -10
  36. data/spec/unit/yaks/config_spec.rb +40 -99
  37. data/spec/unit/yaks/default_policy_spec.rb +20 -0
  38. data/spec/unit/yaks/format/collection_json_spec.rb +41 -0
  39. data/spec/unit/yaks/format/hal_spec.rb +38 -3
  40. data/spec/unit/yaks/format/json_api_spec.rb +2 -2
  41. data/spec/unit/yaks/format_spec.rb +28 -3
  42. data/spec/unit/yaks/fp/callable_spec.rb +13 -0
  43. data/spec/unit/yaks/mapper_spec.rb +226 -126
  44. data/spec/unit/yaks/resource/link_spec.rb +2 -3
  45. data/spec/unit/yaks/resource_spec.rb +15 -0
  46. data/spec/unit/yaks/runner_spec.rb +260 -0
  47. data/spec/unit/yaks/util_spec.rb +7 -1
  48. data/yaks.gemspec +4 -1
  49. metadata +72 -15
  50. /data/spec/json/{hal_plant_collection.json → plant_collection.hal.json} +0 -0
@@ -12,192 +12,292 @@ RSpec.describe Yaks::Mapper do
12
12
 
13
13
  its(:env) { should equal rack_env }
14
14
 
15
- context 'with attributes' do
16
- before do
17
- mapper_class.attributes :foo, :bar
18
- end
19
-
20
- it 'should make the configured attributes available on the instance' do
21
- expect(mapper.attributes).to eq [
22
- Yaks::Mapper::Attribute.new(:foo),
23
- Yaks::Mapper::Attribute.new(:bar)
24
- ]
25
- end
26
-
27
- it 'should load them from the model' do
28
- expect(resource.attributes).to eq(foo: 'hello', bar: 'world')
29
- end
30
-
31
- context 'with attribute filtering' do
15
+ describe '#call' do
16
+ context 'with attributes' do
32
17
  before do
33
- mapper_class.class_eval do
34
- def attributes
35
- super.reject {|attr| attr.name == :foo}
36
- end
37
- end
18
+ mapper_class.attributes :foo, :bar
38
19
  end
39
20
 
40
- it 'should only map the non-filtered attributes' do
41
- expect(resource.attributes).to eq(:bar => 'world')
21
+ it 'should make the configured attributes available on the instance' do
22
+ expect(mapper.attributes).to eq [
23
+ Yaks::Mapper::Attribute.new(:foo),
24
+ Yaks::Mapper::Attribute.new(:bar)
25
+ ]
42
26
  end
43
- end
44
- end
45
27
 
46
- context 'with links' do
47
- before do
48
- mapper_class.link :profile, 'http://foo/bar'
49
- end
28
+ it 'should load them from the model' do
29
+ expect(resource.attributes).to eq(foo: 'hello', bar: 'world')
30
+ end
50
31
 
51
- it 'should map the link' do
52
- expect(resource.links).to eq [
53
- Yaks::Resource::Link.new(:profile, 'http://foo/bar', {})
54
- ]
55
- end
32
+ context 'with attribute filtering' do
33
+ before do
34
+ mapper_class.class_eval do
35
+ def attributes
36
+ super.reject {|attr| attr.name == :foo}
37
+ end
38
+ end
39
+ end
56
40
 
57
- it 'should use the link in the resource' do
58
- expect(resource.links).to include Yaks::Resource::Link.new(:profile, 'http://foo/bar', {})
41
+ it 'should only map the non-filtered attributes' do
42
+ expect(resource.attributes).to eq(:bar => 'world')
43
+ end
44
+ end
59
45
  end
60
46
 
61
- context 'with the same link rel defined multiple times' do
47
+ context 'with links' do
62
48
  before do
63
- mapper_class.class_eval do
64
- link(:self, 'http://foo/bam')
65
- link(:self, 'http://foo/baz')
66
- link(:self, 'http://foo/baq')
67
- end
49
+ mapper_class.link :profile, 'http://foo/bar'
68
50
  end
69
51
 
70
- it 'should map all the links' do
52
+ it 'should map the link' do
71
53
  expect(resource.links).to eq [
72
- Yaks::Resource::Link.new(:profile, 'http://foo/bar', {}),
73
- Yaks::Resource::Link.new(:self, 'http://foo/bam', {}),
74
- Yaks::Resource::Link.new(:self, 'http://foo/baz', {}),
75
- Yaks::Resource::Link.new(:self, 'http://foo/baq', {})
54
+ Yaks::Resource::Link.new(:profile, 'http://foo/bar', {})
76
55
  ]
77
56
  end
78
- end
79
- end
80
57
 
81
- context 'with subresources' do
82
- let(:widget) { fake(type: 'super_widget') }
83
- let(:instance) { fake(widget: widget) }
84
- let(:widget_mapper) { Class.new(Yaks::Mapper) { type 'widget' } }
85
- fake(:policy) { Yaks::DefaultPolicy }
86
-
87
- describe 'has_one' do
88
- let(:has_one_opts) do
89
- { mapper: widget_mapper,
90
- rel: 'http://foo.bar/rels/widgets' }
58
+ it 'should use the link in the resource' do
59
+ expect(resource.links).to include Yaks::Resource::Link.new(:profile, 'http://foo/bar', {})
91
60
  end
92
61
 
93
- before do
94
- widget_mapper.attributes :type
95
- mapper_class.has_one(:widget, has_one_opts)
96
- end
97
-
98
-
99
- it 'should have the subresource in the resource' do
100
- expect(resource.subresources).to eq("http://foo.bar/rels/widgets" => Yaks::Resource.new(type: 'widget', attributes: {:type => "super_widget"}))
101
- end
62
+ context 'with the same link rel defined multiple times' do
63
+ before do
64
+ mapper_class.class_eval do
65
+ link(:self, 'http://foo/bam')
66
+ link(:self, 'http://foo/baz')
67
+ link(:self, 'http://foo/baq')
68
+ end
69
+ end
102
70
 
103
- context 'with explicit mapper and rel' do
104
- it 'should delegate to the given mapper' do
105
- expect(resource.subresources).to eq(
106
- "http://foo.bar/rels/widgets" => Yaks::Resource.new(type: 'widget', attributes: {:type => "super_widget"})
107
- )
71
+ it 'should map all the links' do
72
+ expect(resource.links).to eq [
73
+ Yaks::Resource::Link.new(:profile, 'http://foo/bar', {}),
74
+ Yaks::Resource::Link.new(:self, 'http://foo/bam', {}),
75
+ Yaks::Resource::Link.new(:self, 'http://foo/baz', {}),
76
+ Yaks::Resource::Link.new(:self, 'http://foo/baq', {})
77
+ ]
108
78
  end
109
79
  end
80
+ end
81
+
82
+ context 'with subresources' do
83
+ let(:widget) { fake(type: 'super_widget') }
84
+ let(:instance) { fake(widget: widget) }
85
+ let(:widget_mapper) { Class.new(Yaks::Mapper) { type 'widget' } }
86
+ fake(:policy) { Yaks::DefaultPolicy }
110
87
 
111
- context 'with unspecified mapper' do
88
+ describe 'has_one' do
112
89
  let(:has_one_opts) do
113
- { rel: 'http://foo.bar/rels/widgets' }
90
+ { mapper: widget_mapper,
91
+ rel: 'http://foo.bar/rels/widgets' }
114
92
  end
115
93
 
116
94
  before do
117
- stub(policy).derive_mapper_from_association(mapper.associations.first) do
118
- widget_mapper
119
- end
95
+ widget_mapper.attributes :type
96
+ mapper_class.has_one(:widget, has_one_opts)
120
97
  end
121
98
 
122
- it 'should derive the mapper based on policy' do
123
- expect(resource.subresources).to eq(
124
- "http://foo.bar/rels/widgets" => Yaks::Resource.new(type: 'widget', attributes: {:type => "super_widget"})
125
- )
126
- end
127
- end
128
99
 
129
- context 'with unspecified rel' do
130
- let(:has_one_opts) do
131
- { mapper: widget_mapper }
100
+ it 'should have the subresource in the resource' do
101
+ expect(resource.subresources).to eq("http://foo.bar/rels/widgets" => Yaks::Resource.new(type: 'widget', attributes: {:type => "super_widget"}))
132
102
  end
133
103
 
134
- before do
135
- stub(policy).derive_rel_from_association(mapper.associations.first) do
136
- 'http://rel/rel'
104
+ context 'with explicit mapper and rel' do
105
+ it 'should delegate to the given mapper' do
106
+ expect(resource.subresources).to eq(
107
+ "http://foo.bar/rels/widgets" => Yaks::Resource.new(type: 'widget', attributes: {:type => "super_widget"})
108
+ )
137
109
  end
138
110
  end
139
111
 
140
- it 'should derive the rel based on policy' do
141
- expect(resource.subresources).to eq(
142
- "http://rel/rel" => Yaks::Resource.new(type: 'widget', attributes: {:type => "super_widget"})
143
- )
112
+ context 'with unspecified mapper' do
113
+ let(:has_one_opts) do
114
+ { rel: 'http://foo.bar/rels/widgets' }
115
+ end
116
+
117
+ before do
118
+ stub(policy).derive_mapper_from_association(mapper.associations.first) do
119
+ widget_mapper
120
+ end
121
+ end
122
+
123
+ it 'should derive the mapper based on policy' do
124
+ expect(resource.subresources).to eq(
125
+ "http://foo.bar/rels/widgets" => Yaks::Resource.new(type: 'widget', attributes: {:type => "super_widget"})
126
+ )
127
+ end
144
128
  end
145
- end
146
129
 
147
- context 'with the association filtered out' do
148
- before do
149
- mapper_class.class_eval do
150
- def associations
151
- []
130
+ context 'with unspecified rel' do
131
+ let(:has_one_opts) do
132
+ { mapper: widget_mapper }
133
+ end
134
+
135
+ before do
136
+ stub(policy).derive_rel_from_association(mapper.associations.first) do
137
+ 'http://rel/rel'
152
138
  end
153
139
  end
140
+
141
+ it 'should derive the rel based on policy' do
142
+ expect(resource.subresources).to eq(
143
+ "http://rel/rel" => Yaks::Resource.new(type: 'widget', attributes: {:type => "super_widget"})
144
+ )
145
+ end
154
146
  end
155
147
 
156
- it 'should not map the resource' do
157
- expect(resource.subresources).to eq({})
148
+ context 'with the association filtered out' do
149
+ before do
150
+ mapper_class.class_eval do
151
+ def associations
152
+ []
153
+ end
154
+ end
155
+ end
156
+
157
+ it 'should not map the resource' do
158
+ expect(resource.subresources).to eq({})
159
+ end
158
160
  end
159
161
  end
160
162
  end
161
- end
162
163
 
163
- context 'when the mapper implements a method with the attribute name' do
164
- before do
165
- mapper_class.class_eval do
166
- attributes :fooattr, :bar
164
+ context 'when the mapper implements a method with the attribute name' do
165
+ before do
166
+ mapper_class.class_eval do
167
+ attributes :fooattr, :bar
167
168
 
168
- def fooattr
169
- "#{object.foo} my friend"
169
+ def fooattr
170
+ "#{object.foo} my friend"
171
+ end
170
172
  end
171
173
  end
174
+
175
+ it 'should get the attribute from the mapper' do
176
+ expect(resource.attributes).to eq(fooattr: 'hello my friend', bar: 'world')
177
+ end
172
178
  end
173
179
 
174
- it 'should get the attribute from the mapper' do
175
- expect(resource.attributes).to eq(fooattr: 'hello my friend', bar: 'world')
180
+ context 'with a nil subject' do
181
+ it 'should return a NullResource when the subject is nil' do
182
+ expect(mapper.call(nil)).to be_a Yaks::NullResource
183
+ end
184
+ end
185
+
186
+ context 'with a link generated by a method that returns nil' do
187
+ before do
188
+ mapper_class.class_eval do
189
+ attributes :id
190
+ link :bar_link, :link_generating_method
191
+
192
+ def link_generating_method
193
+ end
194
+ end
195
+ end
196
+
197
+ it 'should not render the link' do
198
+ expect(mapper.call(fake(id: 123))).to eql Yaks::Resource.new(
199
+ type: 'foo',
200
+ attributes: {id: 123}
201
+ )
202
+ end
176
203
  end
177
204
  end
178
205
 
179
- context 'with a nil subject' do
180
- it 'should return a NullResource when the subject is nil' do
181
- expect(mapper.call(nil)).to be_a Yaks::NullResource
206
+ describe '.mapper_name' do
207
+ context 'with a type configured' do
208
+ it 'should use the type' do
209
+ expect(mapper_class.mapper_name(policy)).to eql 'foo'
210
+ end
211
+ end
212
+
213
+ context 'without a type configured' do
214
+ let(:mapper_class) { PetMapper }
215
+
216
+ it 'should infer the type from the name' do
217
+ expect(mapper_class.mapper_name(policy)).to eql 'pet'
218
+ end
182
219
  end
183
220
  end
184
221
 
185
- context 'with a link generated by a method that returns nil' do
186
- before do
187
- mapper_class.class_eval do
188
- attributes :id
189
- link :bar_link, :link_generating_method
222
+ describe '#mapper_name' do
223
+ context 'with a type configured' do
224
+ it 'should use the type' do
225
+ expect(mapper.mapper_name).to eql 'foo'
226
+ end
227
+ end
228
+
229
+ context 'without a type configured' do
230
+ let(:mapper_class) { PetMapper }
190
231
 
191
- def link_generating_method
232
+ it 'should infer the type from the name' do
233
+ expect(mapper.mapper_name).to eql 'pet'
234
+ end
235
+ end
236
+ end
237
+
238
+ describe '#load_attribute' do
239
+ context 'with the attribute defined as a method on the mapper' do
240
+ it 'should call the local implementation' do
241
+ def mapper.foo
242
+ 14
192
243
  end
244
+
245
+ expect(mapper.load_attribute(:foo)).to eql 14
246
+ end
247
+ end
248
+
249
+ context 'without a mapper method with the attribute name' do
250
+ it 'should call the method on the object being mapped' do
251
+ mapper.call(instance) # set @object
252
+ expect(mapper.load_attribute(:foo)).to eql 'hello'
193
253
  end
194
254
  end
255
+ end
256
+
257
+ describe '#map_attributes' do
258
+ let(:attribute) { fake('Attribute') }
259
+
260
+ it 'should receive a context' do
261
+ stub(attribute).add_to_resource(any_args) {|r,_,_| Yaks::Resource.new}
262
+
263
+ mapper.config.attributes[0..-1] = [attribute]
264
+ mapper.call(instance)
265
+
266
+ expect(attribute).to have_received.add_to_resource(any(Yaks::Resource), mapper, yaks_context)
267
+ end
268
+ end
269
+
270
+ describe '#map_links' do
271
+ let(:link) { fake('Link') }
272
+
273
+ it 'should receive a context' do
274
+ stub(link).add_to_resource(any_args) {|r,_,_| Yaks::Resource.new}
275
+
276
+ mapper.config.links[0..-1] = [link]
277
+ mapper.call(instance)
278
+
279
+ expect(link).to have_received.add_to_resource(any(Yaks::Resource), mapper, yaks_context)
280
+ end
281
+ end
282
+
283
+ describe '#map_subresources' do
284
+ let(:association) { fake('Association') }
285
+
286
+ it 'should receive a context' do
287
+ stub(association).add_to_resource(any_args) {|r,_,_| Yaks::Resource.new}
288
+
289
+ mapper.config.associations[0..-1] = [association]
290
+ mapper.call(instance)
291
+
292
+ expect(association).to have_received.add_to_resource(any(Yaks::Resource), mapper, yaks_context)
293
+ end
294
+ end
295
+
296
+ describe '#mapper_stack' do
297
+ let(:yaks_context) { super().merge(mapper_stack: [:foo]) }
195
298
 
196
- it 'should not render the link' do
197
- expect(mapper.call(fake(id: 123))).to eql Yaks::Resource.new(
198
- type: 'foo',
199
- attributes: {id: 123}
200
- )
299
+ it 'should delegate to context' do
300
+ expect(mapper.mapper_stack).to eql [:foo]
201
301
  end
202
302
  end
203
303
  end
@@ -4,13 +4,12 @@ RSpec.describe Yaks::Resource::Link do
4
4
  subject(:link) { described_class.new(rel, uri, options) }
5
5
  let(:rel) { :foo_rel }
6
6
  let(:uri) { 'http://api.example.org/rel/foo' }
7
- let(:options) { {name: 'jimmy', title: 'mr. spectacular' } }
7
+ let(:options) { { title: 'mr. spectacular' } }
8
8
 
9
9
  its(:rel) { should eql :foo_rel }
10
10
  its(:uri) { should eql 'http://api.example.org/rel/foo' }
11
- its(:options) { should eql(name: 'jimmy', title: 'mr. spectacular') }
11
+ its(:options) { should eql(title: 'mr. spectacular') }
12
12
 
13
- its(:name) { should eql('jimmy') }
14
13
  its(:title) { should eql('mr. spectacular') }
15
14
  its(:templated?) { should be false }
16
15
 
@@ -90,4 +90,19 @@ RSpec.describe Yaks::Resource do
90
90
  )
91
91
  end
92
92
  end
93
+
94
+ describe '#self_link' do
95
+ let(:init_opts) {
96
+ { links:
97
+ [
98
+ Yaks::Resource::Link.new(:self, 'foo', {}),
99
+ Yaks::Resource::Link.new(:self, 'bar', {}),
100
+ Yaks::Resource::Link.new(:profile, 'baz', {})
101
+ ]
102
+ }
103
+ }
104
+ it 'should return the last self link' do
105
+ expect(resource.self_link).to eql Yaks::Resource::Link.new(:self, 'bar', {})
106
+ end
107
+ end
93
108
  end