yaks 0.4.0 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f3292feb7cef328921828cc97b68f397d23b7269
4
- data.tar.gz: 0d074d9a0ea85e905d22572182904fe4f6fb3e36
3
+ metadata.gz: 40c42f259955a15a723d0782ab4378201bf511b3
4
+ data.tar.gz: 360a9de28a43b27cf820c26aec19c6627a7dac2b
5
5
  SHA512:
6
- metadata.gz: a61962b2c69d90ba0b4ccbd2223bc14b27e0ee5ae773bcb12392c7d5bb9a9969497e5e86ae85761ebbab732230dd14f59dc3e8241b35282d9630c39b5c0f41fe
7
- data.tar.gz: 12d9e712b28185dc1409518045da51d4eb4a9fd32c7be42a55747e7fd33b4698728a3c46bf170f54183576f7437eafa54db795fb13d4177594b9de62ed418165
6
+ metadata.gz: 5fb54a5b204c60f385aa4b8e4915c6fce1c89dcb1340b48323242c4ade3a4265d44a5ff37f5d587302cab90ad6e05003e160e290fe69b36f19285934d94f3faf
7
+ data.tar.gz: 5d4462422ddd28e6106846a8663fbdb43bf2ad33b302c24bdcc0f424edae60d1774455d0f70cd3041422aafdd6d0d6ee697558942026ac1bd84d66f149816748
data/CHANGELOG.md CHANGED
@@ -1,10 +1,17 @@
1
+ # v0.4.1
2
+
3
+ * Change how env is passed to yaks.serialize to match docs
4
+ * Fix JSON-API bug (#18 reported by Nicolas Blanco)
5
+ * Don't pluralize has_one association names in JSON-API
6
+
1
7
  # v0.4.0
2
8
 
3
9
  * Introduce after {} post-processing hook
4
- * Streamline interfaces and variable names
10
+ * Streamline interfaces and variable names, especially the use of `call`
5
11
  * Improve deriving mappers automatically, even with Rails style autoloading
6
12
  * Give CollectionResource a members_rel, for HAL-like formats with no top-level collection concept
7
- * Switch back to using `src` and `dest` as the rel-template keys
13
+ * Switch back to using `src` and `dest` as the rel-template keys, instead of `association_name`
14
+ * deprecate `mapper_namespace` in favor of `namespace`
8
15
 
9
16
  # v0.4.0.rc1
10
17
 
data/README.md CHANGED
@@ -16,8 +16,6 @@
16
16
 
17
17
  Yaks is a tool for turning your domain models into Hypermedia resources.
18
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
-
21
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
22
20
 
23
21
  * Data in key-value format, possibly with composite values
@@ -64,29 +62,13 @@ or a bit more elaborate
64
62
  ```ruby
65
63
  yaks = Yaks.new do
66
64
  default_format :json_api
67
- rel_template 'http://api.example.com/rels/{association_name}'
65
+ rel_template 'http://api.example.com/rels/{dest}'
68
66
  format_options(:hal, plural_links: [:copyright])
69
67
  end
70
68
 
71
69
  yaks.serialize(post, mapper: PostMapper, format: :hal)
72
70
  ```
73
71
 
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
88
- ```
89
-
90
72
  ### Attributes
91
73
 
92
74
  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.
@@ -178,6 +160,80 @@ Options
178
160
  * `: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
179
161
  * `: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
180
162
 
163
+ ## Namespace
164
+
165
+ 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`.
166
+
167
+ ```ruby
168
+ module API
169
+ module Mappers
170
+ class PostMapper < Yaks::Mapper
171
+ #...
172
+ end
173
+ end
174
+ end
175
+
176
+ yaks = Yaks.new do
177
+ namespace API::Mappers
178
+ end
179
+ ```
180
+
181
+ If your namespace contains a `CollectionMapper`, Yaks will use that instead of `Yaks::CollectionMapper`, e.g.
182
+
183
+ ```ruby
184
+ module API
185
+ module Mappers
186
+ class CollectionMapper < Yaks::CollectionMapper
187
+ link :profile, 'http://api.example.com/profiles/collection'
188
+ end
189
+ end
190
+ end
191
+ ```
192
+
193
+ You can also have collection mappers based on the type of members the collection holds, e.g.
194
+
195
+ ```ruby
196
+ module API
197
+ module Mappers
198
+ class LineItemCollectionMapper < Yaks::CollectionMapper
199
+ link :profile, 'http://api.example.com/profiles/line_items'
200
+ attributes :total
201
+
202
+ def total
203
+ collection.inject(0) do |memo, line_item|
204
+ memo + line_item.price * line_item.quantity
205
+ end
206
+ end
207
+ end
208
+ end
209
+ end
210
+ ```
211
+
212
+ Yaks will automatically detect and use this collection when serializing an array of `LineItem` objects.
213
+
214
+ ## Rack env
215
+
216
+ When serializing, Yaks lets you pass in an `env` hash, which will be made available to all mappers.
217
+
218
+ ```ruby
219
+ yaks = Yaks.new
220
+ yaks.serialize(foo, env: my_env)
221
+
222
+ class FooMapper
223
+ attributes :bar
224
+
225
+ def bar
226
+ if env['something']
227
+ #...
228
+ end
229
+ end
230
+ end
231
+ ```
232
+
233
+ 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.
234
+
235
+ 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.
236
+
181
237
  ## Custom attribute/link/subresource handling
182
238
 
183
239
  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
@@ -320,8 +376,8 @@ For JSON based formats, a final step in serializing is to turn the nested data i
320
376
 
321
377
  ```ruby
322
378
  Yaks.new do
323
- map_to_primitive Date, Time, DateTime do
324
- object.iso8601
379
+ map_to_primitive Date, Time, DateTime do |date|
380
+ date.iso8601
325
381
  end
326
382
  end
327
383
  ```
@@ -330,8 +386,8 @@ This can also be used to transform alternative data structures, like those from
330
386
 
331
387
  ```ruby
332
388
  Yaks.new do
333
- map_to_primitive Hamster::Vector, Hamster::List do
334
- object.map do |item|
389
+ map_to_primitive Hamster::Vector, Hamster::List do |list|
390
+ list.map do |item|
335
391
  call(item)
336
392
  end
337
393
  end
@@ -20,21 +20,24 @@ module Yaks
20
20
  def call(collection)
21
21
  @object = collection
22
22
 
23
- CollectionResource.new(
23
+ attrs = {
24
24
  type: collection_type,
25
- members_rel: members_rel,
26
25
  links: map_links,
27
26
  attributes: map_attributes,
28
27
  members: collection.map do |obj|
29
28
  mapper_for_model(obj).new(context).call(obj)
30
29
  end
31
- )
30
+ }
31
+
32
+ attrs[ :members_rel ] = members_rel if members_rel
33
+
34
+ CollectionResource.new(attrs)
32
35
  end
33
36
 
34
37
  private
35
38
 
36
39
  def members_rel
37
- policy.expand_rel( 'collection', pluralize( collection_type ) )
40
+ policy.expand_rel( 'collection', pluralize( collection_type ) ) if collection_type
38
41
  end
39
42
 
40
43
  def collection_type
data/lib/yaks/config.rb CHANGED
@@ -88,7 +88,8 @@ module Yaks
88
88
  # Yaks::Resource => serialized structure
89
89
  # serialized structure => serialized flat
90
90
 
91
- def call(object, opts = {}, env = {})
91
+ def call(object, opts = {})
92
+ env = opts.fetch(:env, {})
92
93
  context = {
93
94
  policy: policy,
94
95
  env: env
@@ -38,7 +38,7 @@ module Yaks
38
38
  end
39
39
 
40
40
  def derive_mapper_from_association(association)
41
- @options[:namespace].const_get("#{camelize(singularize(association.name.to_s))}Mapper")
41
+ @options[:namespace].const_get("#{camelize(association.singular_name)}Mapper")
42
42
  end
43
43
 
44
44
  def derive_rel_from_association(mapper, association)
@@ -1,7 +1,10 @@
1
1
  module Yaks
2
2
  class Mapper
3
3
  class HasMany < Association
4
+ include Util
5
+
4
6
  def map_resource(collection, context)
7
+ return NullResource.new(collection: true) if collection.nil?
5
8
  policy = context.fetch(:policy)
6
9
  member_mapper = association_mapper(policy)
7
10
  context = context.merge(member_mapper: member_mapper)
@@ -12,6 +15,10 @@ module Yaks
12
15
  return @collection_mapper unless @collection_mapper.equal? Undefined
13
16
  policy.derive_mapper_from_object(collection)
14
17
  end
18
+
19
+ def singular_name
20
+ singularize(name.to_s)
21
+ end
15
22
  end
16
23
  end
17
24
  end
@@ -6,6 +6,10 @@ module Yaks
6
6
  .new(context)
7
7
  .call(object)
8
8
  end
9
+
10
+ def singular_name
11
+ name.to_s
12
+ end
9
13
  end
10
14
  end
11
15
  end
@@ -2,6 +2,10 @@ module Yaks
2
2
  class NullResource
3
3
  include Enumerable
4
4
 
5
+ def initialize(opts = {})
6
+ @collection = opts.fetch(:collection, false)
7
+ end
8
+
5
9
  def each
6
10
  return to_enum unless block_given?
7
11
  end
@@ -21,8 +25,11 @@ module Yaks
21
25
  def [](*)
22
26
  end
23
27
 
28
+ def type
29
+ end
30
+
24
31
  def collection?
25
- false
32
+ @collection
26
33
  end
27
34
  end
28
35
  end
@@ -29,7 +29,9 @@ module Yaks
29
29
 
30
30
  def serialize_links(subresources)
31
31
  subresources.each_with_object({}) do |(name, resource), hsh|
32
- hsh[pluralize(resource.type)] = serialize_link(resource)
32
+ next if resource.is_a? NullResource
33
+ key = resource.collection? ? pluralize(resource.type) : resource.type
34
+ hsh[key] = serialize_link(resource)
33
35
  end
34
36
  end
35
37
 
@@ -53,7 +55,7 @@ module Yaks
53
55
  def serialize_subresource(resource, linked)
54
56
  key = pluralize(resource.type)
55
57
  set = linked.fetch(key) { Set.new }
56
- linked = linked[key] = (set << serialize_resource(resource))
58
+ linked[key] = (set << serialize_resource(resource))
57
59
  serialize_linked_subresources(resource.subresources, linked)
58
60
  end
59
61
  end
data/lib/yaks/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Yaks
2
- VERSION = '0.4.0'
2
+ VERSION = '0.4.1'
3
3
  end
@@ -3,12 +3,12 @@ require 'spec_helper'
3
3
  require_relative './models'
4
4
 
5
5
  RSpec.shared_examples_for 'JSON output format' do |yaks, format, name|
6
- let(:input) { load_yaml_fixture name }
6
+ let(:input) { load_yaml_fixture(name) }
7
7
  let(:output) { load_json_fixture "#{name}.#{format}" }
8
8
 
9
9
  subject { yaks.serialize(input) }
10
10
 
11
- it { should eql output }
11
+ it { should deep_eql output }
12
12
  end
13
13
 
14
14
  RSpec.describe Yaks::Serializer::Hal do
@@ -22,8 +22,8 @@ RSpec.describe Yaks::Serializer::Hal do
22
22
  end
23
23
  end
24
24
 
25
- include_examples 'JSON output format' , yaks_rel_template , :hal , 'confucius'
26
- include_examples 'JSON output format' , yaks_policy_dsl , :hal , 'confucius'
25
+ include_examples 'JSON output format' , yaks_rel_template , :hal , 'confucius'
26
+ include_examples 'JSON output format' , yaks_policy_dsl , :hal , 'confucius'
27
27
  end
28
28
 
29
29
  RSpec.describe Yaks::Serializer::JsonApi do
@@ -5,7 +5,15 @@ class Scholar
5
5
  end
6
6
 
7
7
  class Work
8
- include Anima.new(:id, :chinese_name, :english_name)
8
+ include Anima.new(:id, :chinese_name, :english_name, :quotes, :era)
9
+ end
10
+
11
+ class Quote
12
+ include Anima.new(:id, :chinese, :english, :sources)
13
+ end
14
+
15
+ class Era
16
+ include Anima.new(:id, :name)
9
17
  end
10
18
 
11
19
  class LiteratureBaseMapper < Yaks::Mapper
@@ -25,4 +33,14 @@ end
25
33
 
26
34
  class WorkMapper < LiteratureBaseMapper
27
35
  attributes :id, :chinese_name, :english_name
36
+ has_many :quotes
37
+ has_one :era
38
+ end
39
+
40
+ class QuoteMapper < Yaks::Mapper
41
+ attributes :id, :chinese
42
+ end
43
+
44
+ class EraMapper < Yaks::Mapper
45
+ attributes :id, :name
28
46
  end
@@ -16,6 +16,35 @@
16
16
  "_links": {
17
17
  "self": { "href": "http://literature.example.com/work/11" },
18
18
  "profile": { "href": "http://literature.example.com/profiles/work" }
19
+ },
20
+ "_embedded": {
21
+ "http://literature.example.com/rel/quotes": [
22
+ {
23
+ "id": 17,
24
+ "chinese": "廄焚。子退朝,曰:“傷人乎?” 不問馬。"
25
+ },
26
+ {
27
+ "id": 18,
28
+ "chinese": "子曰:“其恕乎!己所不欲、勿施於人。”"
29
+ }
30
+ ],
31
+ "http://literature.example.com/rel/era": {
32
+ "id": 99,
33
+ "name": "Zhou Dynasty"
34
+ }
35
+ }
36
+ },
37
+ {
38
+ "id": 12,
39
+ "chinese_name": "易經",
40
+ "english_name": "Commentaries to the Yi-jing",
41
+ "_links": {
42
+ "self": { "href": "http://literature.example.com/work/12" },
43
+ "profile": { "href": "http://literature.example.com/profiles/work" }
44
+ },
45
+ "_embedded": {
46
+ "http://literature.example.com/rel/quotes": [],
47
+ "http://literature.example.com/rel/era": null
19
48
  }
20
49
  }
21
50
  ]
@@ -6,7 +6,7 @@
6
6
  "pinyin": "Kongzi",
7
7
  "latinized": "Confucius",
8
8
  "links": {
9
- "works": [11]
9
+ "works": [11,12]
10
10
  }
11
11
  }
12
12
  ],
@@ -15,7 +15,33 @@
15
15
  {
16
16
  "id": 11,
17
17
  "chinese_name": "論語",
18
- "english_name": "Analects"
18
+ "english_name": "Analects",
19
+ "links": {
20
+ "quotes": [17, 18],
21
+ "era": 99
22
+ }
23
+ },
24
+ {
25
+ "id": 12,
26
+ "chinese_name": "易經",
27
+ "english_name": "Commentaries to the Yi-jing",
28
+ "links": {}
29
+ }
30
+ ],
31
+ "quotes": [
32
+ {
33
+ "id": 17,
34
+ "chinese": "廄焚。子退朝,曰:“傷人乎?” 不問馬。"
35
+ },
36
+ {
37
+ "id": 18,
38
+ "chinese": "子曰:“其恕乎!己所不欲、勿施於人。”"
39
+ }
40
+ ],
41
+ "erae": [
42
+ {
43
+ "id": 99,
44
+ "name": "Zhou Dynasty"
19
45
  }
20
46
  ]
21
47
  }
data/spec/spec_helper.rb CHANGED
@@ -12,6 +12,7 @@ require_relative 'support/friends_mapper'
12
12
  require_relative 'support/fixtures'
13
13
  require_relative 'support/shared_contexts'
14
14
  require_relative 'support/youtypeit_models_mappers'
15
+ require_relative 'support/deep_eql'
15
16
 
16
17
 
17
18
  RSpec.configure do |rspec|
@@ -0,0 +1,110 @@
1
+ # When comparing deep nested structures, it can be really hard to figure out what
2
+ # the actual differences are looking at the RSpec output. This custom matcher
3
+ # traverses nested hashes and arrays recursively, and reports each difference
4
+ # separately, with a JSONPath string of where the difference was found
5
+ #
6
+ # e.g.
7
+ #
8
+ # at $.shows[0].venues[0].name, got Foo, expected Bar
9
+
10
+ module Matchers
11
+ class DeepEql
12
+ extend Forwardable
13
+ attr_reader :expectation, :stack, :target, :diffs, :result
14
+ def_delegators :stack, :push, :pop
15
+
16
+ def initialize(expectation, stack = [], diffs = [])
17
+ @expectation = expectation
18
+ @stack = stack
19
+ @diffs = diffs
20
+ @result = true
21
+ end
22
+
23
+ def description
24
+ 'be deeply equal'
25
+ end
26
+
27
+ def recurse(target, expectation)
28
+ @result &&= DeepEql.new(expectation, stack, diffs).matches?(target)
29
+ end
30
+
31
+ def stack_as_jsonpath
32
+ '$' + stack.map do |item|
33
+ case item
34
+ when Integer, /\W/
35
+ "[#{item.inspect}]"
36
+ else
37
+ ".#{item}"
38
+ end
39
+ end.join
40
+ end
41
+
42
+ def failure_message(message)
43
+ diffs << "at %s, %s" % [stack_as_jsonpath, message]
44
+ @result = false
45
+ end
46
+
47
+ def compare(key)
48
+ push key
49
+ if target[key] != expectation[key]
50
+ if [Hash, Array].any?{|klz| target[key].is_a? klz }
51
+ recurse(target[key], expectation[key])
52
+ else
53
+ failure_message begin
54
+ if expectation[key].class == target[key].class
55
+ "expected #{expectation[key].inspect}, got #{target[key].inspect}"
56
+ else
57
+ "expected #{expectation[key].class}: #{expectation[key].inspect}, got #{target[key].class}: #{target[key].inspect}"
58
+ end
59
+ rescue Encoding::CompatibilityError
60
+ "expected #{expectation[key].encoding}, got #{target[key].encoding}"
61
+ end
62
+ end
63
+ end
64
+ pop
65
+ end
66
+
67
+ def matches?(target)
68
+ @target = target
69
+
70
+ case expectation
71
+ when Hash
72
+ if target.is_a?(Hash)
73
+ if target.class != expectation.class # e.g. HashWithIndifferentAccess
74
+ failure_message("expected #{expectation.class}, got #{target.class}")
75
+ end
76
+ (target.keys | expectation.keys).each do |key|
77
+ compare key
78
+ end
79
+ else
80
+ failure_message("expected Hash got #{@target.inspect}")
81
+ end
82
+
83
+ when Array
84
+ if target.is_a?(Array)
85
+ 0.upto([target.count, expectation.count].max) do |idx|
86
+ compare idx
87
+ end
88
+ else
89
+ failure_message("expected Array got #{@target.inspect}")
90
+ end
91
+ end
92
+
93
+ result
94
+ end
95
+
96
+ def failure_message_for_should
97
+ diffs.join("\n")
98
+ end
99
+
100
+ def failure_message_for_should_not
101
+ "expected #{@target.inspect} not to be in deep_eql with #{@expectation.inspect}"
102
+ end
103
+ end
104
+ end
105
+
106
+ module RSpec::Matchers
107
+ def deep_eql(exp)
108
+ Matchers::DeepEql.new(exp)
109
+ end
110
+ end
@@ -8,3 +8,21 @@ works:
8
8
  id: 11
9
9
  chinese_name: "論語"
10
10
  english_name: "Analects"
11
+ era: !ruby/object:Era
12
+ id: 99
13
+ name: "Zhou Dynasty"
14
+ quotes:
15
+ - !ruby/object:Quote
16
+ id: 17
17
+ chinese: "廄焚。子退朝,曰:“傷人乎?” 不問馬。"
18
+ english: "When the stables were burnt down, on returning from court Confucius said, “Was anyone hurt?” He did not ask about the horses."
19
+ sources: "Analects X.11 (tr. Waley), 10–13 (tr. Legge), or X-17 (tr. Lau)"
20
+ - !ruby/object:Quote
21
+ id: 18
22
+ chinese: "子曰:“其恕乎!己所不欲、勿施於人。”"
23
+ english: "The Master replied: “How about 'reciprocity'! Never impose on others what you would not choose for yourself.”"
24
+ sources: "Analects XV.24, tr. David Hinton"
25
+ - !ruby/object:Work
26
+ id: 12
27
+ chinese_name: "易經"
28
+ english_name: "Commentaries to the Yi-jing"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yaks
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Arne Brasseur
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-06-17 00:00:00.000000000 Z
11
+ date: 2014-06-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: inflection
@@ -219,6 +219,7 @@ files:
219
219
  - spec/json/john.hal.json
220
220
  - spec/json/youtypeitwepostit.collection.json
221
221
  - spec/spec_helper.rb
222
+ - spec/support/deep_eql.rb
222
223
  - spec/support/fixtures.rb
223
224
  - spec/support/friends_mapper.rb
224
225
  - spec/support/models.rb
@@ -280,6 +281,7 @@ test_files:
280
281
  - spec/json/john.hal.json
281
282
  - spec/json/youtypeitwepostit.collection.json
282
283
  - spec/spec_helper.rb
284
+ - spec/support/deep_eql.rb
283
285
  - spec/support/fixtures.rb
284
286
  - spec/support/friends_mapper.rb
285
287
  - spec/support/models.rb