yaks 0.4.4 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/CHANGELOG.md +44 -3
- data/README.md +90 -33
- data/Rakefile +10 -0
- data/bench/bench.rb +0 -1
- data/bench/bench_1000.rb +60 -0
- data/lib/yaks/breaking_changes.rb +22 -0
- data/lib/yaks/config/dsl.rb +114 -27
- data/lib/yaks/config.rb +39 -54
- data/lib/yaks/default_policy.rb +32 -14
- data/lib/yaks/format/collection_json.rb +4 -4
- data/lib/yaks/format/hal.rb +20 -3
- data/lib/yaks/format/json_api.rb +3 -3
- data/lib/yaks/format.rb +54 -9
- data/lib/yaks/fp/callable.rb +9 -0
- data/lib/yaks/fp/hash_updatable.rb +2 -0
- data/lib/yaks/fp/updatable.rb +2 -0
- data/lib/yaks/fp.rb +8 -0
- data/lib/yaks/mapper/link.rb +2 -2
- data/lib/yaks/mapper.rb +6 -6
- data/lib/yaks/primitivize.rb +2 -2
- data/lib/yaks/resource/link.rb +0 -4
- data/lib/yaks/runner.rb +90 -0
- data/lib/yaks/util.rb +4 -0
- data/lib/yaks/version.rb +1 -1
- data/lib/yaks.rb +3 -0
- data/spec/acceptance/acceptance_spec.rb +6 -1
- data/spec/json/confucius.collection.json +5 -16
- data/spec/json/plant_collection.collection.json +32 -0
- data/spec/spec_helper.rb +2 -1
- data/spec/support/deep_eql.rb +14 -7
- data/spec/support/pet_mapper.rb +0 -2
- data/spec/unit/yaks/collection_mapper_spec.rb +24 -2
- data/spec/unit/yaks/config/dsl_spec.rb +6 -10
- data/spec/unit/yaks/config_spec.rb +40 -99
- data/spec/unit/yaks/default_policy_spec.rb +20 -0
- data/spec/unit/yaks/format/collection_json_spec.rb +41 -0
- data/spec/unit/yaks/format/hal_spec.rb +38 -3
- data/spec/unit/yaks/format/json_api_spec.rb +2 -2
- data/spec/unit/yaks/format_spec.rb +28 -3
- data/spec/unit/yaks/fp/callable_spec.rb +13 -0
- data/spec/unit/yaks/mapper_spec.rb +226 -126
- data/spec/unit/yaks/resource/link_spec.rb +2 -3
- data/spec/unit/yaks/resource_spec.rb +15 -0
- data/spec/unit/yaks/runner_spec.rb +260 -0
- data/spec/unit/yaks/util_spec.rb +7 -1
- data/yaks.gemspec +4 -1
- metadata +72 -15
- /data/spec/json/{hal_plant_collection.json → plant_collection.hal.json} +0 -0
data/spec/support/deep_eql.rb
CHANGED
@@ -39,19 +39,18 @@ module Matchers
|
|
39
39
|
end.join
|
40
40
|
end
|
41
41
|
|
42
|
-
def
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
95
|
+
add_failure_message("expected Array got #{@target.inspect}")
|
91
96
|
end
|
92
97
|
|
93
98
|
else
|
94
99
|
if target != expectation
|
95
|
-
|
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
|
|
data/spec/support/pet_mapper.rb
CHANGED
@@ -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].
|
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
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
-
|
27
|
-
|
25
|
+
context 'with a default format' do
|
26
|
+
configure do
|
27
|
+
default_format :json_api
|
28
|
+
end
|
28
29
|
|
29
|
-
|
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
|
-
|
36
|
-
|
37
|
-
|
33
|
+
context 'with a custom policy class' do
|
34
|
+
MyPolicy = Struct.new(:options)
|
35
|
+
configure do
|
36
|
+
policy MyPolicy
|
37
|
+
end
|
38
38
|
|
39
|
-
|
40
|
-
|
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
|
-
|
45
|
-
|
43
|
+
context 'with a rel template' do
|
44
|
+
configure do
|
45
|
+
rel_template 'http://rel/foo'
|
46
|
+
end
|
46
47
|
|
47
|
-
|
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
|
-
|
53
|
-
|
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 '#
|
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
|
-
|
4
|
+
context 'with the plant collection resource' do
|
5
|
+
include_context 'plant collection resource'
|
5
6
|
|
6
|
-
|
7
|
+
subject { Yaks::Primitivize.create.call(described_class.new.call(resource)) }
|
7
8
|
|
8
|
-
|
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::
|
5
|
-
let(:format) { Yaks::Format::
|
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
|
6
|
-
|
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
|
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
|