yaks 0.3.1 → 0.4.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/CHANGELOG.md +11 -0
  4. data/Gemfile +0 -2
  5. data/LICENSE +7 -0
  6. data/README.md +160 -35
  7. data/Rakefile +2 -1
  8. data/lib/yaks/collection_mapper.rb +25 -18
  9. data/lib/yaks/collection_resource.rb +11 -17
  10. data/lib/yaks/config.rb +96 -0
  11. data/lib/yaks/default_policy.rb +34 -4
  12. data/lib/yaks/fp.rb +18 -0
  13. data/lib/yaks/mapper/association.rb +19 -27
  14. data/lib/yaks/mapper/class_methods.rb +4 -2
  15. data/lib/yaks/mapper/config.rb +24 -39
  16. data/lib/yaks/mapper/has_many.rb +7 -6
  17. data/lib/yaks/mapper/has_one.rb +4 -3
  18. data/lib/yaks/mapper/link.rb +52 -55
  19. data/lib/yaks/mapper.rb +38 -26
  20. data/lib/yaks/null_resource.rb +3 -3
  21. data/lib/yaks/primitivize.rb +29 -27
  22. data/lib/yaks/resource/link.rb +4 -0
  23. data/lib/yaks/resource.rb +18 -7
  24. data/lib/yaks/serializer/collection_json.rb +38 -0
  25. data/lib/yaks/serializer/hal.rb +55 -0
  26. data/lib/yaks/serializer/json_api.rb +61 -0
  27. data/lib/yaks/serializer.rb +25 -4
  28. data/lib/yaks/util.rb +2 -42
  29. data/lib/yaks/version.rb +1 -1
  30. data/lib/yaks.rb +10 -32
  31. data/notes.org +72 -0
  32. data/shaved_yak.gif +0 -0
  33. data/spec/acceptance/acceptance_spec.rb +46 -0
  34. data/spec/acceptance/models.rb +28 -0
  35. data/spec/integration/map_to_resource_spec.rb +11 -15
  36. data/spec/json/confucius.hal.json +23 -0
  37. data/spec/json/confucius.json_api.json +22 -0
  38. data/spec/json/john.hal.json +29 -0
  39. data/spec/json/youtypeitwepostit.collection.json +45 -0
  40. data/spec/spec_helper.rb +12 -1
  41. data/spec/support/shared_contexts.rb +7 -10
  42. data/spec/support/youtypeit_models_mappers.rb +20 -0
  43. data/spec/unit/yaks/collection_mapper_spec.rb +84 -0
  44. data/spec/unit/yaks/collection_resource_spec.rb +72 -0
  45. data/spec/unit/yaks/config_spec.rb +129 -0
  46. data/spec/unit/yaks/fp_spec.rb +31 -0
  47. data/spec/unit/yaks/mapper/association_spec.rb +80 -0
  48. data/spec/{yaks → unit/yaks}/mapper/class_methods_spec.rb +4 -4
  49. data/spec/unit/yaks/mapper/config_spec.rb +191 -0
  50. data/spec/unit/yaks/mapper/has_many_spec.rb +46 -0
  51. data/spec/unit/yaks/mapper/has_one_spec.rb +34 -0
  52. data/spec/unit/yaks/mapper/link_spec.rb +152 -0
  53. data/spec/unit/yaks/mapper_spec.rb +177 -0
  54. data/spec/unit/yaks/resource_spec.rb +40 -0
  55. data/spec/{yaks/hal_serializer_spec.rb → unit/yaks/serializer/hal_spec.rb} +2 -2
  56. data/spec/unit/yaks/serializer_spec.rb +12 -0
  57. data/spec/unit/yaks/util_spec.rb +43 -0
  58. data/spec/yaml/confucius.yaml +10 -0
  59. data/spec/yaml/youtypeitwepostit.yaml +9 -0
  60. data/yaks.gemspec +7 -8
  61. metadata +92 -53
  62. data/Gemfile.lock +0 -111
  63. data/lib/yaks/hal_serializer.rb +0 -59
  64. data/lib/yaks/json_api_serializer.rb +0 -59
  65. data/lib/yaks/link_lookup.rb +0 -23
  66. data/lib/yaks/mapper/lookup.rb +0 -19
  67. data/lib/yaks/mapper/map_links.rb +0 -17
  68. data/lib/yaks/profile_registry.rb +0 -60
  69. data/lib/yaks/rel_registry.rb +0 -20
  70. data/lib/yaks/shared_options.rb +0 -15
  71. data/spec/support/shorthands.rb +0 -22
  72. data/spec/yaks/collection_resource_spec.rb +0 -9
  73. data/spec/yaks/mapper/association_spec.rb +0 -21
  74. data/spec/yaks/mapper/config_spec.rb +0 -77
  75. data/spec/yaks/mapper/has_one_spec.rb +0 -16
  76. data/spec/yaks/mapper/link_spec.rb +0 -38
  77. data/spec/yaks/mapper/map_links_spec.rb +0 -46
  78. data/spec/yaks/mapper_spec.rb +0 -65
  79. data/spec/yaks/resource_spec.rb +0 -23
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ef13062afc3ed4ab23fcb46adb79f920f0ea19eb
4
- data.tar.gz: 3af6551324cee70e9dafea7c809f57109649f72d
3
+ metadata.gz: 598793af50666377a34d0efb9e5264b2c7767f3b
4
+ data.tar.gz: 87071e277c536147dcbe47a3201dbb59156191ef
5
5
  SHA512:
