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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/CHANGELOG.md +11 -0
- data/Gemfile +0 -2
- data/LICENSE +7 -0
- data/README.md +160 -35
- data/Rakefile +2 -1
- data/lib/yaks/collection_mapper.rb +25 -18
- data/lib/yaks/collection_resource.rb +11 -17
- data/lib/yaks/config.rb +96 -0
- data/lib/yaks/default_policy.rb +34 -4
- data/lib/yaks/fp.rb +18 -0
- data/lib/yaks/mapper/association.rb +19 -27
- data/lib/yaks/mapper/class_methods.rb +4 -2
- data/lib/yaks/mapper/config.rb +24 -39
- data/lib/yaks/mapper/has_many.rb +7 -6
- data/lib/yaks/mapper/has_one.rb +4 -3
- data/lib/yaks/mapper/link.rb +52 -55
- data/lib/yaks/mapper.rb +38 -26
- data/lib/yaks/null_resource.rb +3 -3
- data/lib/yaks/primitivize.rb +29 -27
- data/lib/yaks/resource/link.rb +4 -0
- data/lib/yaks/resource.rb +18 -7
- data/lib/yaks/serializer/collection_json.rb +38 -0
- data/lib/yaks/serializer/hal.rb +55 -0
- data/lib/yaks/serializer/json_api.rb +61 -0
- data/lib/yaks/serializer.rb +25 -4
- data/lib/yaks/util.rb +2 -42
- data/lib/yaks/version.rb +1 -1
- data/lib/yaks.rb +10 -32
- data/notes.org +72 -0
- data/shaved_yak.gif +0 -0
- data/spec/acceptance/acceptance_spec.rb +46 -0
- data/spec/acceptance/models.rb +28 -0
- data/spec/integration/map_to_resource_spec.rb +11 -15
- data/spec/json/confucius.hal.json +23 -0
- data/spec/json/confucius.json_api.json +22 -0
- data/spec/json/john.hal.json +29 -0
- data/spec/json/youtypeitwepostit.collection.json +45 -0
- data/spec/spec_helper.rb +12 -1
- data/spec/support/shared_contexts.rb +7 -10
- data/spec/support/youtypeit_models_mappers.rb +20 -0
- data/spec/unit/yaks/collection_mapper_spec.rb +84 -0
- data/spec/unit/yaks/collection_resource_spec.rb +72 -0
- data/spec/unit/yaks/config_spec.rb +129 -0
- data/spec/unit/yaks/fp_spec.rb +31 -0
- data/spec/unit/yaks/mapper/association_spec.rb +80 -0
- data/spec/{yaks → unit/yaks}/mapper/class_methods_spec.rb +4 -4
- data/spec/unit/yaks/mapper/config_spec.rb +191 -0
- data/spec/unit/yaks/mapper/has_many_spec.rb +46 -0
- data/spec/unit/yaks/mapper/has_one_spec.rb +34 -0
- data/spec/unit/yaks/mapper/link_spec.rb +152 -0
- data/spec/unit/yaks/mapper_spec.rb +177 -0
- data/spec/unit/yaks/resource_spec.rb +40 -0
- data/spec/{yaks/hal_serializer_spec.rb → unit/yaks/serializer/hal_spec.rb} +2 -2
- data/spec/unit/yaks/serializer_spec.rb +12 -0
- data/spec/unit/yaks/util_spec.rb +43 -0
- data/spec/yaml/confucius.yaml +10 -0
- data/spec/yaml/youtypeitwepostit.yaml +9 -0
- data/yaks.gemspec +7 -8
- metadata +92 -53
- data/Gemfile.lock +0 -111
- data/lib/yaks/hal_serializer.rb +0 -59
- data/lib/yaks/json_api_serializer.rb +0 -59
- data/lib/yaks/link_lookup.rb +0 -23
- data/lib/yaks/mapper/lookup.rb +0 -19
- data/lib/yaks/mapper/map_links.rb +0 -17
- data/lib/yaks/profile_registry.rb +0 -60
- data/lib/yaks/rel_registry.rb +0 -20
- data/lib/yaks/shared_options.rb +0 -15
- data/spec/support/shorthands.rb +0 -22
- data/spec/yaks/collection_resource_spec.rb +0 -9
- data/spec/yaks/mapper/association_spec.rb +0 -21
- data/spec/yaks/mapper/config_spec.rb +0 -77
- data/spec/yaks/mapper/has_one_spec.rb +0 -16
- data/spec/yaks/mapper/link_spec.rb +0 -38
- data/spec/yaks/mapper/map_links_spec.rb +0 -46
- data/spec/yaks/mapper_spec.rb +0 -65
- data/spec/yaks/resource_spec.rb +0 -23
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 598793af50666377a34d0efb9e5264b2c7767f3b
|
4
|
+
data.tar.gz: 87071e277c536147dcbe47a3201dbb59156191ef
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 45b38a236ceb4ceafcb68007663069a158b638116a46599071e054979d2747fd47550cfa3b0d04028ccefd8e47e0999e251686ab2bba2f3b08615a84e9b8bf16
|
7
|
+
data.tar.gz: 0a0887de90d46d746a4dc1761a2f2d377dbb0bae99dd59c6e1101c1a493eceb147a4fae8344655f98fbebbd1a35ca00c7b0e7fc92b11f4db97df1205e71dcb1b
|
data/.gitignore
CHANGED
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
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
|
-
#
|
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
|
-
|
55
|
+
Configure a Yaks instance and start serializing!
|
52
56
|
|
53
57
|
```ruby
|
54
|
-
|
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
|
-
* `:
|
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
|
-
##
|
181
|
+
## Custom attribute/link/subresource handling
|
149
182
|
|
150
|
-
|
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
|
-
|
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
|
-
|
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
|
-
###
|
223
|
+
### HAL
|
160
224
|
|
161
|
-
|
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
|
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
|
-
###
|
237
|
+
### JSON-API
|
172
238
|
|
173
|
-
|
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
|
-
|
176
|
-
|
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
|
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.
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
6
|
-
|
4
|
+
class CollectionMapper < Mapper
|
5
|
+
attr_reader :collection
|
6
|
+
alias collection object
|
7
7
|
|
8
|
-
|
9
|
-
|
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
|
15
|
-
|
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(
|
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
|
-
|
25
|
-
respond_to?(name) ? send(name) : collection.map(&name.to_sym)
|
26
|
-
end
|
27
|
+
private
|
27
28
|
|
28
|
-
def
|
29
|
-
|
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
|
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(
|
28
|
-
|
29
|
-
@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
|
51
|
-
|
46
|
+
if members.any?
|
47
|
+
profile_link = links.select{|link| link.rel.equal? :profile}.first.uri
|
48
|
+
{ profile_link => self }
|
52
49
|
else
|
53
|
-
|
50
|
+
{}
|
54
51
|
end
|
55
52
|
end
|
56
53
|
|
57
|
-
def []
|
58
|
-
end
|
59
|
-
|
60
54
|
def collection?
|
61
55
|
true
|
62
56
|
end
|
data/lib/yaks/config.rb
ADDED
@@ -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
|
data/lib/yaks/default_policy.rb
CHANGED
@@ -2,12 +2,42 @@ module Yaks
|
|
2
2
|
class DefaultPolicy
|
3
3
|
include Util
|
4
4
|
|
5
|
-
|
6
|
-
|
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
|
10
|
-
|
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
|