yaks 0.5.0 → 0.6.0.alpha

Sign up to get free protection for your applications and to get access to all the features.
Files changed (99) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +20 -9
  3. data/CHANGELOG.md +2 -3
  4. data/Gemfile +2 -1
  5. data/Rakefile +29 -50
  6. data/yaks/README.md +526 -0
  7. data/{lib → yaks/lib}/yaks/breaking_changes.rb +0 -0
  8. data/{lib → yaks/lib}/yaks/collection_mapper.rb +0 -0
  9. data/{lib → yaks/lib}/yaks/collection_resource.rb +0 -0
  10. data/{lib → yaks/lib}/yaks/config/dsl.rb +0 -0
  11. data/{lib → yaks/lib}/yaks/config.rb +0 -0
  12. data/{lib → yaks/lib}/yaks/default_policy.rb +2 -1
  13. data/{lib → yaks/lib}/yaks/format/collection_json.rb +0 -0
  14. data/{lib → yaks/lib}/yaks/format/hal.rb +0 -0
  15. data/{lib → yaks/lib}/yaks/format/json_api.rb +0 -0
  16. data/{lib → yaks/lib}/yaks/format.rb +0 -0
  17. data/{lib → yaks/lib}/yaks/fp/callable.rb +0 -0
  18. data/{lib → yaks/lib}/yaks/fp/hash_updatable.rb +0 -0
  19. data/{lib → yaks/lib}/yaks/fp/updatable.rb +0 -0
  20. data/{lib → yaks/lib}/yaks/fp.rb +0 -0
  21. data/{lib → yaks/lib}/yaks/mapper/association.rb +0 -0
  22. data/{lib → yaks/lib}/yaks/mapper/association_mapper.rb +0 -0
  23. data/{lib → yaks/lib}/yaks/mapper/attribute.rb +0 -0
  24. data/{lib → yaks/lib}/yaks/mapper/class_methods.rb +0 -0
  25. data/{lib → yaks/lib}/yaks/mapper/config.rb +0 -0
  26. data/{lib → yaks/lib}/yaks/mapper/has_many.rb +0 -0
  27. data/{lib → yaks/lib}/yaks/mapper/has_one.rb +0 -0
  28. data/{lib → yaks/lib}/yaks/mapper/link.rb +0 -0
  29. data/{lib → yaks/lib}/yaks/mapper.rb +0 -0
  30. data/{lib → yaks/lib}/yaks/null_resource.rb +0 -0
  31. data/{lib → yaks/lib}/yaks/primitivize.rb +0 -0
  32. data/{lib → yaks/lib}/yaks/resource/link.rb +0 -0
  33. data/{lib → yaks/lib}/yaks/resource.rb +0 -0
  34. data/{lib → yaks/lib}/yaks/runner.rb +12 -1
  35. data/{lib → yaks/lib}/yaks/util.rb +0 -0
  36. data/yaks/lib/yaks/version.rb +3 -0
  37. data/{lib → yaks/lib}/yaks.rb +0 -0
  38. data/{spec → yaks/spec}/acceptance/acceptance_spec.rb +0 -0
  39. data/{spec → yaks/spec}/acceptance/json_shared_examples.rb +0 -0
  40. data/{spec → yaks/spec}/acceptance/models.rb +0 -0
  41. data/{spec → yaks/spec}/fixture_helpers.rb +0 -0
  42. data/{spec → yaks/spec}/integration/map_to_resource_spec.rb +0 -0
  43. data/{spec → yaks/spec}/json/confucius.collection.json +0 -0
  44. data/{spec → yaks/spec}/json/confucius.hal.json +0 -0
  45. data/{spec → yaks/spec}/json/confucius.json_api.json +0 -0
  46. data/{spec → yaks/spec}/json/john.hal.json +0 -0
  47. data/{spec → yaks/spec}/json/plant_collection.collection.json +0 -0
  48. data/{spec → yaks/spec}/json/plant_collection.hal.json +0 -0
  49. data/{spec → yaks/spec}/json/youtypeitwepostit.collection.json +0 -0
  50. data/{spec → yaks/spec}/spec_helper.rb +0 -0
  51. data/{spec → yaks/spec}/support/classes_for_policy_testing.rb +0 -0
  52. data/{spec → yaks/spec}/support/deep_eql.rb +0 -0
  53. data/{spec → yaks/spec}/support/fixtures.rb +0 -0
  54. data/{spec → yaks/spec}/support/friends_mapper.rb +0 -0
  55. data/{spec → yaks/spec}/support/models.rb +0 -0
  56. data/{spec → yaks/spec}/support/pet_mapper.rb +0 -0
  57. data/{spec → yaks/spec}/support/pet_peeve_mapper.rb +0 -0
  58. data/{spec → yaks/spec}/support/shared_contexts.rb +0 -0
  59. data/{spec → yaks/spec}/support/youtypeit_models_mappers.rb +0 -0
  60. data/{spec → yaks/spec}/unit/yaks/collection_mapper_spec.rb +0 -0
  61. data/{spec → yaks/spec}/unit/yaks/collection_resource_spec.rb +0 -0
  62. data/{spec → yaks/spec}/unit/yaks/config/dsl_spec.rb +0 -0
  63. data/{spec → yaks/spec}/unit/yaks/config_spec.rb +0 -0
  64. data/{spec → yaks/spec}/unit/yaks/default_policy/derive_mapper_from_object_spec.rb +0 -0
  65. data/{spec → yaks/spec}/unit/yaks/default_policy_spec.rb +0 -0
  66. data/{spec → yaks/spec}/unit/yaks/format/collection_json_spec.rb +0 -0
  67. data/{spec → yaks/spec}/unit/yaks/format/hal_spec.rb +0 -0
  68. data/{spec → yaks/spec}/unit/yaks/format/json_api_spec.rb +0 -0
  69. data/{spec → yaks/spec}/unit/yaks/format_spec.rb +0 -0
  70. data/{spec → yaks/spec}/unit/yaks/fp/callable_spec.rb +0 -0
  71. data/{spec → yaks/spec}/unit/yaks/fp/hash_updatable_spec.rb +0 -0
  72. data/{spec → yaks/spec}/unit/yaks/fp/updatable_spec.rb +0 -0
  73. data/{spec → yaks/spec}/unit/yaks/fp_spec.rb +0 -0
  74. data/{spec → yaks/spec}/unit/yaks/mapper/association_mapper_spec.rb +0 -0
  75. data/{spec → yaks/spec}/unit/yaks/mapper/association_spec.rb +0 -0
  76. data/{spec → yaks/spec}/unit/yaks/mapper/attribute_spec.rb +0 -0
  77. data/{spec → yaks/spec}/unit/yaks/mapper/class_methods_spec.rb +0 -0
  78. data/{spec → yaks/spec}/unit/yaks/mapper/config_spec.rb +0 -0
  79. data/{spec → yaks/spec}/unit/yaks/mapper/has_many_spec.rb +0 -0
  80. data/{spec → yaks/spec}/unit/yaks/mapper/has_one_spec.rb +0 -0
  81. data/{spec → yaks/spec}/unit/yaks/mapper/link_spec.rb +0 -0
  82. data/{spec → yaks/spec}/unit/yaks/mapper_spec.rb +0 -0
  83. data/{spec → yaks/spec}/unit/yaks/null_resource_spec.rb +0 -0
  84. data/{spec → yaks/spec}/unit/yaks/primitivize_spec.rb +0 -0
  85. data/{spec → yaks/spec}/unit/yaks/resource/link_spec.rb +0 -0
  86. data/{spec → yaks/spec}/unit/yaks/resource_spec.rb +0 -0
  87. data/{spec → yaks/spec}/unit/yaks/runner_spec.rb +1 -1
  88. data/{spec → yaks/spec}/unit/yaks/util_spec.rb +0 -0
  89. data/{spec → yaks/spec}/yaml/confucius.yaml +0 -0
  90. data/{spec → yaks/spec}/yaml/youtypeitwepostit.yaml +0 -0
  91. data/{yaks.gemspec → yaks/yaks.gemspec} +0 -0
  92. data/yaks-html/README.md +3 -0
  93. data/yaks-html/lib/yaks/format/html.rb +70 -0
  94. data/yaks-html/lib/yaks/format/template.html +56 -0
  95. data/yaks-html/lib/yaks-html.rb +3 -0
  96. data/yaks-html/spec/spec_helper.rb +6 -0
  97. data/yaks-html/yaks-html.gemspec +22 -0
  98. metadata +99 -164
  99. data/lib/yaks/version.rb +0 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 35548188d34c9c00d48ed2c575a2be98d424f313