6
- metadata.gz: dea7bd7a65ff486565f0ff020bbfaf5dc8d7860d92d18d83a84d57a10c856c646ae66bdb7ed2c87a16be703e964a646e3256c48897c6120f46c373168bd5a6c4
7
- data.tar.gz: 264fe081d2943d5143d698b9389742a4833fa9cdae2cb29ee255765617a6f4b273832d756d59b4f7144fdbc2cbb15181416769c8a1c631efc8fef0164b7f3b6e
6
+ metadata.gz: 45b38a236ceb4ceafcb68007663069a158b638116a46599071e054979d2747fd47550cfa3b0d04028ccefd8e47e0999e251686ab2bba2f3b08615a84e9b8bf16
7
+ data.tar.gz: 0a0887de90d46d746a4dc1761a2f2d377dbb0bae99dd59c6e1101c1a493eceb147a4fae8344655f98fbebbd1a35ca00c7b0e7fc92b11f4db97df1205e71dcb1b
data/.gitignore CHANGED
@@ -1,3 +1,4 @@
1
1
  pkg
2
2
  .bundle
3
3
  coverage
4
+ Gemfile.lock
data/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ # v0.4.0 (unreleased)
2
+
3
+ * Introduce Yaks.new as the main public interface
4
+ * Fix JsonApiSerializer and make it compliant with current spec
5
+ * Remove Hamster dependency, Yaks new uses plain old Ruby arrays and hashes
6
+ * Remove RelRegistry and ProfileRegistry in favor of a simpler explicit syntax + policy based fallback
7
+ * Add more policy derivation hooks, plus make DefaultPolicy template for rel urls configurable
8
+ * Optionally take a Rack env hash, pass it around so mappers can inspect it
9
+ * Honor the HTTP Accept header if it is present in the rack env
10
+ * Add map_to_primitive configuration option
11
+
1
12
  # v0.3.0
2
13
 
3
14
  * Allow partial expansion of templates, expand certain fields, leave others as URI template in the result.
data/Gemfile CHANGED
@@ -1,5 +1,3 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
-
5
- gem 'hamster', github: 'harukizaemon/hamster'
data/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2013-2014 Arne Brasseur
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md CHANGED
@@ -8,12 +8,16 @@
8
8
  [gemnasium]: https://gemnasium.com/plexus/yaks
