yaks 0.4.4 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/CHANGELOG.md +44 -3
  4. data/README.md +90 -33
  5. data/Rakefile +10 -0
  6. data/bench/bench.rb +0 -1
  7. data/bench/bench_1000.rb +60 -0
  8. data/lib/yaks/breaking_changes.rb +22 -0
  9. data/lib/yaks/config/dsl.rb +114 -27
  10. data/lib/yaks/config.rb +39 -54
  11. data/lib/yaks/default_policy.rb +32 -14
  12. data/lib/yaks/format/collection_json.rb +4 -4
  13. data/lib/yaks/format/hal.rb +20 -3
  14. data/lib/yaks/format/json_api.rb +3 -3
  15. data/lib/yaks/format.rb +54 -9
  16. data/lib/yaks/fp/callable.rb +9 -0
  17. data/lib/yaks/fp/hash_updatable.rb +2 -0
  18. data/lib/yaks/fp/updatable.rb +2 -0
  19. data/lib/yaks/fp.rb +8 -0
  20. data/lib/yaks/mapper/link.rb +2 -2
  21. data/lib/yaks/mapper.rb +6 -6
  22. data/lib/yaks/primitivize.rb +2 -2
  23. data/lib/yaks/resource/link.rb +0 -4
  24. data/lib/yaks/runner.rb +90 -0
  25. data/lib/yaks/util.rb +4 -0
  26. data/lib/yaks/version.rb +1 -1
  27. data/lib/yaks.rb +3 -0
  28. data/spec/acceptance/acceptance_spec.rb +6 -1
  29. data/spec/json/confucius.collection.json +5 -16
  30. data/spec/json/plant_collection.collection.json +32 -0
  31. data/spec/spec_helper.rb +2 -1
  32. data/spec/support/deep_eql.rb +14 -7
  33. data/spec/support/pet_mapper.rb +0 -2
  34. data/spec/unit/yaks/collection_mapper_spec.rb +24 -2
  35. data/spec/unit/yaks/config/dsl_spec.rb +6 -10
  36. data/spec/unit/yaks/config_spec.rb +40 -99
  37. data/spec/unit/yaks/default_policy_spec.rb +20 -0
  38. data/spec/unit/yaks/format/collection_json_spec.rb +41 -0
  39. data/spec/unit/yaks/format/hal_spec.rb +38 -3
  40. data/spec/unit/yaks/format/json_api_spec.rb +2 -2
  41. data/spec/unit/yaks/format_spec.rb +28 -3
  42. data/spec/unit/yaks/fp/callable_spec.rb +13 -0
  43. data/spec/unit/yaks/mapper_spec.rb +226 -126
  44. data/spec/unit/yaks/resource/link_spec.rb +2 -3
  45. data/spec/unit/yaks/resource_spec.rb +15 -0
  46. data/spec/unit/yaks/runner_spec.rb +260 -0
  47. data/spec/unit/yaks/util_spec.rb +7 -1
  48. data/yaks.gemspec +4 -1
  49. metadata +72 -15
  50. /data/spec/json/{hal_plant_collection.json → plant_collection.hal.json} +0 -0
@@ -39,19 +39,18 @@ module Matchers
39
39
  end.join
40
40
  end
41
41
 
42
- def failure_message(message)
42
+ def add_failure_message(message)
43
43
  diffs << "at %s, %s" % [stack_as_jsonpath, message]
44
44
  @result = false
45
45
  end
46
46
 
47
47
  def compare(key)
48
- #require 'pry' ; binding.pry
49
48
  push key
50
49
  if target[key] != expectation[key]
51
50
  if [Hash, Array].any?{|klz| target[key].is_a? klz }
52
51
  recurse(target[key], expectation[key])
53
52
  else
54
- failure_message begin
53
+ add_failure_message begin
55
54
  if expectation[key].class == target[key].class
56
55
  "expected #{expectation[key].inspect}, got #{target[key].inspect}"
57
56
  else
@@ -72,13 +71,19 @@ module Matchers
72
71
  when Hash
73
72
  if target.is_a?(Hash)
74
73
  if target.class != expectation.class # e.g. HashWithIndifferentAccess
75
- failure_message("expected #{expectation.class}, got #{target.class}")
74
+ add_failure_message("expected #{expectation.class}, got #{target.class}")
75
+ end
76
+ (expectation.keys - target.keys).each do |key|
77
+ add_failure_message "Expected key #{key}"
78
+ end
79
+ (target.keys - expectation.keys).each do |key|
80
+ add_failure_message "Unexpected key #{key}"
76
81
  end