4
- data.tar.gz: 203ad17187b3063c9cf04592f026234d6474bc4e
3
+ metadata.gz: d43a0d564f2fe2f4f074611448fdc3158211aa76
4
+ data.tar.gz: b15fa02453e1b71c6de06ac9fc157dddf09be27d
5
5
  SHA512:
6
- metadata.gz: dc6e520c6d8d5d878578481f3ab373867af02a90c3c1ccdf247dba7f3203049b0ba88b4681c6296c726eac52d5d85f30706449be9ae68193680a56ea407050fa
7
- data.tar.gz: 465fbab2456ff24a8b1d948eb9cae76133a8d3fdbe2e71a6a54c246fe926693e58296626276560a8e55b99a45e4540345fdaf5b797745791af0ee97f53d4905c
6
+ metadata.gz: 115a1a00edaf48376c53e47b63ac3c2c96c0eec27fa6cba627c3e455320b5c36a1c6c331f834d08f5718fd991365cfb96ae8c46139df5f17f8cff38326a475f3
7
+ data.tar.gz: 03103204631478678810e10eccaca2cd66ed9bafbb3b25cf891b8f23e4aab01ed1b2493c918ebf3622f1a20d32d34e42476d8c3ac4b8fdea52752bd557d49066
data/.travis.yml CHANGED
@@ -1,25 +1,36 @@
1
1
  language: ruby