9
9
  [codeclimate]: https://codeclimate.com/github/plexus/yaks
10
10
 
11
- # Yaks
11
+ # Yak Serializers
12
12
 
13
13
  ### One Stop Hypermedia Shopping ###
14
14
 
15
+ *We did the shaving for you*
16
+
15
17
  Yaks is a tool for turning your domain models into Hypermedia resources.
16
18
 
19
+ **If you're just starting out with Yaks it is currently recommended to run directly from master until 0.4.0 comes out.**
20
+
17
21
  There are at the moment a number of competing media types for building Hypermedia APIs. These all add a layer of semantics on top of a low level serialization format such as JSON or XML. Even though they each have their own design goals, the core features mostly overlap. They typically provide a way to represent resources (entities), and resource collections, consisting of
18
22
 
19
23
  * Data in key-value format, possibly with composite values
@@ -48,10 +52,39 @@ class PostMapper < Yaks::Mapper
48
52
  end
49
53
  ```
50
54
 
51
- Now you can use this to create a Resource
55
+ Configure a Yaks instance and start serializing!
52
56
 
53
57
  ```ruby
54
- resource = PostMapper.new(post).to_resource
58
+ yaks = Yaks.new
59
+ yaks.serialize(post)
60
+ ```
61
+
62
+ or a bit more elaborate
63
+
64
+ ```ruby
65
+ yaks = Yaks.new do
66
+ default_format :json_api
67
+ rel_template 'http://api.example.com/rels/{association_name}'
68
+ format_options(:hal, plural_links: [:copyright])
69
+ end
70
+
71
+ yaks.serialize(post, mapper: PostMapper, format: :hal)
72
+ ```
73
+
74
+ Yaks by default will find your mappers for you if they follow the naming convention of appending 'Mapper' to the model class name. This (and all other "conventions") can be easily redefined though, see below. If you have your mappers inside a module, use `mapper_namespace`.
75
+
76
+ ```ruby
77
+ module API
78
+ module Mappers
79
+ class PostMapper < Yaks::Mapper
80
+ #...
81
+ end
82
+ end
83
+ end
84
+
85
+ yaks = Yaks.new do
86
+ mapper_namespace API::Mappers
87
+ end
55
88
  ```
56
89
 
57
90
  ### Attributes
@@ -64,6 +97,7 @@ For example, if you are representing data that is stored in a Hash, you could do
64
97
  class PostHashMapper < Yaks::Mapper
65
98
  attributes :id, :body
66
99
 
100
+ # @param name [Symbol]
67
101
  def load_attribute(name)
68
102
  object[name]
69
103
  end
@@ -88,7 +122,7 @@ Implement `filter(attrs)` to filter out specific attributes, e.g. based on optio
88
122
 
89
123
  ```ruby
90
124
  def filter(attrs)