77
82
  (target.keys | expectation.keys).each do |key|
78
83
  compare key
79
84
  end
80
85
  else
81
- failure_message("expected Hash got #{@target.inspect}")
86
+ add_failure_message("expected Hash got #{@target.inspect}")
82
87
  end
83
88
 
84
89
  when Array
@@ -87,12 +92,12 @@ module Matchers
87
92
  compare idx
88
93
  end
89
94
  else
90
- failure_message("expected Array got #{@target.inspect}")
95
+ add_failure_message("expected Array got #{@target.inspect}")
91
96
  end
92
97
 
93
98
  else
94
99
  if target != expectation
95
- failure_message("expected #{expectation.inspect}, got #{@target.inspect}")
100
+ add_failure_message("expected #{expectation.inspect}, got #{@target.inspect}")
96
101
  end
97
102
  end
98
103
 
@@ -102,10 +107,12 @@ module Matchers
102
107
  def failure_message_for_should
103
108
  diffs.join("\n")
104
109
  end
110
+ alias failure_message failure_message_for_should
105
111
 
106
112
  def failure_message_for_should_not
107
113
  "expected #{@target.inspect} not to be in deep_eql with #{@expectation.inspect}"
108
114
  end
115
+ alias failure_message_when_negated failure_message_for_should_not
109
116
  end
110
117
  end
111
118
 
@@ -1,5 +1,3 @@
1
1
  class PetMapper < Yaks::Mapper
2
2
  attributes :id, :name, :species
3
-
4
- #link :collection, '/api/pets/{id*}'
5
3
  end
@@ -10,8 +10,7 @@ RSpec.describe Yaks::CollectionMapper do
10
10
  { item_mapper: item_mapper,
11
11
  policy: policy,
12
12
  env: {},
13
- mapper_stack: []
14
- }
13
+ mapper_stack: [] }
15
14
  }
16
15
 
17
16
  let(:collection) { [] }
@@ -33,6 +32,10 @@ RSpec.describe Yaks::CollectionMapper do
33
32
  let(:item_mapper) { PetMapper }
34
33
 
35
34
  it 'should map the members' do