2
- script: bundle exec $RUN
2
+ script: bundle exec rake $TASK
3
3
  rvm:
4
4
  - 1.9.3
5
- - 2.0.0
6
- - 2.1.2
5
+ - 2.0
6
+ - 2.1
7
7
  - ruby-head
8
8
  - rbx-2
9
9
  - jruby
10
10
  - jruby-head
11
11
  env:
12
- - RUN=rspec
13
- - RUN=rake
12
+ - TASK=yaks:rspec
13
+ - TASK=yaks:mutant
14
+ - TASK=yaks-html:rspec
15
+ - TASK=yaks-html:mutant
14
16
  matrix:
15
17
  allow_failures:
16
18
  - rvm: ruby-head
17
19
  - rvm: jruby-head
18
- - env: RUN=rake
20
+ - env: TASK=yaks:mutant
21
+ - env: TASK=yaks-html:mutant
19
22
  exclude:
20
23
  - rvm: jruby
21
- env: RUN=rake
24
+ env: TASK=yaks:mutant
25
+ - rvm: jruby
26
+ env: TASK=yaks-html:mutant
27
+
28
+ - rvm: jruby-head
29
+ env: TASK=yaks:mutant
22
30
  - rvm: jruby-head
23
- env: RUN=rake
31
+ env: TASK=yaks-html:mutant
32
+
33
+ - rvm: rbx-2
34
+ env: TASK=yaks:mutant
24
35
  - rvm: rbx-2
25
- env: RUN=rake
36
+ env: TASK=yaks-html:mutant
data/CHANGELOG.md CHANGED
@@ -23,12 +23,11 @@ end
23
23
 
24
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
25
 
26
+ * Collection+JSON uses a link's "title" attribute to output a link's "name", to better correspond with other formats
27
+
26
28
  * When registering a custom format (Yaks::Format subclass), the signature has changed
27
29
 
