yaks 0.4.4 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e75dbaa5cc865ca07fb16ae3e4662f3983652d96
4
- data.tar.gz: bedaee826c1ae525bf0aa5511c62ccb3c02a0738
3
+ metadata.gz: 35548188d34c9c00d48ed2c575a2be98d424f313
4
+ data.tar.gz: 203ad17187b3063c9cf04592f026234d6474bc4e
5
5
  SHA512:
6
- metadata.gz: 8f43aa2b03a6beb9f37009d3540ac8a75623e8dd3fb2b634785be696b81e20a3e7918cb3aef5eac86c63df49390f3ffd806c8a83596beff94e6dfb53793eba3d
7
- data.tar.gz: 326578cce03f31740ceb6e927d431b8a42eefc0f79d8c65e0255bcd4475b952a6b0393eaf5ff11fcc5ae15f738bd7983ee767bd5964148cebccd9f8d68f50547
6
+ metadata.gz: dc6e520c6d8d5d878578481f3ab373867af02a90c3c1ccdf247dba7f3203049b0ba88b4681c6296c726eac52d5d85f30706449be9ae68193680a56ea407050fa
7
+ data.tar.gz: 465fbab2456ff24a8b1d948eb9cae76133a8d3fdbe2e71a6a54c246fe926693e58296626276560a8e55b99a45e4540345fdaf5b797745791af0ee97f53d4905c
data/.gitignore CHANGED
@@ -3,3 +3,5 @@ pkg
3
3
  coverage
4
4
  Gemfile.lock
5
5
  *~
6
+ .yardoc
7
+ doc
data/CHANGELOG.md CHANGED
@@ -1,7 +1,48 @@
1
1
  ### Development
