yaks 0.4.1 → 0.4.2

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: 40c42f259955a15a723d0782ab4378201bf511b3
4
- data.tar.gz: 360a9de28a43b27cf820c26aec19c6627a7dac2b
3
+ metadata.gz: 0b888259337e9fa643458e613fda2f50e561a72e
4
+ data.tar.gz: e6ff86a9a5d402b857041f1a676b81f6324078d7
5
5
  SHA512:
6
- metadata.gz: 5fb54a5b204c60f385aa4b8e4915c6fce1c89dcb1340b48323242c4ade3a4265d44a5ff37f5d587302cab90ad6e05003e160e290fe69b36f19285934d94f3faf
7
- data.tar.gz: 5d4462422ddd28e6106846a8663fbdb43bf2ad33b302c24bdcc0f424edae60d1774455d0f70cd3041422aafdd6d0d6ee697558942026ac1bd84d66f149816748
6
+ metadata.gz: edfa665288d44ed62d397beeb2419092b7220129475c77f5cbce4413a981f1d57f44578faf27e3a77da3c6a818a3b5bc0f5bbeaeaa966aeab82d55f6b1960694
7
+ data.tar.gz: 7391bc4fab6497ce52c6353db032786df91ab01730cd8486aed53179321ebd3c9baf49a54509edbcddd14796b1daefecc98903b0bfd8b9fe94753cb4fe2f1490
data/.travis.yml CHANGED
@@ -1,16 +1,25 @@
1
1
  language: ruby
2
- script: "bundle exec rspec"
2
+ script: bundle exec $RUN
3
3
  rvm:
4
4
  - 1.9.3
5
5
  - 2.0.0
6
- - 2.1.1
7
- - rbx
8
- - jruby
6
+ - 2.1.2
9
7
  - ruby-head
8
+ - rbx-2
9
+ - jruby
10
10
  - jruby-head
11
+ env:
12
+ - RUN=rspec
13
+ - RUN=rake
11
14
  matrix:
12
15
  allow_failures:
13
- - rvm: rbx
14
- - rvm: jruby
15
16
  - rvm: ruby-head
16
17
  - rvm: jruby-head