35
+ stub(policy).derive_mapper_from_object(any_args) do
36
+ raise ":item_mapper was specified explicitly, should not be derived from object"
37
+ end
38
+
36
39
  expect(mapper.call(collection)).to eql Yaks::CollectionResource.new(
37
40
  type: 'pet',
38
41
  links: [],
@@ -140,4 +143,23 @@ RSpec.describe Yaks::CollectionMapper do
140
143
  end
141
144
  end
142
145
 
146
+ context 'with an empty collection' do
147
+
148
+ context 'without an item_mapper specified' do
149
+ let(:context) { Yaks::Util.slice_hash(super(), :policy, :env) }
150
+
151
+ it 'should use a rel of "collection"' do
152
+ expect(mapper.([]).collection_rel).to eq 'collection'
153
+ end
154
+ end
155
+
156
+ context 'with an item_mapper specified' do
157
+ let(:context) { Yaks::Util.slice_hash(super(), :policy, :env, :item_mapper) }
158
+
159
+ it 'should derive the collection rel from the item mapper' do
160
+ expect(mapper.([]).collection_rel).to eq 'rel:the_types'
161
+ end
162
+ end
163
+
164
+ end
143
165
  end
@@ -43,7 +43,7 @@ RSpec.describe Yaks::Config::DSL do
43
43
 
44
44
  describe '#format_options' do
45
45
  configure { format_options :hal, singular_link: [:self] }
46
- specify { expect(yaks_config.format_options[:hal].should eq(singular_link: [:self])) }
46
+ specify { expect(yaks_config.format_options[:hal]).to eq(singular_link: [:self]) }
47
47
  end
48
48
 
49
49
  describe '#default_format' do
@@ -61,6 +61,11 @@ RSpec.describe Yaks::Config::DSL do
61
61
  specify { expect(yaks_config.policy_options[:rel_template]).to eql 'rels:{rel}' }
62
62
  end
63
63
 
64
+ describe '#json_serializer' do
65
+ configure { json_serializer { |i| "foo #{i}" } }
66
+ specify { expect(yaks_config.serializers[:json].call(7)).to eql 'foo 7' }
67
+ end
68
+
64
69
  describe '#mapper_namespace' do
65
70
  configure { mapper_namespace RSpec }
66
71
  specify { expect(yaks_config.policy_options[:namespace]).to eql RSpec }
@@ -79,13 +84,4 @@ RSpec.describe Yaks::Config::DSL do
79
84
  specify { expect(yaks_config.primitivize.call({:abc => Foo.new('hello')})).to eql 'abc' => 'hello' }
80
85
  end
81
86
 
82
- describe '#after' do
83
- configure do
84
- after {|x| x + 1}
85
- after {|x| x + 10}
86
- end
87
- it 'should register the block' do
88
- expect(yaks_config.steps.inject(0) {|memo, step| step.call(memo)}).to be 11
89
- end
90
- end
91
87
  end
@@ -7,57 +7,63 @@ RSpec.describe Yaks::Config do
7
7
  subject(:config) { described_class.new(&blk) }
8
8
  end
9
9
 
10
- context 'defaults' do
11
- configure {}
12
-
13
- its(:default_format) { should equal :hal }
14
- its(:policy_class) { should < Yaks::DefaultPolicy }
15
-
16
- it 'should have empty format options' do
17
- expect(config.options_for_format(:hal)).to eql({})
18
- end
19
- end
20
-
21
- context 'with a default format' do
22
- configure do
23
- default_format :json_api
10
+ describe '#initialize' do
11
+ context 'defaults' do
12
+ configure {}
13
+
14
+ its(:default_format) { should equal :hal }
15
+ its(:policy_class) { should < Yaks::DefaultPolicy }
16
+ its(:primitivize) { should be_a Yaks::Primitivize }
17
+ its(:serializers) { should eql({}) }
18
+ its(:hooks) { should eql([]) }
19
+
20
+ it 'should have empty format options' do
21
+ expect(config.format_options[:hal]).to eql({})
22
+ end
24
23
  end
25
24
 
26
- its(:default_format) { should equal :json_api }
27
- end
25
+ context 'with a default format' do
26
+ configure do
27
+ default_format :json_api
28
+ end
28
29
 
29
- context 'with a custom policy class' do
30
- MyPolicy = Struct.new(:options)
31
- configure do
32
- policy MyPolicy
30
+ its(:default_format) { should equal :json_api }
33
31
  end
34
32
 
35
- its(:policy_class) { should equal MyPolicy }
36
- its(:policy) { should be_a MyPolicy }
37
- end
33
+ context 'with a custom policy class' do
34
+ MyPolicy = Struct.new(:options)
35
+ configure do
36
+ policy MyPolicy
37
+ end
38
38
 
39
- context 'with a rel template' do
40
- configure do
41
- rel_template 'http://rel/foo'
39
+ its(:policy_class) { should equal MyPolicy }
40
+ its(:policy) { should be_a MyPolicy }
42
41
  end
43
42
 
44
- its(:policy_options) { should eql(rel_template: 'http://rel/foo') }
45
- end
43
+ context 'with a rel template' do
44
+ configure do
45
+ rel_template 'http://rel/foo'
46
+ end
46
47
 
47
- context 'with format options' do
48
- configure do
49
- format_options :hal, plural_links: [:self, :profile]
48
+ its(:policy_options) { should eql(rel_template: 'http://rel/foo') }
50
49
  end
51
50
 
52
- specify do
53
- expect(config.options_for_format(:hal)).to eql(plural_links: [:self, :profile])
51
+ context 'with format options' do
52
+ configure do
53
+ format_options :hal, plural_links: [:self, :profile]
54
+ end
55
+
56
+ specify do
57
+ expect(config.format_options[:hal]).to eql(plural_links: [:self, :profile])
58
+ end
54
59
  end
55
60
  end
56
61
 
57
- describe '#serialize' do
62
+ describe '#call' do
58
63
  configure do
59
64
  rel_template 'http://api.mysuperfriends.com/{rel}'
60
65
  format_options :hal, plural_links: [:copyright]
66
+ skip :serialize
61
67
  end
62
68
 
63
69
  specify do
@@ -65,69 +71,4 @@ RSpec.describe Yaks::Config do
65
71
  end
66
72
  end
67
73
 
68
- describe '#mapper_namespace' do
69
- module MyMappers
70
- class PetMapper < Yaks::Mapper
71
- end
72
- end
73
-
74
- configure do
75
- mapper_namespace MyMappers
76
- end
77
-
78
- specify do
79
- expect(config.policy.derive_mapper_from_object(boingboing)).to eql(MyMappers::PetMapper)
80
- end
81
- end
82
-
83
- describe '#map_to_primitive' do
84
- class TheMapper < Yaks::Mapper
85
- attributes :a_date
86
- end
87
-
88
- TheModel = Struct.new(:a_date)
89
-
90
- configure do
91
- map_to_primitive Date do |object|
92
- object.iso8601
93
- end
94
- end
95
-
96
- let(:model) {
97
- TheModel.new(Date.new(2014, 5, 6))
98
- }
99
-
100
- specify do
101
- expect(config.serialize(model, mapper: TheMapper)).to eq({"a_date"=>"2014-05-06"})
102
- end
103
- end
104
-
105
- describe '#format_class' do
106
- configure do
107
- default_format :collection_json
108
- end
109
-
110
- let(:rack_env) {
111
- { 'HTTP_ACCEPT' => 'application/hal+json;q=0.8, application/vnd.api+json' }
112
- }
113
-
114
- it 'should fall back to the default when no HTTP_ACCEPT key is present' do
115
- expect(config.format_class({}, {})).to equal Yaks::Format::CollectionJson
116
- end
117
-
118
- it 'should detect format based on accept header' do
119
- rack_env = { 'HTTP_ACCEPT' => 'application/hal+json;q=0.8, application/vnd.api+json' }
120
- expect(config.format_class({}, rack_env)).to equal Yaks::Format::JsonApi
121
- end
122
-
123
- it 'should know to pick the best match' do
124
- rack_env = { 'HTTP_ACCEPT' => 'application/hal+json;q=0.8, application/vnd.api+json;q=0.7' }
125
- expect(config.format_class({}, rack_env)).to equal Yaks::Format::Hal
126
- end
127
-
128
- it 'should fall back to the default when no mime type is recognized' do
129
- rack_env = { 'HTTP_ACCEPT' => 'text/html, application/json' }
130
- expect(config.format_class({}, rack_env)).to equal Yaks::Format::CollectionJson
131
- end
132
- end
133
74
  end
@@ -26,6 +26,20 @@ RSpec.describe Yaks::DefaultPolicy do
26
26
  end
27
27
  end
28
28
 
29
+ describe '#derive_type_from_collection' do
30
+ specify do
31
+ expect(
32
+ policy.derive_type_from_collection([Soy.new])
33
+ ).to eql 'soy'
34
+ end
35
+
36
+ specify do
37
+ expect(
38
+ policy.derive_type_from_collection([])
39
+ ).to be_nil
40
+ end
41
+ end
42
+
29
43
  describe '#derive_mapper_from_association' do
30
44
  let(:options) { { namespace: Namespace } }
31
45
 
@@ -47,4 +61,10 @@ RSpec.describe Yaks::DefaultPolicy do
47
61
  end
48
62
  end
49
63
 
64
+ describe '#serializer_for_format' do
65
+ specify {
66
+ expect(policy.serializer_for_format(Yaks::Format::JsonAPI).call('foo' => [1,2])).to eql "{\n \"foo\": [\n 1,\n 2\n ]\n}"
67
+ }
68
+ end
69
+
50
70
  end
@@ -0,0 +1,41 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Yaks::Format::CollectionJson do
4
+ context 'with the plant collection resource' do
5
+ include_context 'plant collection resource'
6
+
7
+ subject { Yaks::Primitivize.create.call(described_class.new.call(resource)) }
8
+
9
+ it { should deep_eql(load_json_fixture('plant_collection.collection')) }
10
+ end
11
+
12
+ context 'with a link without title' do
13
+ let(:resource) {
14
+ Yaks::Resource.new(
15
+ attributes: {foo: 'fooval', bar: 'barval'},
16
+ links: [Yaks::Resource::Link.new('the_rel', 'the_uri', {})]
17
+ )
18
+ }
19
+
20
+ subject {
21
+ Yaks::Primitivize.create.call(described_class.new.call(resource))
22
+ }
23
+
24
+ it 'should not render a name' do
25
+ should deep_eql(
26
+ "collection" => {
27
+ "version" => "1.0",
28
+ "items" => [
29
+ {
30
+ "data" => [
31
+ { "name"=>"foo", "value"=>"fooval" },
32
+ { "name"=>"bar", "value"=>"barval" }
33
+ ],
34
+ "links" => [{"rel"=>"the_rel", "href"=>"the_uri"}]
35
+ }
36
+ ]
37
+ }
38
+ )
39
+ end
40
+ end
41
+ end
@@ -1,9 +1,44 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  RSpec.describe Yaks::Format::Hal do
4
- include_context 'plant collection resource'
4
+ context 'with the plant collection resource' do
5
+ include_context 'plant collection resource'
5
6
 
6
- subject { Yaks::Primitivize.create.call(described_class.new.call(resource)) }
7
+ subject { Yaks::Primitivize.create.call(described_class.new.call(resource)) }
7
8
 
8
- it { should eq(load_json_fixture('hal_plant_collection')) }
9
+ it { should deep_eql(load_json_fixture('plant_collection.hal')) }
10
+ end
11
+
12
+ context 'with multiple links on the same rel' do
13
+ let(:format) {
14
+ described_class.new(:plural_links => 'my_plural_rel')
15
+ }
16
+
17
+ let(:resource) {
18
+ Yaks::Resource.new(
19
+ attributes: {foo: 'fooval', bar: 'barval'},
20
+ links: [
21
+ Yaks::Resource::Link.new('my_plural_rel', 'the_uri1', {}),
22
+ Yaks::Resource::Link.new('my_plural_rel', 'the_uri2', {})
23
+ ]
24
+ )
25
+ }
26
+
27
+ subject {
28
+ Yaks::Primitivize.create.call(format.call(resource))
29
+ }
30
+
31
+ it 'should render both links' do
32
+ should deep_eql(
33
+ 'foo' => 'fooval',
34
+ 'bar' => 'barval',
35
+ '_links' => {
36
+ "my_plural_rel" => [
37
+ {"href"=>"the_uri1"},
38
+ {"href"=>"the_uri2"}
39
+ ]
40
+ }
41
+ )
42
+ end
43
+ end
9
44
  end
@@ -1,8 +1,8 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  # Mainly tested through the acceptance tests, here covering a few specific edge cases
4
- RSpec.describe Yaks::Format::JsonApi do
5
- let(:format) { Yaks::Format::JsonApi.new }
4
+ RSpec.describe Yaks::Format::JsonAPI do
5
+ let(:format) { Yaks::Format::JsonAPI.new }
6
6
 
7
7
  context 'with no subresources' do
8
8
  let(:resource) { Yaks::Resource.new(type: 'wizard', attributes: {foo: :bar}) }
@@ -2,11 +2,36 @@ require 'spec_helper'
2
2
 
3
3
  RSpec.describe Yaks::Format do
4
4
  describe '.by_name' do
5
- specify { expect(Yaks::Format.by_name(:hal)).to eql Yaks::Format::Hal }
6
- specify { expect(Yaks::Format.by_name(:json_api)).to eql Yaks::Format::JsonApi }
5
+ specify do
6
+ expect(Yaks::Format.by_name(:hal)).to eql Yaks::Format::Hal
7
+ end
8
+ specify do
9
+ expect(Yaks::Format.by_name(:json_api)).to eql Yaks::Format::JsonAPI
10
+ end
7
11
  end
8
12
 
9
13
  describe '.by_mime_type' do
10
- specify { expect(Yaks::Format.by_mime_type('application/hal+json')).to eql Yaks::Format::Hal }
14
+ specify do
15
+ expect(Yaks::Format.by_mime_type('application/hal+json')).to eql Yaks::Format::Hal
16
+ end
17
+ end
18
+
19
+ describe '.by_accept_header' do
20
+ specify do
21
+ expect(Yaks::Format.by_accept_header('application/hal+json;q=0.8, application/vnd.api+json')).to eql Yaks::Format::JsonAPI
22
+ end
23
+ specify do
24
+ expect(Yaks::Format.by_accept_header('application/hal+json;q=0.8, application/vnd.api+json;q=0.7')).to eql Yaks::Format::Hal
25
+ end
26
+ end
27
+
28
+ describe '.mime_types' do
29
+ specify do
30
+ expect(Yaks::Format.mime_types).to eql(
31
+ collection_json: "application/vnd.collection+json",
32
+ hal: "application/hal+json",
33
+ json_api: "application/vnd.api+json"
34
+ )
35
+ end
11
36
  end
12
37
  end
@@ -0,0 +1,13 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Yaks::FP::Callable do
4
+ it 'should delegate to_proc to method(:call)' do
5
+ obj = Class.new do
6
+ include Yaks::FP::Callable
7
+
8
+ def call(x) ; x * x ; end
9
+ end.new
10
+
11
+ expect([1,2,3].map(&obj)).to eql [1,4,9]
12
+ end
13
+ end