2
- [full changelog](http://github.com/plexus/yaks/compare/v0.4.3...master)
2
+ [full changelog](http://github.com/plexus/yaks/compare/v0.5.0...master)
3
3
 
4
- Mapping a non-empty collection will try to infer the type, and hence rel of the nested items, based on the first object in the collection. This is only relevant for formats like HAL that don't have a top-level collection representation, and only matters when mapping a collection at the top level, not when mapping a collection from an association.
4
+ ### 0.5.0
5
+
6
+ * Yaks now serializes (returns a string), instead of returning a data structure. This is a preparatory step for supporting non-JSON formats. To get the old behavior back, do this
7
+
8
+ ``` ruby
9
+ yaks = Yaks.new do
10
+ skip :serialize
11
+ end
12
+ ```
13
+
14
+ * The old `after` hook has been removed, instead there are now generic hooks for all steps: `before`, `after`, `around`, `skip`; `:map`, `:format`, `:primitivize`, `:serialize`.
15
+
16
+ * By default Yaks uses `JSON.pretty_generate` as a JSON unparser. To use something else, for example `Oj.dump`, do this
17
+
18
+ ``` ruby
19
+ yaks = Yaks.new do
20
+ json_serializer &Oj.method(:dump)
21
+ end
22
+ ```
23
+
24
+ * Mapping a non-empty collection will try to infer the type, and hence rel of the nested items, based on the first object in the collection. This is only relevant for formats like HAL that don't have a top-level collection representation, and only matters when mapping a collection at the top level, not when mapping a collection from an association.
25
+
26
+ * When registering a custom format (Yaks::Format subclass), the signature has changed
27
+
28
+ ``` ruby
29
+
30
+ * Collection+JSON uses a link's "title" attribute to output a link's "name", to better correspond with other formats
31
+
32
+ # 0.4.3
33
+ Format.register self, :collection_json, 'application/vnd.collection+json'
34
+
35
+ # 0.5.0
36
+ register :collection_json, :json, 'application/vnd.collection+json'
37
+ ```
38
+
39
+ * `yaks.call` is now the preferred interface, rather than `yaks.serialize`, although there are no plans yet to remove the alias.
40
+
41
+ * The result of a call to `Yaks.new` now responds to `to_proc`, so you can treat it as a Proc/Symbol, e.g. `some_method &yaks`
42
+
43
+ * Improved YARD documentation
44
+
45
+ * 100% mutation coverage :trumpet: :tada:
5
46
 
6
47
  ### 0.4.3
7
48
 
@@ -106,4 +147,4 @@ end
106
147
 
107
148
  ## v0.2.0
108
149
 
109
- * links can now take a simple for a template to compute a link just like an attribute
150
+ * links can now take a simple for a template to compute a link just like an attribute
data/README.md CHANGED
@@ -27,17 +27,43 @@ They might also contain extra control data to specify possible future interactio
27
27
 
28
28
  These different media types for Hypermedia clients and servers base themselves on the same set of internet standards, such as [RFC4288 Media types](http://tools.ietf.org/html/rfc4288), [RFC5988 Web Linking](http://tools.ietf.org/html/rfc5988), [RFC6906 The "profile" link relation](http://tools.ietf.org/search/rfc6906) and [RFC6570 URI Templates](http://tools.ietf.org/html/rfc6570).
29
29
 
30
- ## Yaks Resources
30
+ ## Concepts
31
31
 
32
- At the core of Yaks is the concept of a Resource, consisting of key-value attributes, RFC5988 style links, and embedded sub-resources.
32
+ Yaks is a processing pipeline, you create and configure the pipeline, then feed data through it.
33
33
 
34
- To build an API you create 'mappers' that turn your domain models into resources. Then you pick a media type 'serializer', which can turn the resource into a hypermedia message.
34
+ ``` ruby
35
+ yaks = Yaks.new do
36
+ default_format :hal
37
+ rel_template 'http://api.example.com/rels/{rel}'
38
+ format_options(:hal, plural_links: [:copyright])
39
+ namespace ::MyAPI
40
+ json_serializer do |data|
41
+ MultiJson.dump(data)
42
+ end
43
+ end
44
+
45
+ yaks.call(data) # => JSON
46
+ ```
47
+
48
+ Yaks performs this serialization in three steps
35
49
 
36
- The web linking standard which defines link relations like 'self', 'next' or 'alternate' is embraced as far as practically possible, for instance to find the URI that uniquely defines a resource, we look at the 'self' link. To distinguish different types of resources we use the 'profile' link.
50
+ * It *maps* your data to a `Yaks::Resource`
51
+ * It *formats* the resource to a syntax tree representation
52
+ * It *serializes* to get the final output
53
+
54
+ For JSON types, the "syntax tree" is just a combination of Ruby primitives, nested arrays and hashes with strings, numbers, booleans, nils.
55
+
56
+ A Resource is an abstraction shared by all output formats. It can contain key-value attributes, RFC5988 style links, and embedded sub-resources.
57
+
58
+ To build an API you create a "mapper" for each type of object you want to represent. Yaks takes care of the rest.
59
+
60
+ For all configuration options see [Yaks::Config::DSL](http://rdoc.info/gems/yaks/frames/Yaks/Config/DSL).
61
+
62
+ See also the [API Docs on rdoc.info](http://rdoc.info/gems/yaks/frames/file/README.md)
37
63
 
38
64
  ## Mappers
39
65
 
40
- To turn your domain models into resources, you define mappers, for example :
66
+ Say your app has a `Post` object for blog posts. To serve posts over your API, define a `PostMapper`
41
67
 
42
68
  ```ruby
43
69
  class PostMapper < Yaks::Mapper
@@ -54,7 +80,7 @@ Configure a Yaks instance and start serializing!
54
80
 
55
81
  ```ruby
56
82
  yaks = Yaks.new
57
- yaks.serialize(post)
83
+ yaks.call(post)
58
84
  ```
59
85
 
60
86
  or a bit more elaborate
@@ -66,7 +92,7 @@ yaks = Yaks.new do
66
92
  format_options(:hal, plural_links: [:copyright])
67
93
  end
68
94
 
69
- yaks.serialize(post, mapper: PostMapper, format: :hal)
95
+ yaks.call(post, mapper: PostMapper, format: :hal)
70
96
  ```
71
97
 
72
98
  ### Attributes
@@ -100,7 +126,7 @@ end
100
126
 
101
127
  #### Filtering
102
128
 
103
- Implementing a `#filter` method in your mappers is no longer supported. Instead you can override `#attributes`, or `#associations`.
129
+ You can override `#attributes`, or `#associations`.
104
130
 
105
131
  ```ruby
106
132
  class SongMapper
@@ -186,6 +212,40 @@ class ShowMapper < Yaks::Mapper
186
212
  end
187
213
  ```
188
214
 
215
+ ## Calling Yaks
216
+
217
+ Once you have a Yaks instance, you can call it with `call`
218
+ (`serialize` also works but might be deprecated in the future.) Pass
219
+ it the data to be serialized, plus options.
220
+
221
+ * `:env` a Rack environment, see next section
222
+ * `:format` the format to be used, e.g. `:json_api`. Note that if the Rack env contains an `Accept` header which resolves to a recognized format, then the header takes precedence
223
+ * `:mapper` the mapper to be used. Will be inferred if omitted
224
+ * `:item_mapper` When rendering a collection, the mapper to be used for each item in the collection. Will be inferred from the class of the first item in the collection if omitted.
225
+
226
+ ### Rack env
227
+
228
+ When serializing, Yaks lets you pass in an `env` hash, which will be made available to all mappers.
229
+
230
+ ```ruby
231
+ yaks = Yaks.new
232
+ yaks.call(foo, env: my_env)
233
+
234
+ class FooMapper
235
+ attributes :bar
236
+
237
+ def bar
238
+ if env['something']
239
+ #...
240
+ end
241
+ end
242
+ end
243
+ ```
244
+
245
+ The env hash will be available to all mappers, so you can use this to pass around context. In particular context related to the current HTTP request, e.g. the current logged in user, which is why the recommended use is to pass in the Rack environment.
246
+
247
+ If `env` contains a `HTTP_ACCEPT` key (Rack's way of representing the `Accept` header), Yaks will return the format that most closely matches what was requested.
248
+
189
249
  ## Namespace
190
250
 
191
251
  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 `namespace`.
@@ -237,28 +297,6 @@ end
237
297
 
238
298
  Yaks will automatically detect and use this collection when serializing an array of `LineItem` objects.
239
299
 
240
- ## Rack env
241
-
242
- When serializing, Yaks lets you pass in an `env` hash, which will be made available to all mappers.
243
-
244
- ```ruby
245
- yaks = Yaks.new
246
- yaks.serialize(foo, env: my_env)
247
-
248
- class FooMapper
249
- attributes :bar
250
-
251
- def bar
252
- if env['something']
253
- #...
254
- end
255
- end
256
- end
257
- ```
258
-
259
- You can use this to pass around context, in particular context related to the current HTTP request, e.g. the current logged in user, which is why the recommended use is to pass in the Rack environment.
260
-
261
- If `env` contains a `HTTP_ACCEPT` key (Rack's way of representing the `Accept` header), Yaks will return the format that most closely matches what was requested.
262
300
 
263
301
  ## Custom attribute/link/subresource handling
264
302
 
@@ -296,7 +334,7 @@ Since version 0.4 the recommended API is through `Yaks.new {...}.serialize`. Thi
296
334
 
297
335
  ```ruby
298
336
  my_yaks = Yaks.new
299
- hal = my_yaks.serialize(model)
337
+ hal = my_yaks.call(model)
300
338
  puts JSON.dump(hal)
301
339
  ```
302
340
 
@@ -352,6 +390,23 @@ Subresources aren't mapped because Collection+JSON doesn't really have that conc
352
390
 
353
391
  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.
354
392
 
393
+ ## Hooks
394
+
395
+ It is possible to hook into the Yaks pipeline to perform extra processing steps before, after, or around each step. It also possible to skip a step.
396
+
397
+ ``` ruby
398
+ yaks = Yaks.new do
399
+ # Automatically give every resource a self link
400
+ after :map, :add_self_link do |resource|
401
+ resource.add_link(Yaks::Resource::Link.new(:self, "/#{resource.type}/#{resource.attributes[:id]}"))
402
+ end
403
+
404
+ # Skip serialization, so Ruby primitives come back instead of JSON
405
+ # This was the default before versions < 0.5.0
406
+ skip :serialize
407
+ end
408
+ ```
409
+
355
410
  ## Policy over Configuration
356
411
 
357
412
  It's an old adage in the Ruby/Rails world to have "Convention over Configuration", mostly to derive values that were not given explicitly. Typically based on things having similar names and a 1-1 derivable relationship.
@@ -398,7 +453,7 @@ end
398
453
 
399
454
  ## Primitives
400
455
 
401
- For JSON based formats, a final step in serializing is to turn the nested data into primitives that have a JSON equivalent. For example, JSON has no notion of symbols or dates. If your mappers return these types as attributes, then Yaks needs to know how to turn these into primitives. To add extra types, use `map_to_primitive`
456
+ For JSON based formats, the "syntax tree" is merely a structure of Ruby primitives that have a JSON equivalent. If your mappers return non-primitive attribute values, you can define how they should be converted. For example, JSON has no notion of dates. If your mappers return these types as attributes, then Yaks needs to know how to turn these into primitives. To add extra types, use `map_to_primitive`
402
457
 
403
458
  ```ruby
404
459
  Yaks.new do
@@ -420,7 +475,9 @@ Yaks.new do
420
475
  end
421
476
  ```
422
477
 
423
- ## Usage
478
+ Yaks by default "primitivizes" symbols (as strings), and classes that include Enumerable (as arrays).
479
+
480
+ ## Real World Usage
424
481
 
425
482
  Yaks is used in production by [Ticketsolve](http://www.ticketsolve.com/). You can find an example API endpoint [here](http://leicestersquaretheatre.ticketsolve.com/api).
426
483
 
data/Rakefile CHANGED
@@ -52,3 +52,13 @@ task :mutant_chunked do
52
52
  Rake::Task["mutant"].execute
53
53
  end
54
54
  end
55
+
56
+ begin
57
+ require 'yard'
58
+
59
+ YARD::Rake::YardocTask.new
60
+ rescue LoadError
61
+ task :yard do
62
+ $stderr.puts 'In order to run yard, you must: gem install yard'
63
+ end
64
+ end
data/bench/bench.rb CHANGED
@@ -13,5 +13,4 @@ Benchmark.ips do |x|
13
13
  x.report "Simple HAL mapping" do
14
14
  $yaks.serialize(input)
15
15
  end
16
-
17
16
  end
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'benchmark/ips'
4
+ require 'yaks'
5
+ require 'ruby-prof'
6
+
7
+ SIZE=20
8
+ $timestamp = Time.now.utc.iso8601.gsub('-', '').gsub(':', '')
9
+ $yaks = Yaks.new
10
+
11
+ FlatModel = Struct.new(:field1, :field2)
12
+ DeepModel = Struct.new(:field, :next)
13
+
14
+ flat = SIZE.times.map do |i|
15
+ FlatModel.new(i, 'x' * (i % 50))
16
+ end
17
+
18
+ deep = nil
19
+ SIZE.times do |i|
20
+ deep = DeepModel.new(i, deep)
21
+ end
22
+
23
+ class FlatMapper < Yaks::Mapper
24
+ attributes :field1, :field2
25
+ link :self, '/model/{field1}'
26
+ end
27
+
28
+ class DeepMapper < Yaks::Mapper
29
+ attributes :field
30
+ link :self, '/model/{field}'
31
+ has_one :next, mapper: DeepMapper
32
+ end
33
+
34
+
35
+ def profile!(name)
36
+ RubyProf.start
37
+ yield
38
+ results = RubyProf.stop
39
+ File.open "/tmp/#{name}-#{$timestamp}.out.#{$$}", 'w' do |file|
40
+ RubyProf::CallTreePrinter.new(results).print(file)
41
+ end
42
+ end
43
+
44
+ do_flat = ->(format) { -> { $yaks.serialize(flat, item_mapper: FlatMapper, format: format) } }
45
+ do_deep = ->(format) { -> { $yaks.serialize(deep, mapper: DeepMapper, format: format) } }
46
+
47
+ 10.times { do_flat[:hal][] }
48
+ 10.times { do_deep[:hal][] }
49
+
50
+ profile!('flat', &do_flat.(:hal))
51
+ profile!('deep', &do_deep.(:hal))
52
+ exit
53
+
54
+ Benchmark.ips(10) do |job|
55
+ Yaks::Format.names.each do |format|
56
+
57
+ job.report "#{format} ; #{SIZE} objects in a list ; no nesting", &do_flat.(format)
58
+ job.report "#{format} ; #{SIZE} objects nested", &do_deep.(format)
59
+ end
60
+ end
@@ -4,6 +4,28 @@ module Yaks
4
4
  # gem to aid upgraiding
5
5
 
6
6
  BreakingChanges = {
7
+
8
+ '0.5.0' => %q~
9
+
10
+ Breaking Changes in Yaks 0.5.0
11
+ ==============================
12
+
13
+ Yaks now serializes its output, you no longer have to convert to JSON
14
+ yourself. Use `skip :serialize' to get the old behavior, or
15
+ `json_serializer` to use a different JSON implementation.
16
+
17
+ The single `after' hook has been replaced with a set of `before',
18
+ `after', `around' and `skip' hooks.
19
+
20
+ If you've created your own subclass of `Yaks::Format' (previously:
21
+ `Yaks::Serializer'), then you need to update the call to
22
+ `Format.register'.
23
+
24
+ These are potentially breaking changes. See the CHANGELOG and README
25
+ for full documentation.
26
+
27
+ ~,
28
+
7
29
  '0.4.3' => %q~
8
30
 
9
31
  Breaking Changes in Yaks 0.4.3
@@ -6,70 +6,157 @@ module Yaks
6
6
  attr_reader :config
7
7
 
8
8
  # @param [Yaks::Config] config
9
- # @param [Proc] blk
10
- # @return [Yaks::Config::DSL]
11
- def initialize(config, &blk)
9
+ # @param [Proc] block
10
+ def initialize(config, &block)
12
11
  @config = config
13
12
  @policy_class = Class.new(DefaultPolicy)
14
13
  @policies = []
15
- instance_eval(&blk) if blk
16
- @policies.each do |policy_blk|
17
- @policy_class.class_eval &policy_blk
14
+ instance_eval(&block) if block
15
+ @policies.each do |policy_block|
16
+ @policy_class.class_eval &policy_block
18
17
  end
19
18
  config.policy_class = @policy_class
20
19
  end
21
20
 
21
+ # Set the options for a format
22
+ #
22
23
  # @param [Symbol] format
23
- # @return [Symbol]
24
+ # @param [Hash] options
25
+ #
26
+ # @example
27
+ #
28
+ # yaks = Yaks.new do
29
+ # format_options :hal, {plural_links: [:related_content]}
30
+ # end
31
+ #
24
32
  def format_options(format, options)
25
33
  config.format_options[format] = options
26
34
  end
27
35
 
36
+ # Set the default format
37
+ #
38
+ # Defaults to +:hal+
39
+ #
28
40
  # @param [Symbol] format
29
- # @return [Symbol]
41
+ # Format identifier, one of +Yaks::Format.names+
42
+ #
43
+ # @example
44
+ #
45
+ # yaks = Yaks.new do
46
+ # default_fomat :json_api
47
+ # end
48
+ #
30
49
  def default_format(format)
31
50
  config.default_format = format
32
51
  end
33
52
 
34
- # @param [Object] klass
35
- # @return [Object]
53
+ # Configure JSON serializer
54
+ #
55
+ # Defaults to JSON.pretty_generate
56
+ #
57
+ # @example
58
+ #
59
+ # yaks = Yaks.new do
60
+ # json_serializer &Oj.method(:dump)
61
+ # end
62
+ #
63
+ # @param [Proc] block
64
+ # Serialization procedure
65
+ #
66
+ def json_serializer(&block)
67
+ config.serializers[:json] = block
68
+ end
69
+
70
+ %w[before after around skip].map(&:intern).each do |hook_type|
71
+ define_method hook_type do |step, name = :"#{hook_type}_#{step}", &block|
72
+ config.hooks << [hook_type, step, name, block]
73
+ end
74
+ end
75
+
76
+ # Set a different policy implementation
77
+ #
78
+ # By default Yaks uses +Yaks::DefaultPolicy+ to derive missing
79
+ # information. You can swap in a class with a compatible
80
+ # interface to change the default behavior
81
+ #
82
+ # To override a single policy method, simply call a method with
83
+ # the same name as part of your Yaks configuration, passing a
84
+ # block to define the new behavior.
85
+ #
86
+ # @example
87
+ #
88
+ # yaks = Yaks.new do
89
+ # derive_type_from_mapper_class do |mapper_class|
90
+ # mapper_class.name.sub(/Mapper^/,'')
91
+ # end
92
+ # end
93
+ #
94
+ # @param [Class] klass
95
+ # Policy class
96
+ #
36
97
  def policy(klass)
37
98
  @policy_class = klass
38
99
  end
39
100
 
40
- # @param [String] templ
41
- # @return [String]
42
- def rel_template(templ)
43
- config.policy_options[:rel_template] = templ
101
+ # Set the template for deriving rels
102
+ #
103
+ # Used to derive rels for links and subresources.
104
+ #
105
+ # @example
106
+ #
107
+ # yaks = Yaks.new do
108
+ # rel_template 'http://api.example.com/rels/{rel}'
109
+ # end
110
+ #
111
+ # @param [String] template
112
+ # A valid URI template containing +{rel}+
113
+ #
114
+ def rel_template(template)
115
+ config.policy_options[:rel_template] = template
44
116
  end
45
117
 
46
- # @param [Object] namespace
47
- # @return [Object]
118
+ # Set the namespace (Ruby module) that contains your mappers
119
+ #
120
+ # When your mappers don't live at the top-level, then set this
121
+ # so Yaks can correctly infer the mapper class from the model
122
+ # class.
123
+ #
124
+ # @example
125
+ #
126
+ # yaks = Yaks.new do
127
+ # mapper_namespace API::Mappers
128
+ # end
129
+ #
130
+ # module API::Mappers
131
+ # class FruitMapper < Yaks::Mapper
132
+ # ...
133
+ # end
134
+ # end
135
+ #
136
+ # class Fruit < BaseModel
137
+ # ...
138
+ # end
139
+ #
140
+ # @param [Module] namespace
141
+ #
48
142
  def mapper_namespace(namespace)
49
143
  config.policy_options[:namespace] = namespace
50
144
  end
51
145
  alias namespace mapper_namespace
52
146
 
53
147
  # @param [Array] args
54
- # @param [Proc] blk
55
- # @return [Array]
56
- def map_to_primitive(*args, &blk)
57
- config.primitivize.map(*args, &blk)
58
- end
59
-
60
148
  # @param [Proc] block
61
- # @return [Array]
62
- def after(&block)
63
- config.steps << block
149
+ def map_to_primitive(*args, &block)
150
+ config.primitivize.map(*args, &block)
64
151
  end
65
152
 
66
153
  # Will define each method available in the DefaultPolicy upon the DSL
67
154
  # and then make it available to apply to any Class taking on the
68
155
  # `@policies` Array.
69
156
  DefaultPolicy.public_instance_methods(false).each do |method|
70
- define_method method do |&blk|
157
+ define_method method do |&block|
71
158
  @policies << proc {
72
- define_method method, &blk
159
+ define_method method, &block
73
160
  }
74
161
  end
75
162
  end
data/lib/yaks/config.rb CHANGED
@@ -1,79 +1,64 @@
1
1
  module Yaks
2
2
  class Config
3
+ include Yaks::FP::Callable
4
+
5
+ # @!attribute [r] format_options
6
+ # @return [Hash<Symbol,Hash>]
7
+ attr_reader :format_options
3
8
 
4
- # @!attribute [rw] format_options
5
- # @return [Hash]
6
9
  # @!attribute [rw] default_format
7
10
  # @return [Symbol]
11
+ attr_accessor :default_format
12
+
8
13
  # @!attribute [rw] policy_class
9
- # @return [Constant]
10
- # @!attribute [rw] policy_options
14
+ # @return [Class]
15
+ attr_accessor :policy_class
16
+
17
+ # @!attribute [r] policy_options
11
18
  # @return [Hash]
19
+ attr_reader :policy_options
20
+
12
21
  # @!attribute [rw] primitivize
13
- # @return [Boolean]
14
- # @!attribute [rw] steps
22
+ # @return [Primitivize]
23
+ attr_accessor :primitivize
24
+
25
+ # @!attribute [r] serializers
26
+ # @return [Hash<Symbol,#call>]
27
+ attr_reader :serializers
28
+
29
+ # @!attribute [r] hooks
15
30
  # @return [Array]
16
- attr_accessor :format_options, :default_format, :policy_class, :policy_options, :primitivize, :steps
31
+ attr_reader :hooks
17
32
 
18
- # @param [Proc] blk
19
- # @return [Yaks::Config]
33
+ # @param blk [Proc] Configuration block
20
34
  def initialize(&blk)
21
35
  @format_options = Hash.new({})
22
36
  @default_format = :hal
23
37
  @policy_options = {}
24
38
  @primitivize = Primitivize.create
25
- @steps = [
26
- @primitivize
27
- ]
39
+ @serializers = {}
40
+ @hooks = []
41
+
28
42
  DSL.new(self, &blk)
29
43
  end
30
44
 
31
- # @return [Yaks::DefaultPolicy, Object]
45
+ # @return [Yaks::DefaultPolicy]
32
46
  def policy
33
- @policy_class.new(@policy_options)
34
- end
35
-
36
- # @param [Hash] opts
37
- # @param [Hash] env
38
- # @return [Class]
39
- def format_class(opts, env)
40
- accept = Rack::Accept::MediaType.new(env['HTTP_ACCEPT'])
41
- mime_type = accept.best_of([nil] + Format.mime_types.values)
42
- return Format.by_mime_type(mime_type) if mime_type
43
- Format.by_name(opts.fetch(:format) { @default_format })
44
- end
45
-
46
- # @param [Hash] opts
47
- # @return [String]
48
- def format_name(opts)
49
- opts.fetch(:format) { @default_format }
50
- end
51
-
52
- # @param [Symbol] format
53
- # @return [Object]
54
- def options_for_format(format)
55
- format_options[format]
47
+ @policy ||= @policy_class.new(@policy_options)
56
48
  end
57
49
 
58
- # model => Yaks::Resource
59
- # Yaks::Resource => serialized structure
60
- # serialized structure => serialized flat
50
+ # Main entry point into yaks
61
51
  #
62
- # @param [Object] object
63
- # @param [Hash] opts
64
- # @return [Object]
65
- def call(object, opts = {})
66
- env = opts.fetch(:env, {})
67
- context = {
68
- policy: policy,
69
- env: env,
70
- mapper_stack: []
71
- }
72
-
73
- mapper = opts.fetch(:mapper) { policy.derive_mapper_from_object(object) }.new(context)
74
- format = format_class(opts, env).new(format_options[format_name(opts)])
75
-
76
- [ mapper, format, *steps ].inject(object) {|memo, step| step.call(memo) }
52
+ # @param object [Object] The object to serialize
53
+ # @param options [Hash<Symbol,Object>] Serialization options
54
+ #
55
+ # @option env [Hash] The rack environment
56
+ # @option format [Symbol] The target format, default :hal
57
+ # @option mapper [Class] Mapper class to use
58
+ # @option item_mapper [Class] Mapper class to use for items in a top-level collection
59
+ #
60
+ def call(object, options = {})
61
+ Runner.new(config: self, object: object, options: options).call
77
62
  end
78
63
  alias serialize call
79
64
  end