28
30
  ``` 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
31
  # 0.4.3
33
32
  Format.register self, :collection_json, 'application/vnd.collection+json'
34
33
 
data/Gemfile CHANGED
@@ -1,3 +1,4 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- gemspec
3
+ gemspec path: 'yaks'
4
+ gemspec path: 'yaks-html'
data/Rakefile CHANGED
@@ -1,64 +1,43 @@
1
- require 'rubygems/package_task'
2
1
  require 'yaks'
3
-
4
- spec = Gem::Specification.load(Pathname.glob('*.gemspec').first.to_s)
5
- Gem::PackageTask.new(spec).define
2
+ require 'yaks-html'
3
+ require 'mutant'
4
+ require 'rubygems/package_task'
5
+ require 'rspec/core/rake_task'
6
+ require 'yard'
6
7
 
7
8
  desc "Push gem to rubygems.org"
8
- task :push => :gem do
9
+ task :push => ["yaks:gem", "yaks-html:gem"] do
9
10
  sh "git tag v#{Yaks::VERSION}"
10
11
  sh "git push --tags"
11
12
  sh "gem push pkg/yaks-#{Yaks::VERSION}.gem"
13
+ sh "gem push pkg/yaks-html-#{Yaks::VERSION}.gem"
12
14
  end
13
15
 
14
- require 'mutant'
15
- task :default => :mutant
16
+ def gem_tasks(gem)
17
+ namespace gem do
18
+ spec = Gem::Specification.load("#{gem}/#{gem}.gemspec")
19
+ Gem::PackageTask.new(spec).define
16
20
 
17
- task :mutant do
18
- pattern = ENV.fetch('PATTERN', 'Yaks*')
19
- opts = ENV.fetch('MUTANT_OPTS', '').split(' ')
20
- result = Mutant::CLI.run(%w[-Ilib -ryaks --use rspec --score 100] + opts + [pattern])
21
- fail unless result == Mutant::CLI::EXIT_SUCCESS
22
- end
21
+ task :mutant do
22
+ pattern = ENV.fetch('PATTERN', gem == :yaks ? 'Yaks*' : 'Yaks::Format::HTML*')
23
+ opts = ENV.fetch('MUTANT_OPTS', '').split(' ')
24
+ Dir.chdir gem.to_s do
25
+ result = Mutant::CLI.run(%W[-Ilib -ryaks --use rspec --score 100] + opts + [pattern])
26
+ fail unless result == Mutant::CLI::EXIT_SUCCESS
27
+ end
28
+ end
23
29
 
24
- task :mutant_chunked do
25
- [
26
- # Yaks::Util,
27
- # Yaks::Primitivize,
28
- Yaks::FP,
29
- Yaks::Resource,
30
- Yaks::NullResource,
31
- Yaks::CollectionResource,
32
- Yaks::Mapper::Association,
33
- Yaks::Mapper::AssociationMapper,
34
- Yaks::Mapper::HasMany,
35
- Yaks::Mapper::HasOne,
36
- Yaks::Mapper::Config,
37
- Yaks::Mapper::ClassMethods,
38
- Yaks::Mapper::Attribute,
39
- Yaks::Format,
40
- Yaks::Config::DSL,
41
- Yaks::CollectionMapper,
42
- Yaks::Mapper::Link,
43
- Yaks::Format::JsonApi,
44
- Yaks::DefaultPolicy, # 45/249 (81.93%)
45
- Yaks::Format::CollectionJson, # 15/183 (91.80%)
46
- Yaks::Format::Hal, # 17/209 (91.87%)
47
- Yaks::Mapper, # 12/203 (94.09%)
48
- Yaks::Config, # 12/263 (95.44%)
49
- ].each do |space|
50
- puts space
51
- ENV['PATTERN'] = "#{space}"
52
- Rake::Task["mutant"].execute
53
- end
54
- end
30
+ RSpec::Core::RakeTask.new(:rspec) do |t, task_args|
31
+ t.rspec_opts = "-I#{gem}/spec #{gem}/spec"
32
+ end
55
33
 
56
- begin
57
- require 'yard'
58
34
 
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'
35
+ YARD::Rake::YardocTask.new do |t|
36
+ t.files = ["#{gem}/lib/**/*.rb" "#{gem}/**/*.md"]
37
+ end
63
38
  end
39
+
64
40
  end
41
+
42
+ gem_tasks(:yaks)
43
+ gem_tasks(:"yaks-html")
data/yaks/README.md ADDED
@@ -0,0 +1,526 @@
1
+ [![Gem Version](https://badge.fury.io/rb/yaks.png)][gem]
2
+ [![Build Status](https://secure.travis-ci.org/plexus/yaks.png?branch=master)][travis]
3
+ [![Dependency Status](https://gemnasium.com/plexus/yaks.png)][gemnasium]
4
+ [![Code Climate](https://codeclimate.com/github/plexus/yaks.png)][codeclimate]
5
+
6
+ [gem]: https://rubygems.org/gems/yaks
7
+ [travis]: https://travis-ci.org/plexus/yaks
8
+ [gemnasium]: https://gemnasium.com/plexus/yaks
9
+ [codeclimate]: https://codeclimate.com/github/plexus/yaks
10
+
11
+ # Yak Serializers
12
+
13
+ ### One Stop Hypermedia Shopping ###
14
+
15
+ *We did the shaving for you*
16
+
17
+ Yaks is a tool for turning your domain models into Hypermedia resources.
18
+
19
+ 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
20
+
21
+ * Data in key-value format, possibly with composite values
22
+ * Embedded resources
23
+ * Links to related resources
24
+ * Outbound links that have a specific relation to the resource
25
+
26
+ They might also contain extra control data to specify possible future interactions, not unlike HTML forms.
27
+
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
+
30
+ ## Concepts
31
+
32
+ Yaks is a processing pipeline, you create and configure the pipeline, then feed data through it.
33
+
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
49
+
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)
63
+
64
+ ## Mappers
65
+
66
+ Say your app has a `Post` object for blog posts. To serve posts over your API, define a `PostMapper`
67
+
68
+ ```ruby
69
+ class PostMapper < Yaks::Mapper
70
+ link :self, '/api/posts/{id}'
71
+
72
+ attributes :id, :title
73
+
74
+ has_one :author
75
+ has_many :comments
76
+ end
77
+ ```
78
+
79
+ Configure a Yaks instance and start serializing!
80
+
81
+ ```ruby
82
+ yaks = Yaks.new
83
+ yaks.call(post)
84
+ ```
85
+
86
+ or a bit more elaborate
87
+
88
+ ```ruby
89
+ yaks = Yaks.new do
90
+ default_format :json_api
91
+ rel_template 'http://api.example.com/rels/{rel}'
92
+ format_options(:hal, plural_links: [:copyright])
93
+ end
94
+
95
+ yaks.call(post, mapper: PostMapper, format: :hal)
96
+ ```
97
+
98
+ ### Attributes
99
+
100
+ Use the `attributes` DSL method to specify which attributes of your model you want to expose, as in the example above. You can override the `load_attribute` method to change how attributes are fetched from the model.
101
+
102
+ For example, if you are representing data that is stored in a Hash, you could do
103
+
104
+ ```ruby
105
+ class PostHashMapper < Yaks::Mapper
106
+ attributes :id, :body
107
+
108
+ # @param name [Symbol]
109
+ def load_attribute(name)
110
+ object[name]
111
+ end
112
+ end
113
+ ```
114
+
115
+ The default implementation will first try to find a matching method for an attribute on the mapper itself, and will then fall back to calling the actual model. So you can add extra 'virtual' attributes like so :
116
+
117
+ ```ruby
118
+ class CommentMapper < Yaks::Mapper
119
+ attributes :id, :body, :date
120
+
121
+ def date
122
+ object.created_at.strftime("at %I:%M%p")
123
+ end
124
+ end
125
+ ```
126
+
127
+ #### Filtering
128
+
129
+ You can override `#attributes`, or `#associations`.
130
+
131
+ ```ruby
132
+ class SongMapper
133
+ attributes :title, :duration, :lyrics
134
+
135
+ has_one :artist
136
+ has_one :album
137
+
138
+ def minimal?
139
+ env['HTTP_PREFER'] =~ /minimal/
140
+ end
141
+
142
+ # @return Array<Yaks::Mapper::Attribute>
143
+ def attributes
144
+ return super.reject {|attr| attr.name.equal? :lyrics } if minimal?
145
+ super
146
+ end
147
+
148
+ # @return Array<Yaks::Mapper::Association>
149
+ def associations
150
+ return [] if minimal?
151
+ super
152
+ end
153
+ end
154
+ ```
155
+
156
+ ### Links
157
+
158
+ You can specify link templates that will be expanded with model attributes. The link relation name should be a registered [IANA link relation](http://www.iana.org/assignments/link-relations/link-relations.xhtml) or a URL. The template syntax follows [RFC6570 URI templates](http://tools.ietf.org/html/rfc6570).
159
+
160
+ ```ruby
161
+ class FooMapper < Yaks::Mapper
162
+ link :self, '/api/foo/{id}'
163
+ link 'http://api.foo.com/rels/comments', '/api/foo/{id}/comments'
164
+ end
165
+ ```
166
+
167
+ To prevent a link to be expanded, add `expand: false` as an option. Now the actual template will be rendered in the result, so clients can use it to generate links from.
168
+
169
+ To partially expand the template, pass an array with field names to expand. e.g.
170
+
171
+ ```ruby
172
+ class ProductMapper < Yaks::Mapper
173
+ link 'http://api.foo.com/rels/line_item', '/api/line_items?product_id={product_id}&quantity={quantity}', expand: [:product_id]
174
+ end
175
+
176
+ # "_links": {
177
+ # "http://api.foo.com/rels/line_item": {
178
+ # "href": "/api/line_items?product_id=273&quantity={quantity}",
179
+ # "templated": true
180
+ # }
181
+ # }
182
+
183
+ ```
184
+
185
+ You can pass a symbol instead of a template, in that case the symbol will be used as a method name on the object to retrieve the link. You can override this behavior just like with attributes.
186
+
187
+ ```ruby
188
+ class FooMapper < Yaks::Mapper
189
+ link 'http://api.foo.com/rels/go_home', :home_url
190
+ # by default calls object.home_url
191
+
192
+ def home_url
193
+ object.setting('home_url')
194
+ end
195
+ end
196
+ ```
197
+
198
+ ### Associations
199
+
200
+ Use `has_one` for an association that returns a single object, or `has_many` for embedding a collection.
201
+
202
+ Options
203
+
204
+ * `:mapper` : Use a specific for each instance, will be derived from the class name if omitted (see Policy vs Configuration)
205
+ * `: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
206
+ * `: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
207
+ * `:link_if`: Conditionally render the association as a link. A `:href` option is required
208
+
209
+ ```ruby
210
+ class ShowMapper < Yaks::Mapper
211
+ has_many :events, href: '/show/{id}/events', link_if: ->{ events.count > 50 }
212
+ end
213
+ ```
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
+
249
+ ## Namespace
250
+
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`.
252
+
253
+ ```ruby
254
+ module API
255
+ module Mappers
256
+ class PostMapper < Yaks::Mapper
257
+ #...
258
+ end
259
+ end
260
+ end
261
+
262
+ yaks = Yaks.new do
263
+ namespace API::Mappers
264
+ end
265
+ ```
266
+
267
+ If your namespace contains a `CollectionMapper`, Yaks will use that instead of `Yaks::CollectionMapper`, e.g.
268
+
269
+ ```ruby
270
+ module API
271
+ module Mappers
272
+ class CollectionMapper < Yaks::CollectionMapper
273
+ link :profile, 'http://api.example.com/profiles/collection'
274
+ end
275
+ end
276
+ end
277
+ ```
278
+
279
+ You can also have collection mappers based on the type of members the collection holds, e.g.
280
+
281
+ ```ruby
282
+ module API
283
+ module Mappers
284
+ class LineItemCollectionMapper < Yaks::CollectionMapper
285
+ link :profile, 'http://api.example.com/profiles/line_items'
286
+ attributes :total
287
+
288
+ def total
289
+ collection.inject(0) do |memo, line_item|
290
+ memo + line_item.price * line_item.quantity
291
+ end
292
+ end
293
+ end
294
+ end
295
+ end
296
+ ```
297
+
298
+ Yaks will automatically detect and use this collection when serializing an array of `LineItem` objects.
299
+
300
+
301
+ ## Custom attribute/link/subresource handling
302
+
303
+ 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. These methods take a `Yaks::Resource` instance, and should return an updated resource. They should not alter the resource instance in-place. For example
304
+
305
+ ```ruby
306
+ class ErrorMapper < Yaks::Mapper
307
+ link :profile, '/api/error'
308
+
309
+ def map_attributes(resource)
310
+ attrs = {
311
+ http_code: 500,
312
+ message: object.to_s,
313
+ type: object.class.name.underscore
314
+ }
315
+
316
+ case object
317
+ when AllocationException
318
+ attrs[:http_code] = 422
319
+ when ActiveRecord::RecordNotFound
320
+ attrs[:http_code] = 404
321
+ attrs[:type] = "record_not_found"
322
+ end
323
+
324
+ resource.update_attributes(attrs)
325
+ end
326
+ end
327
+ ```
328
+
329
+ ## Resources and Serializers
330
+
331
+ 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.
332
+
333
+ 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.
334
+
335
+ ```ruby
336
+ my_yaks = Yaks.new
337
+ hal = my_yaks.call(model)
338
+ puts JSON.dump(hal)
339
+ ```
340
+
341
+ 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
342
+
343
+ ### HAL
344
+
345
+ 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.
346
+
347
+ ```ruby
348
+ hal = Yaks.new do
349
+ format_options :hal, plural_links: ['http://api.example.com/rels/foo']
350
+ end
351
+ ```
352
+
353
+ CURIEs are not explicitly supported (yet), but it's possible to use them with some effort, see `examples/hal01.rb` for an example.
354
+
355
+ 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.
356
+
357
+ ### JSON-API
358
+
359
+ ```ruby
360
+ default_format :json_api
361
+ ```
362
+
363
+ 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:
364
+
365
+ ```ruby
366
+ class AnimalMapper
367
+ key :pet
368
+ end
369
+ ```
370
+
371
+ Or the policy can be overridden:
372
+
373
+ ```ruby
374
+ yaks = Yaks.new do
375
+ derive_type_from_mapper_class do |mapper_class|
376
+ piglatinize(mapper_class.to_s.sub(/Mapper$/, ''))
377
+ end
378
+ end
379
+ ```
380
+
381
+ ### Collection+JSON
382
+
383
+ ```ruby
384
+ default_format :collection_json
385
+ ```
386
+
387
+ 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.
388
+
389
+ ### More formats
390
+
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.
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
+
410
+ ## Policy over Configuration
411
+
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.
413
+
414
+ 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.
415
+
416
+ What's worse, is that often the Configuration part is skipped entirely, making it very hard to deviate from the Golden Standard.
417
+
418
+ There is another old adage, "Policy vs Mechanism". Implement the mechanisms, but don't dictate the policy.
419
+
420
+ 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:
421
+
422
+ ```ruby
423
+ yaks = Yaks.new do
424
+ derive_mapper_from_object do |model|
425
+ # ...
426
+ end
427
+
428
+ derive_type_from_mapper_class do |mapper_class|
429
+ # ...
430
+ end
431
+
432
+ derive_mapper_from_association do |association|
433
+ # ...
434
+ end
435
+
436
+ derive_rel_from_association do |mapper, association|
437
+ # ...
438
+ end
439
+ end
440
+ ```
441
+
442
+ You can also subclass or create from scratch your own policy class
443
+
444
+ ```ruby
445
+ class MyPolicy < DefaultPolicy
446
+ #...
447
+ end
448
+
449
+ yaks = Yaks.new do
450
+ policy MyPolicy
451
+ end
452
+ ```
453
+
454
+ ## Primitives
455
+
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`
457
+
458
+ ```ruby
459
+ Yaks.new do
460
+ map_to_primitive Date, Time, DateTime do |date|
461
+ date.iso8601
462
+ end
463
+ end
464
+ ```
465
+
466
+ This can also be used to transform alternative data structures, like those from Hamster, into Ruby arrays and hashes. Use `call()` to recursively turn things into primitives.
467
+
468
+ ```ruby
469
+ Yaks.new do
470
+ map_to_primitive Hamster::Vector, Hamster::List do |list|
471
+ list.map do |item|
472
+ call(item)
473
+ end
474
+ end
475
+ end
476
+ ```
477
+
478
+ Yaks by default "primitivizes" symbols (as strings), and classes that include Enumerable (as arrays).
479
+
480
+ ## Real World Usage
481
+
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).
483
+
484
+ Get in touch if you like to see your name and API here.
485
+
486
+ ## Demo
487
+
488
+ You can find an example app at [Yakports](https://github.com/plexus/yakports), or browse the HAL api directly using the [HAL browser](http://yaks-airports.herokuapp.com/browser.html).
489
+
490
+ ## Acknowledgment
491
+
492
+ 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.
493
+
494
+ ## Lightweight
495
+
496
+ 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.
497
+
498
+ If this approach sounds appealing, have a look at [microrb.com](http://microrb.com/).
499
+
500
+ ## Is it any good
501
+
502
+ [Yes](https://news.ycombinator.com/item?id=3067434)
503
+
504
+ ## How to contribute
505
+
506
+ Run the tests, the examples, try it with your own stuff and leave your impressions in the issues. Or discuss on [API-craft](https://groups.google.com/d/forum/api-craft).
507
+
508
+ To fix a bug
509
+
510
+ 1. Fork the repo
511
+ 2. Fix the bug, add tests for it
512
+ 3. Push it to a named branch
513
+ 4. Add a PR
514
+
515
+ To add a feature
516
+
517
+ 1. Open an issue as soon as possible to gather feedback
518
+ 2. Same as above, fork, push to named branch, make a pull-request
519
+
520
+ Yaks uses [Mutation Testing](https://github.com/mbj/mutant). Run `rake mutant` and look for percentage coverage. In general this should only go up.
521
+
522
+ ## License
523
+
524
+ MIT License (Expat License), see [LICENSE](./LICENSE)
525
+
526
+ ![](shaved_yak.gif)