18
+ - env: RUN=rake
19
+ exclude:
20
+ - rvm: jruby
21
+ env: RUN=rake
22
+ - rvm: jruby-head
23
+ env: RUN=rake
24
+ - rvm: rbx-2
25
+ env: RUN=rake
data/ADDING_FORMATS.md ADDED
@@ -0,0 +1,13 @@
1
+ # Adding Extra Output Formats to Yaks
2
+
3
+ Individual output formats are each handled by a dedicated `Yaks::Serializer` class. These take a `Yaks::Resource` as input, and turn it into the requested output format.
4
+
5
+ A `Yaks::Resource` is created by "mapping" domain models by a `Yaks::Mapper`. In a `Yaks::Mapper` subclass a DSL is available to specify how to extract different types of information, for example attributes or links, and store them in a generalized way in a `Resource`.
6
+
7
+ Different formats have different features. Simple formats might just represent attributes, links, and subresources, other formats have queries, forms, or RDF identifiers. If a format represents data of a different nature, then the first step is to decide on a good and straightforward syntax to specify how to derive this data. This can then be stored in a `Yaks::Resource`, and formats that support it can use it, other formats can ignore it.
8
+
9
+ This is already the case, JSON-API ignores links for example.
10
+
11
+ So adding an output format is generally straightforward, as long as the information that the output format supports is already available in `Yaks::Resource`. In that case adding a `Yaks::Serializer::YourFormat` is all that is needed.
12
+
13
+ If the format has features that are not yet available then syntax needs to be added for those features. The guiding idea there is to try and find more than one format with the given feature, to make sure the intermediate abstraction is general and not tied to the specifics and vocabulary of a single format.
data/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ### Development
2
+ [full changelog](http://github.com/plexus/yaks/compare/v0.4.2...master)
3
+
4
+ ### v0.4.2
5
+
6
+ * JSON-API: render self links as href attributes
7
+ * HAL: render has_one returning nil as null, not as {}
8
+ * Keep track of the mapper stack, useful for figuring out if mapping the top level response or not, or for accessing parent
9
+ * Change Serializer.new(resource, options).serialize to Serializer.new(options).call(resource) for cosistency of "pipeline" interface
10
+ * Make Yaks::CollectionMapper#collection overridable for pagination
11
+ * Don't render links from custom link methods (link :foo, :method_that_generates_url) that return nil
12
+
1
13
  # v0.4.1
2
14
 
3
15
  * Change how env is passed to yaks.serialize to match docs
data/IDENTIFIERS.md ADDED
@@ -0,0 +1,113 @@
1
+ # Identifiers
2
+
3
+ In Yaks, and Hypermedia message formats in general, a number of different types of identifiers are used. Some are full URIs and correspond with well defined specs. Some are just short identifers that are easy to program with.
4
+
5
+ Understanding these types of identifiers is key to creating a unifying model of a "Resource" that can be shared across output formats. We want to unify as much as possible across formats, without conflating things that are really not the same.
6
+
7
+ This document reflects my current limited understanding of things, based on possibly incorrect assumptions. Feedback is more than welcome.
8
+
9
+ ## rels
10
+
11
+ As used in HTML and Atom, these identifiers say what the relationship is between a resource and another resource it links to. There is a [registry of names](http://www.iana.org/assignments/link-relations/link-relations.xhtml), e.g. self, next, profile, stylesheet. Custom rels need to be fully qualified URLs. Keep in mind that these are simply opaque identifiers, but by using a known protocol like http they can be used to point at documentation.
12
+
13
+ Some examples
14
+
15
+ ```
16
+ copyright
17
+ stylesheet
18
+ http://api.example.com/rel/author
19
+ http://api.example.com/api-docs/relationships#comment
20
+ custom_scheme:foo
21
+ /order
22
+ ```
23
+
24
+ The last example is a relative URL, which would have to be expanded against the source URL of the document it is mentioned in.
25
+
26
+ In Yaks both links and subresources are specified with their rel(ationship).
27
+
28
+ ```ruby
29
+ class PersonMapper < Yaks::Mapper
30
+ link :self, '/people/{id}'
31
+ link 'http://api.example.com/rels#friends', '/people/{id}/friends'
32
+
33
+ has_one :address, rel: 'http://api.example.com/rels#address'
34
+ end
35
+ ```
36
+
37
+ For subresources the rel can be omitted, in which case it will be inferred based on the rel_template:
38
+
39
+ ```ruby
40
+ $yaks = Yaks.new do
41
+ rel_template 'http://api.example.com/rels/{dest}'
42
+ end
43
+ ```
44
+
45
+ Links and subresources are rendered keyed by rel in HAL and Collection+JSON. JSON-API renders `self` links as the `href` of a resource.
46
+
47
+ ## profiles
48
+
49
+ A specific IANA registered rel type is profile.
50
+
51
+ > Profile: Identifying that a resource representation conforms to a certain profile, without affecting the non-profile semantics of the resource representation.
52
+
53
+ Profile basically adds a layer of semantics on top of the hypermedia message format (e.g. HAL, Collection+JSON), which in turns defines semantics on top of a serialization format (JSON, XML, EDN). Loosely speaking it could be seen as the "type" or "class". For example if you know the profile of a resource, you might know you can expect to find a "name", "date_of_birth", or "post_body" field.
54
+
55
+ ## "type"
56
+
57
+ Despite the appealing rigor of having fully qualified URIs to identify things, sometimes you just want to call a person a `person`. In Yaks we call these short identifier the *type* for lack of a better word. In some cases, notably JSON-API, they are used literally in the output. More often they are used to derive full URIs based on a template.
58
+
59
+ The type of a mapper is inferred from its class name, but can be set explicitly as well.
60
+
61
+ ```ruby
62
+ class CatMapper < Yaks::Mapper
63
+ end
64
+
65
+ # type = "cat"
66
+ ```
67
+
68
+ ```ruby
69
+ class CatMapper < Yaks::Mapper
70
+ type 'feline'
71
+ end
72
+
73
+ # type => "feline"
74
+ ```
75
+
76
+ ## rdf class
77
+
78
+ RDF (Resource Description Framework) is a set of specifications for use in "semantic web" applications. RDF is based on "ontologies" that precisely define a "vocabulary" of "classes" and "predicates". An example class identifier for all Merlot wines could be
79
+
80
+ > http://www.w3.org/TR/2004/REC-owl-guide-20040210/wine#Merlot
81
+
82
+ (source [wikipedia](http://en.wikipedia.org/wiki/Resource_Description_Framework))
83
+
84
+ Not currently used by Yaks, but might become important when implementing support for JSON-LD or other RDF serialization formats.
85
+
86
+ ## CURIES
87
+
88
+ CURIES are "compact uris". The HAL format uses this so it can have the rigor of fully specified rels, with the ease of use of short-name "type" identifiers. The mechanism is similar to how one specifies and uses XML namespaces.
89
+
90
+ From the HAL spec:
91
+
92
+ ```json
93
+ {
94
+ "_links": {
95
+ "self": { "href": "/orders" },
96
+ "curies": [{ "name": "ea", "href": "http://example.com/docs/rels/{rel}", "templated": true }],
97
+ "next": { "href": "/orders?page=2" },
98
+ "ea:find": {
99
+ "href": "/orders{?id}",
100
+ "templated": true
101
+ },
102
+ "ea:admin": [{
103
+ "href": "/admins/2",
104
+ "title": "Fred"
105
+ }, {
106
+ "href": "/admins/5",
107
+ "title": "Kate"
108
+ }]
109
+ }
110
+ }
111
+ ```
112
+
113
+ In this case "ea:find" is just a shorthand for "http://example.com/docs/rels/find".
@@ -2,7 +2,6 @@
2
2
 
3
3
  module Yaks
4
4
  class CollectionMapper < Mapper
5
- attr_reader :collection
6
5
  alias collection object
7
6
 
8
7
  def initialize(context)
@@ -24,7 +23,7 @@ module Yaks
24
23
  type: collection_type,
25
24
  links: map_links,
26
25
  attributes: map_attributes,
27
- members: collection.map do |obj|
26
+ members: collection().map do |obj|
28
27
  mapper_for_model(obj).new(context).call(obj)
29
28
  end
30
29
  }
@@ -44,7 +44,7 @@ module Yaks
44
44
  #
45
45
  # :(
46
46
  def subresources
47
- if members.any?
47
+ if any?
48
48
  { members_rel => self }
49
49
  else
50
50
  {}
data/lib/yaks/config.rb CHANGED
@@ -70,10 +70,10 @@ module Yaks
70
70
  def serializer_class(opts, env)
71
71
  if env.key? 'HTTP_ACCEPT'
72
72
  accept = Rack::Accept::Charset.new(env['HTTP_ACCEPT'])
73
- mime_type = accept.best_of(Yaks::Serializer.mime_types.values)
74
- return Yaks::Serializer.by_mime_type(mime_type) if mime_type
73
+ mime_type = accept.best_of(Serializer.mime_types.values)
74
+ return Serializer.by_mime_type(mime_type) if mime_type
75
75
  end
76
- Yaks::Serializer.by_name(opts.fetch(:format) { @default_format })
76
+ Serializer.by_name(opts.fetch(:format) { @default_format })
77
77
  end
78
78
 
79
79
  def format_name(opts)
@@ -92,12 +92,14 @@ module Yaks
92
92
  env = opts.fetch(:env, {})
93
93
  context = {
94
94
  policy: policy,
95
- env: env
95
+ env: env,
96
+ mapper_stack: []
96
97
  }
97
- mapper = opts.fetch(:mapper) { policy.derive_mapper_from_object(object) }
98
- resource = mapper.new(context).call(object)
99
- serialized = serializer_class(opts, env).new(resource, format_options[format_name(opts)]).call
100
- steps.inject(serialized) {|memo, step| step.call(memo) }
98
+
99
+ mapper = opts.fetch(:mapper) { policy.derive_mapper_from_object(object) }.new(context)
100
+ serializer = serializer_class(opts, env).new(format_options[format_name(opts)])
101
+
102
+ [ mapper, serializer, *steps ].inject(object) {|memo, step| step.call(memo) }
101
103
  end
102
104
  alias serialize call
103
105
  end
@@ -63,11 +63,10 @@ module Yaks
63
63
  end
64
64
 
65
65
  def map_to_resource_link(mapper)
66
- Resource::Link.new(
67
- rel,
68
- expand_with(mapper.method(:load_attribute)),
69
- resource_link_options(mapper)
70
- )
66
+ uri = expand_with(mapper.method(:load_attribute))
67
+ return if uri.nil?
68
+
69
+ Resource::Link.new(rel, uri, resource_link_options(mapper))
71
70
  end
72
71
 
73
72
  def expand_with(lookup)
data/lib/yaks/mapper.rb CHANGED
@@ -22,6 +22,10 @@ module Yaks
22
22
  context.fetch(:env)
23
23
  end
24
24
 
25
+ def mapper_stack
26
+ context.fetch(:mapper_stack)
27
+ end
28
+
25
29
  def call(object)
26
30
  @object = object
27
31
 
@@ -42,7 +46,7 @@ module Yaks
42
46
  end
43
47
 
44
48
  def map_links
45
- links.map &send_with_args(:map_to_resource_link, self)
49
+ links.map(&send_with_args(:map_to_resource_link, self)).compact
46
50
  end
47
51
 
48
52
  def map_subresources
@@ -52,7 +56,7 @@ module Yaks
52
56
  rel, subresource = association.create_subresource(
53
57
  self,
54
58
  method(:load_association),
55
- context
59
+ context.merge(mapper_stack: mapper_stack + [self])
56
60
  )
57
61
  memo[rel] = subresource
58
62
  end
@@ -3,11 +3,11 @@ module Yaks
3
3
  include Enumerable
4
4
 
5
5
  def initialize(opts = {})
6
- @collection = opts.fetch(:collection, false)
6
+ @collection = opts.fetch(:collection) { false }
7
7
  end
8
8
 
9
9
  def each
10
- return to_enum unless block_given?
10
+ to_enum
11
11
  end
12
12
 
13
13
  def attributes
@@ -31,5 +31,9 @@ module Yaks
31
31
  def collection?
32
32
  @collection
33
33
  end
34
+
35
+ def null_resource?
36
+ true
37
+ end
34
38
  end
35
39
  end
@@ -37,7 +37,7 @@ module Yaks
37
37
  end
38
38
 
39
39
  p.map Enumerable do |object|
40
- object.map(&method(:call)).to_a
40
+ object.map(&method(:call))
41
41
  end
42
42
  end
43
43
  end
data/lib/yaks/resource.rb CHANGED
@@ -24,15 +24,18 @@ module Yaks
24
24
  yield self
25
25
  end
26
26
 
27
- def collection?
28
- false
29
- end
30
-
31
27
  def self_link
32
28
  links.find do |link|
33
- link.rel == :self
29
+ link.rel.equal? :self
34
30
  end
35
31
  end
36
32
 
33
+ def collection?
34
+ false
35
+ end
36
+
37
+ def null_resource?
38
+ false
39
+ end
37
40
  end
38
41
  end
@@ -45,7 +45,7 @@ module Yaks
45
45
  memo[rel] = if resources.collection?
46
46
  resources.map( &method(:serialize_resource) )
47
47
  else
48
- serialize_resource(resources)
48
+ serialize_resource(resources) unless resources.null_resource?
49
49
  end
50
50
  end
51
51
  end
@@ -7,23 +7,28 @@ module Yaks
7
7
 
8
8
  include FP
9
9
 
10
- def call
11
- serialized = {
12
- pluralize(resource.type) => resource.map(&method(:serialize_resource))
13
- }
10
+ def call(resource)
11
+ main_collection = resource.map(&method(:serialize_resource))
14
12
 
15
- linked = resource.each_with_object({}) do |res, hsh|
16
- serialize_linked_subresources(res.subresources, hsh)
13
+ { pluralize(resource.type) => main_collection }.tap do |serialized|
14
+ linked = resource.each_with_object({}) do |res, hsh|
15
+ serialize_linked_subresources(res.subresources, hsh)
16
+ end
17
+ serialized.merge!(linked: linked) unless linked.empty?
17
18
  end
18
- serialized = serialized.merge('linked' => linked)
19
-
20
- serialized
21
19
  end
22
- alias serialize call
23
20
 
24
21
  def serialize_resource(resource)
25
22
  result = resource.attributes
26
- result = result.merge(:links => serialize_links(resource.subresources)) unless resource.subresources.empty?
23
+
24
+ unless resource.subresources.empty?
25
+ result[:links] = serialize_links(resource.subresources)
26
+ end
27
+
28
+ if resource.self_link && !result.key?(:href)
29
+ result[:href] = resource.self_link.uri
30
+ end
31
+
27
32
  result
28
33
  end
29
34
 
@@ -3,17 +3,16 @@ module Yaks
3
3
  extend Forwardable
4
4
  include Util
5
5
 
6
- attr_reader :resource, :options
6
+ attr_reader :options
7
7
  def_delegators :resource, :links, :attributes, :subresources
8
8
 
9
- protected :resource, :links, :attributes, :subresources, :options
9
+ protected :links, :attributes, :subresources, :options
10
10
 
11
- def initialize(resource, options = {})
12
- @resource = resource
11
+ def initialize(options = {})
13
12
  @options = YAKS_DEFAULT_OPTIONS.merge(options)
14
13
  end
15
14
 
16
- def call
15
+ def call(resource)
17
16
  serialize_resource(resource)
18
17
  end
19
18
  alias serialize call
data/lib/yaks/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Yaks
2
- VERSION = '0.4.1'
2
+ VERSION = '0.4.2'
3
3
  end
@@ -4,7 +4,7 @@ require_relative './models'
4
4
 
5
5
  RSpec.shared_examples_for 'JSON output format' do |yaks, format, name|
6
6
  let(:input) { load_yaml_fixture(name) }
7
- let(:output) { load_json_fixture "#{name}.#{format}" }
7
+ let(:output) { load_json_fixture("#{name}.#{format}") }
8
8
 
9
9
  subject { yaks.serialize(input) }
10
10
 
@@ -24,7 +24,9 @@ end
24
24
  class ScholarMapper < LiteratureBaseMapper
25
25
  attributes :id, :name, :pinyin, :latinized
26
26
  has_many :works
27
- link :self, "http://literature.example.com/authors/{downcased_pinyin}"
27
+
28
+ link 'http://literature.example.com/rels/quotes', 'http://literature.example.com/quotes/?author={downcased_pinyin}&q={query}', expand: [:downcased_pinyin]
29
+ link :self, 'http://literature.example.com/authors/{downcased_pinyin}'
28
30
 
29
31
  def downcased_pinyin
30
32
  object.pinyin.downcase
@@ -2,9 +2,10 @@ require 'spec_helper'
2
2
 
3
3
  RSpec.describe 'Mapping domain models to Resource objects' do
4
4
  include_context 'fixtures'
5
+ include_context 'yaks context'
5
6
 
6
7
  subject { mapper.call(john) }
7
- let(:mapper) { FriendMapper.new(policy: Yaks::DefaultPolicy.new, env: {}) }
8
+ let(:mapper) { FriendMapper.new(yaks_context) }
8
9
 
9
10
  it { should be_a Yaks::Resource }
10
11
  its(:type) { should eql 'friend' }
@@ -5,7 +5,11 @@
5
5
  "latinized": "Confucius",
6
6
  "_links": {
7
7
  "self": { "href": "http://literature.example.com/authors/kongzi" },
8
- "profile": { "href": "http://literature.example.com/profiles/scholar" }
8
+ "profile": { "href": "http://literature.example.com/profiles/scholar" },
9
+ "http://literature.example.com/rels/quotes": {
10
+ "href": "http://literature.example.com/quotes/?author=kongzi&q={query}",
11
+ "templated": true
12
+ }
9
13
  },
10
14
  "_embedded": {
11
15
  "http://literature.example.com/rel/works": [
@@ -2,6 +2,7 @@
2
2
  "scholars": [
3
3
  {
4
4
  "id": 9,
5
+ "href": "http://literature.example.com/scholar/9",
5
6
  "name": "孔子",
6
7
  "pinyin": "Kongzi",
7
8
  "latinized": "Confucius",
@@ -14,6 +15,7 @@
14
15
  "works": [
15
16
  {
16
17
  "id": 11,
18
+ "href": "http://literature.example.com/work/11",
17
19
  "chinese_name": "論語",
18
20
  "english_name": "Analects",
19
21
  "links": {
@@ -23,6 +25,7 @@
23
25
  },
24
26
  {
25
27
  "id": 12,
28
+ "href": "http://literature.example.com/work/12",
26
29
  "chinese_name": "易經",
27
30
  "english_name": "Commentaries to the Yi-jing",
28
31
  "links": {}
@@ -45,6 +45,7 @@ module Matchers
45
45
  end
46
46
 
47
47
  def compare(key)
48
+ #require 'pry' ; binding.pry
48
49
  push key
49
50
  if target[key] != expectation[key]
50
51
  if [Hash, Array].any?{|klz| target[key].is_a? klz }
@@ -88,6 +89,11 @@ module Matchers
88
89
  else
89
90
  failure_message("expected Array got #{@target.inspect}")
90
91
  end
92
+
93
+ else
94
+ if target != expectation
95
+ failure_message("expected #{expectation.inspect}, got #{@target.inspect}")
96
+ end
91
97
  end
92
98
 
93
99
  result
@@ -1,4 +1,4 @@
1
- shared_context 'collection resource' do
1
+ RSpec.shared_context 'collection resource' do
2
2
  let(:resource) do
3
3
  Yaks::CollectionResource.new(
4
4
  links: links,
@@ -10,7 +10,14 @@ shared_context 'collection resource' do
10
10
  let(:members) { [] }
11
11
  end
12
12
 
13
- shared_context 'plant collection resource' do
13
+ RSpec.shared_context 'yaks context' do
14
+ let(:policy) { Yaks::DefaultPolicy.new }
15
+ let(:rack_env) { {} }
16
+ let(:mapper_stack) { [] }
17
+ let(:yaks_context) { { policy: policy, env: rack_env, mapper_stack: mapper_stack } }
18
+ end
19
+
20
+ RSpec.shared_context 'plant collection resource' do
14
21
  include_context 'collection resource'
15
22
 
16
23
  let(:links) { [ plants_self_link, plants_profile_link ] }
@@ -3,11 +3,14 @@ require 'spec_helper'
3
3
  RSpec.describe Yaks::CollectionMapper do
4
4
  include_context 'fixtures'
5
5
 
6
- subject(:mapper) { described_class.new(context) }
6
+ subject(:mapper) { mapper_class.new(context) }
7
+ let(:mapper_class) { described_class }
8
+
7
9
  let(:context) {
8
10
  { member_mapper: member_mapper,
9
11
  policy: policy,
10
- env: {}
12
+ env: {},
13
+ mapper_stack: []
11
14
  }
12
15
  }
13
16
  let(:collection) { [] }
@@ -29,8 +32,6 @@ RSpec.describe Yaks::CollectionMapper do
29
32
  let(:member_mapper) { PetMapper }
30
33
 
31
34
  it 'should map the members' do
32
- mapper.call(collection)
33
-
34
35
  expect(mapper.call(collection)).to eql Yaks::CollectionResource.new(
35
36
  type: 'pet',
36
37
  links: [],
@@ -87,4 +88,31 @@ RSpec.describe Yaks::CollectionMapper do
87
88
  end
88
89
  end
89
90
 
91
+ describe 'overriding #collection' do
92
+ let(:mapper_class) do
93
+ Class.new(described_class) do
94
+ type 'pet'
95
+
96
+ def collection
97
+ super.drop(1)
98
+ end
99
+ end
100
+ end
101
+
102
+ let(:collection) { [boingboing, wassup]}
103
+ let(:member_mapper) { PetMapper }
104
+
105
+ it 'should use the redefined collection method' do
106
+ expect(mapper.call(collection)).to eql Yaks::CollectionResource.new(
107
+ type: 'pet',
108
+ links: [],
109
+ attributes: {},
110
+ members: [
111
+ Yaks::Resource.new(type: 'pet', attributes: {:id => 3, :species => "cat", :name => "wassup"})
112
+ ],
113
+ members_rel: 'rel:src=collection&dest=pets'
114
+ )
115
+ end
116
+ end
117
+
90
118
  end
@@ -4,7 +4,8 @@ RSpec.describe Yaks::CollectionResource do
4
4
  subject(:collection) { described_class.new(init_opts) }
5
5
  let(:init_opts) { {} }
6
6
 
7
- its(:collection?) { should equal true }
7
+ its(:collection?) { should be true }
8
+ its(:null_resource?) { should be false }
8
9
 
9
10
  context 'with nothing passed in the contstructor' do
10
11
  its(:type) { should be_nil }
@@ -12,6 +13,7 @@ RSpec.describe Yaks::CollectionResource do
12
13
  its(:attributes) { should eql({}) }
13
14
  its(:members) { should eql [] }
14
15
  its(:subresources) { should eql({}) }
16
+ its(:members_rel) { should eql('members') }
15
17
  end
16
18
 
17
19
  context 'with a full constructor' do
@@ -29,7 +31,8 @@ RSpec.describe Yaks::CollectionResource do
29
31
  links: [Yaks::Resource::Link.new(:self, 'http://order/10', {})],
30
32
  attributes: { customer: 'John Doe', price: 10.00 }
31
33
  )
32
- ]
34
+ ],
35
+ members_rel: 'http://api.example.org/rels/orders'
33
36
  }
34
37
  }
35
38
 
@@ -48,9 +51,10 @@ RSpec.describe Yaks::CollectionResource do
48
51
  )
49
52
  ]
50
53
  }
54
+ its(:members_rel) { should eq 'http://api.example.org/rels/orders'}
51
55
 
52
56
  its(:subresources) { should eql(
53
- 'members' => Yaks::CollectionResource.new(
57
+ 'http://api.example.org/rels/orders' => Yaks::CollectionResource.new(
54
58
  type: 'order',
55
59
  attributes: { total: 10.00 },
56
60
  links: [
@@ -64,6 +68,7 @@ RSpec.describe Yaks::CollectionResource do
64
68
  attributes: { customer: 'John Doe', price: 10.00 }
65
69
  )
66
70
  ],
71
+ members_rel: 'http://api.example.org/rels/orders'
67
72
  )
68
73
  )
69
74
  }
@@ -1,7 +1,11 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  RSpec.describe Yaks::Mapper::HasMany do
4
- let(:closet_mapper) do
4
+ include_context 'yaks context'
5
+
6
+ let(:closet_mapper) { closet_mapper_class.new(yaks_context) }
7
+
8
+ let(:closet_mapper_class) do
5
9
  Class.new(Yaks::Mapper) do
6
10
  type 'closet'
7
11
  has_many :shoes,
@@ -10,6 +14,10 @@ RSpec.describe Yaks::Mapper::HasMany do
10
14
  end
11
15
  end
12
16
 
17
+ subject(:shoe_association) { closet_mapper.associations.first }
18
+
19
+ its(:singular_name) { should eq 'shoe' }
20
+
13
21
  let(:closet) {
14
22
  double(
15
23
  :shoes => [
@@ -20,7 +28,7 @@ RSpec.describe Yaks::Mapper::HasMany do
20
28
  }
21
29
 
22
30
  it 'should map the subresources' do
23
- expect(closet_mapper.new(policy: Yaks::DefaultPolicy.new, env: {}).call(closet).subresources).to eql(
31
+ expect(closet_mapper.call(closet).subresources).to eql(
24
32
  "http://foo/shoes" => Yaks::CollectionResource.new(
25
33
  type: 'shoe',
26
34
  members: [
@@ -3,9 +3,9 @@ require 'spec_helper'
3
3
  RSpec.describe Yaks::Mapper::HasOne do
4
4
  AuthorMapper = Class.new(Yaks::Mapper) { attributes :name }
5
5
 
6
+ subject(:has_one) { described_class.new(:author, mapper, 'http://rel', Yaks::Undefined) }
6
7
  let(:name) { 'William S. Burroughs' }
7
8
  let(:mapper) { AuthorMapper }
8
- let(:has_one) { described_class.new(:author, mapper, 'http://rel', Yaks::Undefined) }
9
9
  let(:author) { double(:name => name) }
10
10
  let(:policy) {
11
11
  double(
@@ -16,6 +16,8 @@ RSpec.describe Yaks::Mapper::HasOne do
16
16
  }
17
17
  let(:context) {{policy: policy, env: {}}}
18
18
 
19
+ its(:singular_name) { should eq 'author' }
20
+
19
21
  it 'should map to a single Resource' do
20
22
  expect(has_one.map_resource(author, context)).to eq Yaks::Resource.new(type: 'author', attributes: {name: name})
21
23
  end
@@ -1,6 +1,8 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  RSpec.describe Yaks::Mapper::Link do
4
+ include_context 'yaks context'
5
+
4
6
  subject(:link) { described_class.new(rel, template, options) }
5
7
 
6
8
  let(:rel) { :next }
@@ -11,8 +13,6 @@ RSpec.describe Yaks::Mapper::Link do
11
13
  its(:uri_template) { should eq URITemplate.new(template) }
12
14
  its(:expand?) { should be true }
13
15
 
14
- let(:policy) { Yaks::DefaultPolicy.new }
15
- let(:context) { { policy: policy, env: {} } }
16
16
 
17
17
  describe '#rel?' do
18
18
  it 'should return true if the relation matches' do
@@ -81,11 +81,11 @@ RSpec.describe Yaks::Mapper::Link do
81
81
 
82
82
  its(:rel) { should eq :next }
83
83
 
84
- let(:object) { Struct.new(:x,:y).new(3,4) }
84
+ let(:object) { Struct.new(:x, :y, :returns_nil).new(3, 4, nil) }
85
85
 
86
86
  let(:mapper) do
87
- Yaks::Mapper.new(context).tap do |mapper|
88
- mapper.call(object)
87
+ Yaks::Mapper.new(yaks_context).tap do |mapper|
88
+ mapper.call(object) # set @object
89
89
  end
90
90
  end
91
91
 
@@ -152,6 +152,13 @@ RSpec.describe Yaks::Mapper::Link do
152
152
  end
153
153
  end
154
154
 
155
+ context 'with a link generation method that returns nil' do
156
+ let(:template) { :returns_nil }
157
+
158
+ it 'should return nil' do
159
+ expect(resource_link).to be_nil
160
+ end
161
+ end
155
162
  end
156
163
 
157
164
  end
@@ -1,16 +1,17 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  RSpec.describe Yaks::Mapper do
4
- subject(:mapper) { mapper_class.new(context) }
4
+ include_context 'yaks context'
5
+
6
+ subject(:mapper) { mapper_class.new(yaks_context) }
5
7
  let(:resource) { mapper.call(instance) }
6
8
 
7
9
  let(:mapper_class) { Class.new(Yaks::Mapper) { type 'foo' } }
8
10
  let(:instance) { double(foo: 'hello', bar: 'world') }
9
- let(:policy) { nil }
10
- let(:options) { {} }
11
- let(:context) {{policy: policy, env: {}}}
12
11
 
13
- describe '#map_attributes' do
12
+ its(:env) { should equal rack_env }
13
+
14
+ context 'with attributes' do
14
15
  before do
15
16
  mapper_class.attributes :foo, :bar
16
17
  end
@@ -38,7 +39,7 @@ RSpec.describe Yaks::Mapper do
38
39
  end
39
40
  end
40
41
 
41
- describe '#map_links' do
42
+ context 'with links' do
42
43
  before do
43
44
  mapper_class.link :profile, 'http://foo/bar'
44
45
  end
@@ -73,7 +74,7 @@ RSpec.describe Yaks::Mapper do
73
74
  end
74
75
  end
75
76
 
76
- describe '#map_subresources' do
77
+ context 'with subresources' do
77
78
  let(:instance) { double(widget: widget) }
78
79
  let(:widget) { double(type: 'super_widget') }
79
80
  let(:widget_mapper) { Class.new(Yaks::Mapper) { type 'widget' } }
@@ -150,25 +151,23 @@ RSpec.describe Yaks::Mapper do
150
151
  end
151
152
  end
152
153
 
153
- describe '#load_attributes' do
154
- context 'when the mapper implements a method with the attribute name' do
155
- before do
156
- mapper_class.class_eval do
157
- attributes :fooattr, :bar
154
+ context 'when the mapper implements a method with the attribute name' do
155
+ before do
156
+ mapper_class.class_eval do
157
+ attributes :fooattr, :bar
158
158
 
159
- def fooattr
160
- "#{object.foo} my friend"
161
- end
159
+ def fooattr
160
+ "#{object.foo} my friend"
162
161
  end
163
162
  end
163
+ end
164
164
 
165
- it 'should get the attribute from the mapper' do
166
- expect(resource.attributes).to eq(fooattr: 'hello my friend', bar: 'world')
167
- end
165
+ it 'should get the attribute from the mapper' do
166
+ expect(resource.attributes).to eq(fooattr: 'hello my friend', bar: 'world')
168
167
  end
169
168
  end
170
169
 
171
- describe '#call' do
170
+ context 'with a nil subject' do
172
171
  it 'should return a NullResource when the subject is nil' do
173
172
  expect(mapper.call(nil)).to be_a Yaks::NullResource
174
173
  end
@@ -0,0 +1,32 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Yaks::NullResource do
4
+ subject(:null_resource) { described_class.new }
5
+
6
+ its(:attributes) { should eq Hash[] }
7
+ its(:links) { should eq [] }
8
+ its(:subresources) { should eq Hash[] }
9
+ its(:collection?) { should be false }
10
+ its(:null_resource?) { should be true }
11
+
12
+ it { should respond_to :[] }
13
+
14
+ its(:type) { should be_nil }
15
+
16
+ describe '#each' do
17
+ its(:each) { should be_a Enumerator }
18
+
19
+ it 'should not yield anything' do
20
+ null_resource.each { fail }
21
+ end
22
+ end
23
+
24
+ it 'should contain nothing' do
25
+ expect( null_resource[:key] ).to be_nil
26
+ end
27
+
28
+ context 'when a collection' do
29
+ subject(:null_resource) { described_class.new( collection: true ) }
30
+ its(:collection?) { should be true }
31
+ end
32
+ end
@@ -0,0 +1,77 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Yaks::Primitivize do
4
+ subject(:primitivizer) { described_class.create }
5
+
6
+ describe '.create' do
7
+ it 'should map String, true, false, nil, numbers to themselves' do
8
+ [
9
+ 'hello',
10
+ true,
11
+ false,
12
+ nil,
13
+ 100,
14
+ 99.99,
15
+ -95.33333
16
+ ].each do |object|
17
+ expect(primitivizer.call(object)).to eql object
18
+ end
19
+ end
20
+
21
+ it 'should stringify symbols' do
22
+ expect(primitivizer.call(:foo)).to eql 'foo'
23
+ end
24
+
25
+ it 'should recursively handle hashes' do
26
+ expect(primitivizer.call(
27
+ :foo => {:wassup => :friends, 123 => '456'}
28
+ )).to eql('foo' => {'wassup' => 'friends', 123 => '456'})
29
+ end
30
+
31
+ it 'should handle arrays recursively' do
32
+ expect(primitivizer.call(
33
+ [:foo, [:wassup, :friends], 123, '456']
34
+ )).to eql( ['foo', ['wassup', 'friends'], 123, '456'] )
35
+ end
36
+ end
37
+
38
+ describe '#call' do
39
+ require 'ostruct'
40
+
41
+ let(:funny_object) {
42
+ OpenStruct.new('a' => 'b')
43
+ }
44
+
45
+ it 'should raise an error when passed an unkown type' do
46
+ def funny_object.inspect
47
+ "I am funny"
48
+ end
49
+
50
+ expect { primitivizer.call(funny_object) }.to raise_error "don't know how to turn OpenStruct (I am funny) into a primitive"
51
+ end
52
+
53
+ context 'with custom mapping' do
54
+ require 'matrix'
55
+
56
+ let(:primitivizer) do
57
+ described_class.new.tap do |p|
58
+ p.map Vector do |vec|
59
+ vec.map do |i|
60
+ call(i)
61
+ end.to_a
62
+ end
63
+
64
+ p.map Symbol do |sym|
65
+ sym.to_s.length
66
+ end
67
+ end
68
+ end
69
+
70
+ it 'should evaluate in the context of primitivize' do
71
+ expect( primitivizer.call( Vector[:foo, :baxxx, :bazz] ) ).to eql( [3, 5, 4] )
72
+ end
73
+ end
74
+
75
+
76
+ end
77
+ end
@@ -0,0 +1,21 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Yaks::Resource::Link do
4
+ subject(:link) { described_class.new(rel, uri, options) }
5
+ let(:rel) { :foo_rel }
6
+ let(:uri) { 'http://api.example.org/rel/foo' }
7
+ let(:options) { {name: 'jimmy', title: 'mr. spectacular' } }
8
+
9
+ its(:rel) { should eql :foo_rel }
10
+ its(:uri) { should eql 'http://api.example.org/rel/foo' }
11
+ its(:options) { should eql(name: 'jimmy', title: 'mr. spectacular') }
12
+
13
+ its(:name) { should eql('jimmy') }
14
+ its(:title) { should eql('mr. spectacular') }
15
+ its(:templated?) { should be false }
16
+
17
+ context 'with explicit templated option' do
18
+ let(:options) { super().merge(templated: true) }
19
+ its(:templated?) { should be true }
20
+ end
21
+ end
@@ -4,10 +4,13 @@ RSpec.describe Yaks::Resource do
4
4
  subject(:resource) { described_class.new(init_opts) }
5
5
  let(:init_opts) { {} }
6
6
 
7
- its(:type) { should be_nil }
8
- its(:attributes) { should eql({}) }
9
- its(:links) { should eql [] }
10
- its(:subresources) { should eql({}) }
7
+ its(:type) { should be_nil }
8
+ its(:attributes) { should eql({}) }
9
+ its(:links) { should eql [] }
10
+ its(:subresources) { should eql({}) }
11
+ its(:self_link) { should be_nil }
12
+ its(:null_resource?) { should be false }
13
+ its(:collection?) { should be false }
11
14
 
12
15
  context 'with a type' do
13
16
  let(:init_opts) { { type: 'post' } }
@@ -23,16 +26,32 @@ RSpec.describe Yaks::Resource do
23
26
  end
24
27
 
25
28
  context 'with links' do
26
- let(:init_opts) { { links: [Yaks::Resource::Link.new(:self, '/foo/bar', {})] } }
27
- its(:links) { should eql [Yaks::Resource::Link.new(:self, '/foo/bar', {})] }
29
+ let(:init_opts) {
30
+ {
31
+ links: [
32
+ Yaks::Resource::Link.new(:profile, '/foo/bar/profile', {}),
33
+ Yaks::Resource::Link.new(:self, '/foo/bar', {})
34
+ ]
35
+ }
36
+ }
37
+ its(:links) { should eql [
38
+ Yaks::Resource::Link.new(:profile, '/foo/bar/profile', {}),
39
+ Yaks::Resource::Link.new(:self, '/foo/bar', {})
40
+ ]
41
+ }
42
+
43
+ its(:self_link) { should eql Yaks::Resource::Link.new(:self, '/foo/bar', {}) }
28
44
  end
29
45
 
30
46
  context 'with subresources' do
31
47
  let(:init_opts) { { subresources: { 'comments' => [Yaks::Resource.new(type: 'comment')] } } }
32
48
  its(:subresources) { should eql 'comments' => [Yaks::Resource.new(type: 'comment')] }
49
+
50
+ it 'should return an enumerator for #each' do
51
+ expect(resource.each.with_index.to_a).to eq [ [resource, 0] ]
52
+ end
33
53
  end
34
54
 
35
- its(:collection?) { should equal false }
36
55
 
37
56
  it 'should act as a collection of one' do
38
57
  expect(resource.each.to_a).to eql [resource]
@@ -3,7 +3,7 @@ require 'spec_helper'
3
3
  RSpec.describe Yaks::Serializer::Hal do
4
4
  include_context 'plant collection resource'
5
5
 
6
- subject { Yaks::Primitivize.create.call(described_class.new(resource).serialize) }
6
+ subject { Yaks::Primitivize.create.call(described_class.new.call(resource)) }
7
7
 
8
8
  it { should eq(load_json_fixture('hal_plant_collection')) }
9
9
  end
metadata CHANGED
@@ -1,125 +1,125 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yaks
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.4.2
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-18 00:00:00.000000000 Z
11
+ date: 2014-06-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: inflection
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ~>
17
+ - - "~>"
18
18
  - !ruby/object:Gem::Version
19
19
  version: '1.0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ~>
24
+ - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: concord
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - ~>
31
+ - - "~>"
32
32
  - !ruby/object:Gem::Version
33
33
  version: 0.1.4
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - ~>
38
+ - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: 0.1.4
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: uri_template
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - ~>
45
+ - - "~>"
46
46
  - !ruby/object:Gem::Version
47
47
  version: 0.6.0
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - ~>
52
+ - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: 0.6.0
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: rack-accept
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - ~>
59
+ - - "~>"
60
60
  - !ruby/object:Gem::Version
61
61
  version: 0.4.5
62
62
  type: :runtime
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
- - - ~>
66
+ - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: 0.4.5
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: virtus
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
- - - '>='
73
+ - - ">="
74
74
  - !ruby/object:Gem::Version
75
75
  version: '0'
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
- - - '>='
80
+ - - ">="
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: rspec
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
- - - ~>
87
+ - - "~>"
88
88
  - !ruby/object:Gem::Version
89
89
  version: '2.99'
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
- - - ~>
94
+ - - "~>"
95
95
  - !ruby/object:Gem::Version
96
96
  version: '2.99'
97
97
  - !ruby/object:Gem::Dependency
98
98
  name: rake
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
- - - '>='
101
+ - - ">="
102
102
  - !ruby/object:Gem::Version
103
103
  version: '0'
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
- - - '>='
108
+ - - ">="
109
109
  - !ruby/object:Gem::Version
110
110
  version: '0'
111
111
  - !ruby/object:Gem::Dependency
112
112
  name: mutant-rspec
113
113
  requirement: !ruby/object:Gem::Requirement
114
114
  requirements:
115
- - - '>='
115
+ - - ">="
116
116
  - !ruby/object:Gem::Version
117
117
  version: '0'
118
118
  type: :development
119
119
  prerelease: false
120
120
  version_requirements: !ruby/object:Gem::Requirement
121
121
  requirements:
122
- - - '>='
122
+ - - ">="
123
123
  - !ruby/object:Gem::Version
124
124
  version: '0'
125
125
  - !ruby/object:Gem::Dependency
@@ -140,28 +140,28 @@ dependencies:
140
140
  name: rspec-its
141
141
  requirement: !ruby/object:Gem::Requirement
142
142
  requirements:
143
- - - '>='
143
+ - - ">="
144
144
  - !ruby/object:Gem::Version
145
145
  version: '0'
146
146
  type: :development
147
147
  prerelease: false
148
148
  version_requirements: !ruby/object:Gem::Requirement
149
149
  requirements:
150
- - - '>='
150
+ - - ">="
151
151
  - !ruby/object:Gem::Version
152
152
  version: '0'
153
153
  - !ruby/object:Gem::Dependency
154
154
  name: benchmark-ips
155
155
  requirement: !ruby/object:Gem::Requirement
156
156
  requirements:
157
- - - '>='
157
+ - - ">="
158
158
  - !ruby/object:Gem::Version
159
159
  version: '0'
160
160
  type: :development
161
161
  prerelease: false
162
162
  version_requirements: !ruby/object:Gem::Requirement
163
163
  requirements:
164
- - - '>='
164
+ - - ">="
165
165
  - !ruby/object:Gem::Version
166
166
  version: '0'
167
167
  description: Serialize to hypermedia. HAL, JSON-API, etc.
@@ -172,10 +172,12 @@ extensions: []
172
172
  extra_rdoc_files:
173
173
  - README.md
174
174
  files:
175
- - .gitignore
176
- - .travis.yml
175
+ - ".gitignore"
176
+ - ".travis.yml"
177
+ - ADDING_FORMATS.md
177
178
  - CHANGELOG.md
178
179
  - Gemfile
180
+ - IDENTIFIERS.md
179
181
  - LICENSE
180
182
  - README.md
181
183
  - Rakefile
@@ -239,6 +241,9 @@ files:
239
241
  - spec/unit/yaks/mapper/has_one_spec.rb
240
242
  - spec/unit/yaks/mapper/link_spec.rb
241
243
  - spec/unit/yaks/mapper_spec.rb
244
+ - spec/unit/yaks/null_resource_spec.rb
245
+ - spec/unit/yaks/primitivize_spec.rb
246
+ - spec/unit/yaks/resource/link_spec.rb
242
247
  - spec/unit/yaks/resource_spec.rb
243
248
  - spec/unit/yaks/serializer/hal_spec.rb
244
249
  - spec/unit/yaks/serializer_spec.rb
@@ -256,12 +261,12 @@ require_paths:
256
261
  - lib
257
262
  required_ruby_version: !ruby/object:Gem::Requirement
258
263
  requirements:
259
- - - '>='
264
+ - - ">="
260
265
  - !ruby/object:Gem::Version
261
266
  version: '0'
262
267
  required_rubygems_version: !ruby/object:Gem::Requirement
263
268
  requirements:
264
- - - '>='
269
+ - - ">="
265
270
  - !ruby/object:Gem::Version
266
271
  version: '0'
267
272
  requirements: []
@@ -301,6 +306,9 @@ test_files:
301
306
  - spec/unit/yaks/mapper/has_one_spec.rb
302
307
  - spec/unit/yaks/mapper/link_spec.rb
303
308
  - spec/unit/yaks/mapper_spec.rb
309
+ - spec/unit/yaks/null_resource_spec.rb
310
+ - spec/unit/yaks/primitivize_spec.rb
311
+ - spec/unit/yaks/resource/link_spec.rb
304
312
  - spec/unit/yaks/resource_spec.rb
305
313
  - spec/unit/yaks/serializer/hal_spec.rb
306
314
  - spec/unit/yaks/serializer_spec.rb