91
- attrs.reject{|attr| options[:exclude].include? attr
125
+ attrs.reject{|attr| options[:exclude].include? attr }
92
126
  end
93
127
  ```
94
128
 
@@ -140,40 +174,101 @@ Use `has_one` for an association that returns a single object, or `has_many` for
140
174
 
141
175
  Options
142
176
 
143
- * `:as` : use a different name for the association in the result
144
177
  * `:mapper` : Use a specific for each instance, will be derived from the class name if omitted (see Policy vs Configuration)
145
178
  * `:collection_mapper` : For mapping the collection as a whole, this defaults to Yaks::CollectionMapper, but you can subclass it for example to add links or attributes on the collection itself
146
- * `:policy` : supply an alternative Policy object
179
+ * `:rel` : Set the relation (symbol or URI) this association has with the object. Will be derived from the association name and the configured rel_template if ommitted
147
180
 
148
- ## Serializers
181
+ ## Custom attribute/link/subresource handling
149
182
 
150
- A resource can be turned in to a specific media type representation, for example [HAL](http://stateless.co/hal_specification.html) using a Serializer
183
+ When inheriting from `Yaks::Mapper`, you can override `map_attributes`, `map_links` and `map_resources` to skip (or augment) above methods, and instead implement your own custom mechanism. For example
151
184
 
152
185
  ```ruby
153
- hal = Yaks::HalSerializer.new(resource).serialize
186
+ class ErrorMapper < Yaks::Mapper
187
+ link :profile, '/api/error'
188
+
189
+ def map_attributes
190
+ attrs = {
191
+ http_code: 500,
192
+ message: object.to_s,
193
+ type: object.class.name.underscore
194
+ }
195
+
196
+ case object
197
+ when AllocationException
198
+ attrs[:http_code] = 422
199
+ when ActiveRecord::RecordNotFound
200
+ attrs[:http_code] = 404
201
+ attrs[:type] = "record_not_found"
202
+ end
203
+
204
+ attrs
205
+ end
206
+ end
207
+ ```
208
+
209
+ ## Resources and Serializers
210
+
211
+ Yaks uses an intermediate "Resource" representation to support multiple output formats. A mapper turns a domain model into a `Yaks::Resource`. A serializer (e.g. `Yaks::Serializer::Hal`) takes the resource and outputs the structure of the target format.
212
+
213
+ Since version 0.4 the recommended API is through `Yaks.new {...}.serialize`. This will give you back a composite value consisting of primitives that have a mapping to JSON, so you can use your favorite JSON encoder to turn this into a character stream.
214
+
215
+ ```ruby
216
+ my_yaks = Yaks.new
217
+ hal = my_yaks.serialize(model)
154
218
  puts JSON.dump(hal)
155
219
  ```
156
220
 
157
- This will give you back a composite types consisting of primitives that have a mapping to JSON, so you can use your favorite JSON encoder to turn this into a character stream.
221
+ There are at least a handful of JSON libraries and implementations for Ruby out there, with different trade-offs. Yaks does not impose an opinion on which one to use
158
222
 
159
- ### Yaks::HalSerializer
223
+ ### HAL
160
224
 
161
- Serializes to HAL. In HAL one decides when building an API which links can only be singular (e.g. self), and which are always represented as an array. So the HalSerializer understand the `:singular_links` option.
225
+ This is the default. In HAL one decides when building an API which links can only be singular (e.g. self), and which are always represented as an array. Yaks defaults to singular as I've found it to be the most common case. If you want specific links to be plural, then configure their rel href as such.
162
226
 
163
227
  ```ruby
164
- hal = Yaks::HalSerializer.new(resource, singular_links: [:self, :"ea:find", :"ea:basket"])
228
+ hal = Yaks.new do
229
+ format_options :hal, plural_links: ['http://api.example.com/rels/foo']
230
+ end
165
231
  ```
166
232
 
167
- CURIEs are not explicitly supported, but it's possible to use them with some effort, see `examples/hal01.rb` for an example.
233
+ CURIEs are not explicitly supported (yet), but it's possible to use them with some effort, see `examples/hal01.rb` for an example.
168
234
 
169
235
  The line between a singular resource and a collection is fuzzy in HAL. To stick close to the spec you're best to create your own singular types that represent collections, rather than rendering a top level CollectionResource.
170
236
 
171
- ### Yaks::JsonApiSerializer
237
+ ### JSON-API
172
238
 
173
- JSON-API has no concept of outbound links, so these will not be rendered, but the profile link information will be used to derive the root key.
239
+ ```ruby
240
+ default_format :json_api
241
+ ```
242
+
243
+ JSON-API has no concept of outbound links, so these will not be rendered. Instead the key will be inferred from the mapper class name by default. This can be changed per mapper:
244
+
245
+ ```ruby
246
+ class AnimalMapper
247
+ key :pet
248
+ end
249
+ ```
174
250
 
175
- * `embed: :resources` : Embed resources in a `{"linked":` section, referenced by id
176
- * `embed: :links` : Use URL style JSON-API
251
+ Or the policy can be overridden:
252
+
253
+ ```ruby
254
+ yaks = Yaks.new do
255
+ derive_type_from_mapper_class do |mapper_class|
256
+ piglatinize(mapper_class.to_s.sub(/Mapper$/, ''))
257
+ end
258
+ end
259
+ ```
260
+
261
+ ### Collection+JSON
262
+
263
+ ```ruby
264
+ default_format :collection_json
265
+ ```
266
+
267
+ Subresources aren't mapped because Collection+JSON doesn't really have that concept, and the other way around templates and queries don't exist (yet) in Yaks.
268
+
269
+ ### More formats
270
+
271
+ Are planned... at the moment HAL is the only format I actually use, so it's the one that's best supported. Adding formats that follow the resource=(attributes, links, subresources) structure or a subset thereof is straightforward. More features, e.g. forms/actions such as used in Mason might be added in the future.
177
272
 
178
273
  ## Policy over Configuration
179
274
 
@@ -181,30 +276,64 @@ It's an old adage in the Ruby/Rails world to have "Convention over Configuration
181
276
 
182
277
  This saves a lot of typing, but for the uninitiated it can also create confusion, the implicitness makes it hard to follow what's going on.
183
278
 
184
- What's worse, is that often the Configuration part is skimmed over, making it very hard to deviate from the Golden Standard.
279
+ What's worse, is that often the Configuration part is skipped entirely, making it very hard to deviate from the Golden Standard.
185
280
 
186
281
  There is another old adage, "Policy vs Mechanism". Implement the mechanisms, but don't dictate the policy.
187
282
 
188
- In Yaks whenever missing values need to be inferred, like finding an unspecified mapper for a relation, this is handled by a policy object. The default is `Yaks::DefaultPolicy`, you can go there to find all the rules of inference. Subclass it and override to fit your needs, then pass it in to each mapper/serializer, they will pass it on to whatever objects they call.
283
+ In Yaks whenever missing values need to be inferred, like finding an unspecified mapper for a relation, this is handled by a policy object. The default is `Yaks::DefaultPolicy`, you can go there to find all the rules of inference. Single rules of inference can be redefined directly in the Yaks configuration:
189
284
 
190
285
  ```ruby
191
- PostMapper.new(post, policy: MyPolicy.new)
286
+ yaks = Yaks.new do
287
+ derive_mapper_from_object do |model|
288
+ # ...
289
+ end
290
+
291
+ derive_type_from_mapper_class do |mapper_class|
292
+ # ...
293
+ end
294
+
295
+ derive_mapper_from_association do |association|
296
+ # ...
297
+ end
298
+
299
+ derive_rel_from_association do |mapper, association|
300
+ # ...
301
+ end
302
+ end
192
303
  ```
193
304
 
194
- ## ProfileRegistry , RelationRegistry
305
+ You can also subclass or create from scratch your own policy class
306
+
307
+ ```ruby
308
+ class MyPolicy < DefaultPolicy
309
+ #...
310
+ end
311
+
312
+ yaks = Yaks.new do
313
+ policy MyPolicy
314
+ end
315
+ ```
195
316
 
196
- ...
317
+ ## Usage
197
318
 
198
- ## Future plans
319
+ Yaks is used in production by [Ticketsolve](http://www.ticketsolve.com/). You can find an example API endpoint [here](http://leicestersquaretheatre.ticketsolve.com/api).
199
320
 
200
- * Collection+JSON
201
- * Siren
202
- * Examples on how to integrate with web frameworks
321
+ Get in touch if you like to see your name and API here.
203
322
 
204
323
  ## Acknowledgment
205
324
 
206
325
  The mapper syntax is largely borrowed from ActiveModel::Serializers, which in turn closely mimics the syntax of ActiveRecord models. It's a great concise syntax that still offers plenty of flexibility, so to not reinvent the wheel I've stuck to the existing syntax as far as practical, although there are several extensions and deviations.
207
326
 
327
+ ## Lightweight
328
+
329
+ Yaks is a lean library. It only depends on a few other tiny libraries (inflection, concord, uri_template). It has no core extensions (monkey patches). There is deliberately no built-in "integration" with existing frameworks, since the API is simply enough. You just call it.
330
+
331
+ If this approach sounds appealing, have a look at [microrb.com](http://microrb.com/).
332
+
333
+ ## Is it any good
334
+
335
+ [Yes](https://news.ycombinator.com/item?id=3067434)
336
+
208
337
  ## How to contribute
209
338
 
210
339
  Run the tests, the examples, try it with your own stuff and leave your impressions in the issues. Or discuss on API-craft.
@@ -221,14 +350,10 @@ To add a feature
221
350
  1. Open an issue as soon as possible to gather feedback
222
351
  2. Same as above, fork, push to named branch, make a pull-request
223
352
 
224
- ## Non-Features
225
-
226
- * No core extensions
227
- * Minimal dependencies
228
- * Only serializes what explicitly has a Serializer, will never call to_json/as_json
229
- * Adding extra output formats does not require altering existing code
230
- * Has no opinion on what to use for final JSON encoding (json, multi_json, yajl, oj, etc.)
353
+ Yaks uses [Mutation Testing](https://github.com/mbj/mutant). Run `rake mutant` and look for percentage coverage. In general this should only go up.
231
354
 
232
355
  ## License
233
356
 
234
- MIT
357
+ MIT License (Expat License), see [LICENSE](./LICENSE)
358
+
359
+ ![](shaved_yak.gif)
data/Rakefile CHANGED
@@ -15,6 +15,7 @@ task :default => :mutant
15
15
 
16
16
  task :mutant do
17
17
  pattern = ENV.fetch('PATTERN', 'Yaks*')
18
- result = Mutant::CLI.run(%w[-Ilib -ryaks --use rspec --score 100] + [pattern])
18
+ opts = ENV.fetch('MUTANT_OPTS', '').split(' ')
19
+ result = Mutant::CLI.run(%w[-Ilib -ryaks --use rspec --score 100] + opts + [pattern])
19
20
  fail unless result == Mutant::CLI::EXIT_SUCCESS
20
21
  end
@@ -1,33 +1,40 @@
1
1
  # -*- coding: utf-8 -*-
2
2
 
3
3
  module Yaks
4
- class CollectionMapper
5
- include Util, Mapper::MapLinks, SharedOptions
6
- extend Mapper::ClassMethods
4
+ class CollectionMapper < Mapper
5
+ attr_reader :collection
6
+ alias collection object
7
7
 
8
- attr_reader :collection, :resource_mapper, :options
9
- private :collection, :resource_mapper, :options
10
-
11
- def_delegators 'self.class', :config
12
- def_delegators :config, :links
8
+ def initialize(collection, context)
9
+ super(collection, context)
10
+ end
13
11
 
14
- def initialize(collection, resource_mapper, options = {})
15
- @collection = collection
16
- @resource_mapper = resource_mapper
17
- @options = YAKS_DEFAULT_OPTIONS.merge(options)
12
+ def resource_mapper
13
+ context[:resource_mapper]
18
14
  end
19
15
 
20
16
  def to_resource
21
- CollectionResource.new(map_links, collection.map {|obj| resource_mapper.new(obj, options).to_resource})
17
+ CollectionResource.new(
18
+ type: collection_type,
19
+ links: map_links,
20
+ attributes: map_attributes,
21
+ members: collection.map do |obj|
22
+ mapper_for_model(obj).new(obj, context).to_resource
23
+ end
24
+ )
22
25
  end
23
26
 
24
- def load_attribute(name)
25
- respond_to?(name) ? send(name) : collection.map(&name.to_sym)
26
- end
27
+ private
27
28
 
28
- def profile_type
29
- resource_mapper.new(nil, options).profile_type
29
+ def collection_type
30
+ return unless context.key?(:resource_mapper)
31
+ resource_mapper.config.type || policy.derive_type_from_mapper_class(resource_mapper)
30
32
  end
31
33
 
34
+ def mapper_for_model(model)
35
+ context.fetch(:resource_mapper) do
36
+ policy.derive_mapper_from_object(model)
37
+ end
38
+ end
32
39
  end
33
40
  end
@@ -14,23 +14,19 @@ module Yaks
14
14
  #
15
15
  # In the second case a collection has a single "subresource", being its
16
16
  # members.
17
- class CollectionResource
18
- include Equalizer.new(:links, :members)
19
- include Enumerable, LinkLookup
17
+ class CollectionResource < Resource
18
+ include Equalizer.new(:type, :links, :attributes, :members)
19
+ include Enumerable
20
20
 
21
21
  extend Forwardable
22
22
 
23
- attr_reader :links, :members
23
+ attr_reader :type, :links, :members
24
24
 
25
25
  def_delegators :members, :each
26
26
 
27
- def initialize(links, members)
28
- @links = Yaks::List(links)
29
- @members = Yaks::List(members)
30
- end
31
-
32
- def attributes
33
- Yaks::Hash()
27
+ def initialize(options)
28
+ super
29
+ @members = options.fetch(:members, [])
34
30
  end
35
31
 
36
32
  # Make a CollectionResource quack like a resource.
@@ -47,16 +43,14 @@ module Yaks
47
43
  #
48
44
  # :(
49
45
  def subresources
50
- if members && members.any?
51
- Yaks::Hash( profile => self )
46
+ if members.any?
47
+ profile_link = links.select{|link| link.rel.equal? :profile}.first.uri
48
+ { profile_link => self }
52
49
  else
53
- Yaks::Hash()
50
+ {}
54
51
  end
55
52
  end
56
53
 
57
- def []
58
- end
59
-
60
54
  def collection?
61
55
  true
62
56
  end
@@ -0,0 +1,96 @@
1
+ module Yaks
2
+ class Config
3
+ class DSL
4
+ attr_reader :config
5
+
6
+ def initialize(config, &blk)
7
+ @config = config
8
+ @policy_class = Class.new(DefaultPolicy)
9
+ @policies = []
10
+ instance_eval(&blk) if blk
11
+ @policies.each do |policy_blk|
12
+ @policy_class.class_eval &policy_blk
13
+ end
14
+ config.policy_class = @policy_class
15
+ end
16
+
17
+ def format_options(format, options)
18
+ config.format_options[format] = options
19
+ end
20
+
21
+ def default_format(format)
22
+ config.default_format = format
23
+ end
24
+
25
+ def policy(klass)
26
+ @policy_class = klass
27
+ end
28
+
29
+ def rel_template(templ)
30
+ config.policy_options[:rel_template] = templ
31
+ end
32
+
33
+ def mapper_namespace(namespace)
34
+ config.policy_options[:namespace] = namespace
35
+ end
36
+
37
+ def map_to_primitive(*args, &blk)
38
+ config.primitivize.map(*args, &blk)
39
+ end
40
+
41
+ DefaultPolicy.public_instance_methods(false).each do |method|
42
+ define_method method do |&blk|
43
+ @policies << proc {
44
+ define_method method, &blk
45
+ }
46
+ end
47
+ end
48
+ end
49
+
50
+ attr_accessor :format_options, :default_format, :policy_class, :policy_options, :primitivize
51
+
52
+ def initialize(&blk)
53
+ @format_options = Hash.new({})
54
+ @default_format = :hal
55
+ @policy_options = {}
56
+ @primitivize = Primitivize.create
57
+ DSL.new(self, &blk)
58
+ end
59
+
60
+ def policy
61
+ @policy_class.new(@policy_options)
62
+ end
63
+
64
+ def serializer_class(opts, env)
65
+ if env.key? 'HTTP_ACCEPT'
66
+ accept = Rack::Accept::Charset.new(env['HTTP_ACCEPT'])
67
+ mime_type = accept.best_of(Yaks::Serializer.mime_types)
68
+ return Yaks::Serializer.by_mime_type(mime_type) if mime_type
69
+ end
70
+ Yaks::Serializer.by_name(opts.fetch(:format) { @default_format })
71
+ end
72
+
73
+ def format_name(opts)
74
+ opts.fetch(:format) { @default_format }
75
+ end
76
+
77
+ def options_for_format(format)
78
+ format_options[format]
79
+ end
80
+
81
+ # model => Yaks::Resource
82
+ # Yaks::Resource => serialized structure
83
+ # serialized structure => serialized flat
84
+
85
+ def serialize(object, opts = {}, env = {})
86
+ context = {
87
+ policy: policy,
88
+ env: env
89
+ }
90
+ mapper = opts.fetch(:mapper) { policy.derive_mapper_from_object(object) }
91
+ resource = mapper.new(object, context).to_resource
92
+ serialized = serializer_class(opts, env).new(resource, format_options[format_name(opts)]).call
93
+ primitivize.call(serialized)
94
+ end
95
+ end
96
+ end
@@ -2,12 +2,42 @@ module Yaks
2
2
  class DefaultPolicy
3
3
  include Util
4
4
 
5
- def derive_profile_from_mapper(mapper)
6
- underscore(mapper.class.name.sub(/Mapper$/, '')).to_sym
5
+ DEFAULTS = {
6
+ rel_template: "rel:src={mapper_name}&dest={association_name}",
7
+ namespace: Kernel
8
+ }
9
+
10
+ attr_reader :options
11
+
12
+ def initialize(options = {})
13
+ @options = DEFAULTS.merge(options)
7
14
  end
8
15
 
9
- def derive_missing_mapper_for_association(association)
10
- Object.const_get("#{camelize(association.name.to_s)}Mapper")
16
+ def derive_mapper_from_object(model)
17
+ if model.respond_to? :to_ary
18
+ if @options[:namespace].const_defined?(:CollectionMapper)
19
+ @options[:namespace].const_get(:CollectionMapper)
20
+ end
21
+ else
22
+ name = model.class.name.split('::').last
23
+ @options[:namespace].const_get(name + 'Mapper')
24
+ end
11
25
  end
26
+
27
+ def derive_type_from_mapper_class(mapper_class)
28
+ underscore(mapper_class.to_s.sub(/Mapper$/, ''))
29
+ end
30
+
31
+ def derive_mapper_from_association(association)
32
+ Object.const_get("#{camelize(singularize(association.name.to_s))}Mapper")
33
+ end
34
+
35
+ def derive_rel_from_association(mapper, association)
36
+ URITemplate.new(@options[:rel_template]).expand(
37
+ mapper_name: derive_type_from_mapper_class(mapper.class),
38
+ association_name: association.name
39
+ )
40
+ end
41
+
12
42
  end
13
43
  end
data/lib/yaks/fp.rb ADDED
@@ -0,0 +1,18 @@
1
+ module Yaks
2
+ module FP
3
+ extend self
4
+
5
+ def curry_method(name)
6
+ method(name).to_proc.curry
7
+ end
8
+
9
+ def identity_function
10
+ ->(x) {x}
11
+ end
12
+ I = identity_function
13
+
14
+ def send_with_args(symbol, *args, &blk)
15
+ ->(obj) { obj.method(symbol).(*args, &blk) }
16
+ end
17
+ end
18
+ end