representors 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +18 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +126 -0
- data/LICENSE.md +19 -0
- data/README.md +28 -0
- data/Rakefile +10 -0
- data/lib/representor_support/utilities.rb +39 -0
- data/lib/representors.rb +5 -0
- data/lib/representors/errors.rb +7 -0
- data/lib/representors/field.rb +108 -0
- data/lib/representors/options.rb +67 -0
- data/lib/representors/representor.rb +161 -0
- data/lib/representors/representor_builder.rb +64 -0
- data/lib/representors/representor_hash.rb +59 -0
- data/lib/representors/serialization.rb +4 -0
- data/lib/representors/serialization/deserializer_base.rb +29 -0
- data/lib/representors/serialization/deserializer_factory.rb +13 -0
- data/lib/representors/serialization/hal_deserializer.rb +44 -0
- data/lib/representors/serialization/hal_serializer.rb +91 -0
- data/lib/representors/serialization/hale_deserializer.rb +162 -0
- data/lib/representors/serialization/hale_serializer.rb +110 -0
- data/lib/representors/serialization/serialization_base.rb +27 -0
- data/lib/representors/serialization/serialization_factory_base.rb +54 -0
- data/lib/representors/serialization/serializer_base.rb +20 -0
- data/lib/representors/serialization/serializer_factory.rb +17 -0
- data/lib/representors/transition.rb +130 -0
- data/lib/representors/version.rb +4 -0
- data/spec/fixtures/complex_hal.json +92 -0
- data/spec/fixtures/complex_hale_document.json +81 -0
- data/spec/fixtures/drds_hash.rb +120 -0
- data/spec/fixtures/hale_spec_examples/basic.json +77 -0
- data/spec/fixtures/hale_spec_examples/complex_reference_objects.json +157 -0
- data/spec/fixtures/hale_spec_examples/data.json +17 -0
- data/spec/fixtures/hale_spec_examples/data_objects.json +96 -0
- data/spec/fixtures/hale_spec_examples/link_objects.json +18 -0
- data/spec/fixtures/hale_spec_examples/nested_ref.json +43 -0
- data/spec/fixtures/hale_spec_examples/reference_objects.json +89 -0
- data/spec/fixtures/hale_tutorial_examples/basic_links.json +85 -0
- data/spec/fixtures/hale_tutorial_examples/basic_links_with_orders.json +96 -0
- data/spec/fixtures/hale_tutorial_examples/basic_links_with_references.json +108 -0
- data/spec/fixtures/hale_tutorial_examples/embedded.json +182 -0
- data/spec/fixtures/hale_tutorial_examples/empty.json +1 -0
- data/spec/fixtures/hale_tutorial_examples/enctype.json +14 -0
- data/spec/fixtures/hale_tutorial_examples/final.json +141 -0
- data/spec/fixtures/hale_tutorial_examples/get_link.json +17 -0
- data/spec/fixtures/hale_tutorial_examples/get_link_with_data.json +29 -0
- data/spec/fixtures/hale_tutorial_examples/links.json +11 -0
- data/spec/fixtures/hale_tutorial_examples/links_only.json +3 -0
- data/spec/fixtures/hale_tutorial_examples/meta.json +208 -0
- data/spec/fixtures/hale_tutorial_examples/self_link.json +7 -0
- data/spec/fixtures/single_drd.rb +266 -0
- data/spec/lib/representors/complex_representor_spec.rb +288 -0
- data/spec/lib/representors/field_spec.rb +141 -0
- data/spec/lib/representors/representor_builder_spec.rb +223 -0
- data/spec/lib/representors/representor_spec.rb +285 -0
- data/spec/lib/representors/serialization/deserializer_factory_spec.rb +118 -0
- data/spec/lib/representors/serialization/hal_deserializer_spec.rb +34 -0
- data/spec/lib/representors/serialization/hal_serializer_spec.rb +171 -0
- data/spec/lib/representors/serialization/hale_deserializer_spec.rb +59 -0
- data/spec/lib/representors/serialization/hale_roundtrip_spec.rb +34 -0
- data/spec/lib/representors/serialization/hale_serializer_spec.rb +659 -0
- data/spec/lib/representors/serialization/serializer_factory_spec.rb +108 -0
- data/spec/lib/representors/transition_spec.rb +349 -0
- data/spec/spec_helper.rb +32 -0
- data/spec/support/basic-hale.json +12 -0
- data/spec/support/hal_representor_shared.rb +206 -0
- data/spec/support/helpers.rb +8 -0
- data/tasks/benchmark.rake +75 -0
- data/tasks/complex_hal_document.json +98 -0
- data/tasks/test_specs.rake +37 -0
- data/tasks/yard.rake +22 -0
- metadata +232 -0
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
unless ENV['MUTANT']
|
2
|
+
require 'simplecov'
|
3
|
+
SimpleCov.start
|
4
|
+
end
|
5
|
+
|
6
|
+
SPEC_DIR = File.expand_path("..", __FILE__)
|
7
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
8
|
+
require 'representors'
|
9
|
+
|
10
|
+
Dir["#{SPEC_DIR}/support/*.rb"].each { |f| require f }
|
11
|
+
|
12
|
+
def create_serializer(name)
|
13
|
+
Class.new(Representors::SerializerBase) do |klass|
|
14
|
+
klass.media_symbol name.to_sym
|
15
|
+
klass.media_type name
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
RSpec.configure do |config|
|
20
|
+
config.expect_with :rspec do |c|
|
21
|
+
c.syntax = :expect
|
22
|
+
end
|
23
|
+
|
24
|
+
# Run specs in random order to surface order dependencies. If you find an
|
25
|
+
# order dependency and want to debug it, you can fix the order by providing
|
26
|
+
# the seed, which is printed after each run.
|
27
|
+
# --seed 1234
|
28
|
+
config.order = 'random' unless ENV['RANDOMIZE'] == 'false'
|
29
|
+
|
30
|
+
config.include Support::Helpers
|
31
|
+
config.include RepresentorSupport::Utilities
|
32
|
+
end
|
@@ -0,0 +1,206 @@
|
|
1
|
+
shared_examples_for 'can create a representor from a hal document' do
|
2
|
+
|
3
|
+
let(:semantics_field) {deserializer.to_representor.properties}
|
4
|
+
let(:transitions_field) {deserializer.to_representor.transitions}
|
5
|
+
let(:embedded_field) {deserializer.to_representor.embedded }
|
6
|
+
context "empty document" do
|
7
|
+
let(:document) { {}.to_json }
|
8
|
+
|
9
|
+
it "returns a hash with no attributes, links or embedded resources" do
|
10
|
+
expect(deserializer.to_representor.properties).to be_empty
|
11
|
+
expect(deserializer.to_representor.transitions).to be_empty
|
12
|
+
expect(deserializer.to_representor.embedded).to be_empty
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
context 'Document with only properties' do
|
17
|
+
let(:original_hash) do
|
18
|
+
{
|
19
|
+
'title' => 'The Neverending Story',
|
20
|
+
'author' => 'Michael Ende',
|
21
|
+
'pages' => '396'
|
22
|
+
}
|
23
|
+
end
|
24
|
+
let(:document) { original_hash.to_json}
|
25
|
+
|
26
|
+
it 'return a representor with all the attributes of the document' do
|
27
|
+
expect(semantics_field).to eq(original_hash)
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
context 'Document with properties and links' do
|
33
|
+
let(:semantics) { { 'title' => 'The Neverending Story'}}
|
34
|
+
let(:transition_rel) { 'author'}
|
35
|
+
let(:transition_href) { '/mike'}
|
36
|
+
let(:document) do
|
37
|
+
{
|
38
|
+
'title' => 'The Neverending Story',
|
39
|
+
'_links' => {
|
40
|
+
'author' => {'href' => '/mike'}
|
41
|
+
}
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'return a hash with all the attributes of the document' do
|
46
|
+
expect(semantics_field).to eq(semantics)
|
47
|
+
end
|
48
|
+
it 'Create a transition with the link' do
|
49
|
+
expect(transitions_field.first.rel).to eq(transition_rel)
|
50
|
+
expect(transitions_field.first.uri).to eq(transition_href)
|
51
|
+
end
|
52
|
+
it 'does not return any embedded resource' do
|
53
|
+
expect(embedded_field).to be_empty
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
context 'Document with properties, links and embedded' do
|
59
|
+
let(:semantics) { { 'title' => 'The Neverending Story'}}
|
60
|
+
let(:transition_rel) { 'author'}
|
61
|
+
let(:transition_href) { '/mike'}
|
62
|
+
let(:embedded_book) { {'content' => 'A...'} }
|
63
|
+
let(:document) do
|
64
|
+
{
|
65
|
+
'title' => 'The Neverending Story',
|
66
|
+
'_links' => {
|
67
|
+
transition_rel => {'href' => transition_href}
|
68
|
+
},
|
69
|
+
'_embedded' => {
|
70
|
+
'embedded_book' => embedded_book
|
71
|
+
}
|
72
|
+
}
|
73
|
+
end
|
74
|
+
it 'Returns a hash with all the attributes of the document' do
|
75
|
+
expect(semantics_field).to eq(semantics)
|
76
|
+
end
|
77
|
+
it 'Creates a transition with the link' do
|
78
|
+
expect(transitions_field.first.rel).to eq(transition_rel)
|
79
|
+
expect(transitions_field.first.uri).to eq(transition_href)
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'Creates an embedded resource with its data' do
|
83
|
+
expect(embedded_field['embedded_book'].properties).to eq(embedded_book)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
|
88
|
+
context 'Document with an embedded collection' do
|
89
|
+
let(:embedded_book1) { {'content' => 'A...'} }
|
90
|
+
let(:embedded_book2) { {'content' => 'When...'} }
|
91
|
+
let(:embedded_book3) { {'content' => 'Once upon...'} }
|
92
|
+
let(:embedded_books) { [ embedded_book1, embedded_book2, embedded_book3 ] }
|
93
|
+
let(:document) do
|
94
|
+
{
|
95
|
+
'_embedded' => {
|
96
|
+
'embedded_books' => [ embedded_book1, embedded_book2, embedded_book3 ]
|
97
|
+
}
|
98
|
+
}
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'Creates three embedded resources' do
|
102
|
+
expect(embedded_field['embedded_books'].size).to eq(embedded_books.size)
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'Creates embedded resources with its data' do
|
106
|
+
embedded_books.each_with_index do |item, index|
|
107
|
+
expect(embedded_field['embedded_books'][index].properties).to eq(item)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
context 'Document with only a self link and a title' do
|
113
|
+
let(:href) { '/example_resource'}
|
114
|
+
let(:title) { 'super!'}
|
115
|
+
let(:document) do
|
116
|
+
{ '_links' => {
|
117
|
+
'self' => { 'href' => href, 'title' => title}
|
118
|
+
}
|
119
|
+
}
|
120
|
+
end
|
121
|
+
|
122
|
+
it 'the transition has a "self" rel' do
|
123
|
+
expect(transitions_field.first.rel).to eq('self')
|
124
|
+
end
|
125
|
+
|
126
|
+
it 'The transition has its href set properly' do
|
127
|
+
expect(transitions_field.first.uri).to eq(href)
|
128
|
+
end
|
129
|
+
|
130
|
+
it 'The transition has a title' do
|
131
|
+
expect(transitions_field.first['title']).to eq(title)
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
|
136
|
+
context 'Document with an array of two links under items' do
|
137
|
+
let(:first_href) {'/example_resource'}
|
138
|
+
let(:second_href) {'/lotr_resource2'}
|
139
|
+
let(:rel) { 'items'}
|
140
|
+
let(:document) do
|
141
|
+
{ '_links' => {
|
142
|
+
rel => [{ 'href' => first_href, 'title' => 'resource1'},
|
143
|
+
{ 'href' => second_href, 'title' => 'resource2'}]
|
144
|
+
}
|
145
|
+
}
|
146
|
+
end
|
147
|
+
|
148
|
+
it 'The representor has two links' do
|
149
|
+
expect(transitions_field.size).to eq(2)
|
150
|
+
end
|
151
|
+
|
152
|
+
it 'The transitions have a rel properly set' do
|
153
|
+
expect(transitions_field.all?{|link| link.rel == 'items'}).to eq(true)
|
154
|
+
end
|
155
|
+
|
156
|
+
it 'The transitions have a href properly set ' do
|
157
|
+
expect(transitions_field[0].uri).to eq(first_href)
|
158
|
+
expect(transitions_field[1].uri).to eq(second_href)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
context 'Document with a link without a href' do
|
163
|
+
let(:document) do
|
164
|
+
{ '_links' => {
|
165
|
+
'self' => { 'title' => 'things'}
|
166
|
+
}
|
167
|
+
}
|
168
|
+
end
|
169
|
+
|
170
|
+
it 'raises a DeserializationError' do
|
171
|
+
expect{transitions_field}.to raise_error(Representors::DeserializationError, "All links must contain the href attribute")
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
context 'Document where not all links have href' do
|
176
|
+
let(:link_properties) { { 'href' => '/example_resource'} }
|
177
|
+
let(:document) do
|
178
|
+
{ '_links' => {
|
179
|
+
'items' => [{ 'title' => 'resource1'},
|
180
|
+
{ 'href' => '/example_resource2', 'title' => 'resource2'}]
|
181
|
+
}
|
182
|
+
}
|
183
|
+
end
|
184
|
+
|
185
|
+
it 'raises a DeserializationError' do
|
186
|
+
expect{transitions_field}.to raise_error(Representors::DeserializationError, "All links must contain the href attribute")
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
context 'Document with CURIEs' do
|
191
|
+
let(:link_properties) { { 'href' => '/example_resource'} }
|
192
|
+
let(:document) do
|
193
|
+
{ '_links' => {
|
194
|
+
'curies' => { 'href' => '/example_resource'}
|
195
|
+
}
|
196
|
+
}
|
197
|
+
end
|
198
|
+
|
199
|
+
it 'raises a DeserializationError' do
|
200
|
+
expect{transitions_field}.to raise_error(Representors::DeserializationError, "CURIE support not implemented for HAL")
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
end
|
205
|
+
|
206
|
+
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'representors'
|
2
|
+
require 'benchmark'
|
3
|
+
|
4
|
+
ITERATIONS = 5
|
5
|
+
DESERIALIZATIONS = 10_000
|
6
|
+
HAL_BENCHMARK_FILE = 'complex_hal_document.json'
|
7
|
+
HALE_BENCHMARK_FILE = 'complex_hale_document.json'
|
8
|
+
|
9
|
+
|
10
|
+
def benchmark
|
11
|
+
benchmark_result = Benchmark.bm do |benchmarker|
|
12
|
+
1.upto(ITERATIONS) do |iteration|
|
13
|
+
benchmarker.report("Iteration #{iteration}") do
|
14
|
+
DESERIALIZATIONS.times do
|
15
|
+
yield
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
benchmark_result
|
21
|
+
average_total_times = benchmark_result.map(&:total).inject(&:+) / ITERATIONS
|
22
|
+
average_operation_ms = (average_total_times * 1000) / DESERIALIZATIONS
|
23
|
+
|
24
|
+
puts "Processing #{DESERIALIZATIONS} objects took on average #{'%.4f' % average_total_times} seconds"
|
25
|
+
puts "It took #{'%.4f' % average_operation_ms} milliseconds to process each document or representor"
|
26
|
+
end
|
27
|
+
|
28
|
+
def benchmark_deserializer(format, document)
|
29
|
+
data = File.read( File.join(File.dirname(__FILE__), document))
|
30
|
+
puts "Deserializing #{format}:"
|
31
|
+
|
32
|
+
benchmark do
|
33
|
+
result = Representors::DeserializerFactory.build(format, data).to_representor
|
34
|
+
result.properties
|
35
|
+
result.transitions
|
36
|
+
result.embedded
|
37
|
+
end
|
38
|
+
puts "------------------"
|
39
|
+
puts " "
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
def benchmark_serializer(format, document)
|
44
|
+
data = File.read( File.join(File.dirname(__FILE__), document))
|
45
|
+
representor = Representors::DeserializerFactory.build(format, data).to_representor
|
46
|
+
|
47
|
+
puts "Serializing #{format}:"
|
48
|
+
|
49
|
+
benchmark do
|
50
|
+
Representors::SerializerFactory.build(format, representor).to_media_type
|
51
|
+
end
|
52
|
+
puts "------------------"
|
53
|
+
puts " "
|
54
|
+
end
|
55
|
+
|
56
|
+
namespace :benchmark do
|
57
|
+
desc 'Benchmark deserializations'
|
58
|
+
task :deserializations do
|
59
|
+
benchmark_deserializer('application/hal+json', HAL_BENCHMARK_FILE )
|
60
|
+
benchmark_deserializer('application/vnd.hale+json', HALE_BENCHMARK_FILE )
|
61
|
+
end
|
62
|
+
|
63
|
+
desc 'Benchmark serializations'
|
64
|
+
task :serializations do
|
65
|
+
benchmark_serializer('application/hal+json', HAL_BENCHMARK_FILE )
|
66
|
+
benchmark_serializer('application/vnd.hale+json', HALE_BENCHMARK_FILE )
|
67
|
+
end
|
68
|
+
|
69
|
+
desc 'runs all benchmarks'
|
70
|
+
task :all do
|
71
|
+
Rake::Task['benchmark:deserializations'].invoke
|
72
|
+
Rake::Task['benchmark:serializations'].invoke
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
{
|
2
|
+
"_links":{
|
3
|
+
"self":{
|
4
|
+
"href":"http://example.org/api/user/ed"
|
5
|
+
},
|
6
|
+
"testrel":{
|
7
|
+
"href":"http://example.org/api/user/test",
|
8
|
+
"templated":true,
|
9
|
+
"type":"some-type",
|
10
|
+
"deprecation":"http://very-deprecated.com",
|
11
|
+
"name":"some-name",
|
12
|
+
"profile":"some-profile",
|
13
|
+
"title":"A Great Title",
|
14
|
+
"hreflang":"en-US"
|
15
|
+
},
|
16
|
+
"testrelarray":[
|
17
|
+
{
|
18
|
+
"href":"http://example.org/api/user/test1",
|
19
|
+
"templated":true,
|
20
|
+
"type":"some-type",
|
21
|
+
"deprecation":"http://very-deprecated.com",
|
22
|
+
"name":"some-name",
|
23
|
+
"profile":"some-profile",
|
24
|
+
"title":"A Great Title",
|
25
|
+
"hreflang":"en-US"
|
26
|
+
},
|
27
|
+
{
|
28
|
+
"href":"http://example.org/api/user/test2",
|
29
|
+
"templated":true,
|
30
|
+
"type":"some-type",
|
31
|
+
"deprecation":"http://very-deprecated.com",
|
32
|
+
"name":"some-name",
|
33
|
+
"profile":"some-profile",
|
34
|
+
"title":"A Great Title",
|
35
|
+
"hreflang":"en-US"
|
36
|
+
}
|
37
|
+
]
|
38
|
+
},
|
39
|
+
"some-property":123,
|
40
|
+
"_embedded":{
|
41
|
+
"embedded1":{
|
42
|
+
"_links":{
|
43
|
+
"self":{
|
44
|
+
"href":"http:/nice.com"
|
45
|
+
},
|
46
|
+
"testrel":{
|
47
|
+
"href":"http://example.org/api/user/test",
|
48
|
+
"templated":true,
|
49
|
+
"type":"some-type",
|
50
|
+
"deprecation":"http://very-deprecated.com",
|
51
|
+
"name":"some-name",
|
52
|
+
"profile":"some-profile",
|
53
|
+
"title":"A Great Title",
|
54
|
+
"hreflang":"en-US"
|
55
|
+
}
|
56
|
+
},
|
57
|
+
"some-property":1234
|
58
|
+
},
|
59
|
+
"embeddedarray":[
|
60
|
+
{
|
61
|
+
"_links":{
|
62
|
+
"self":{
|
63
|
+
"href":"http:/nice.com"
|
64
|
+
},
|
65
|
+
"testrel":{
|
66
|
+
"href":"http://example.org/api/user/test",
|
67
|
+
"templated":true,
|
68
|
+
"type":"some-type",
|
69
|
+
"deprecation":"http://very-deprecated.com",
|
70
|
+
"name":"some-name",
|
71
|
+
"profile":"some-profile",
|
72
|
+
"title":"A Great Title",
|
73
|
+
"hreflang":"en-US"
|
74
|
+
}
|
75
|
+
},
|
76
|
+
"some-property":1234
|
77
|
+
},
|
78
|
+
{
|
79
|
+
"_links":{
|
80
|
+
"self":{
|
81
|
+
"href":"http:/nice.com"
|
82
|
+
},
|
83
|
+
"testrel":{
|
84
|
+
"href":"http://example.org/api/user/test",
|
85
|
+
"templated":true,
|
86
|
+
"type":"some-type",
|
87
|
+
"deprecation":"http://very-deprecated.com",
|
88
|
+
"name":"some-name",
|
89
|
+
"profile":"some-profile",
|
90
|
+
"title":"A Great Title",
|
91
|
+
"hreflang":"en-US"
|
92
|
+
}
|
93
|
+
},
|
94
|
+
"some-property":1234
|
95
|
+
}
|
96
|
+
]
|
97
|
+
}
|
98
|
+
}
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'open-uri'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
|
5
|
+
def convert_to_representor(index, example)
|
6
|
+
representor = Representors::HaleDeserializer.new(example).to_representor
|
7
|
+
serialized_representor = Representors::Serialization::HaleSerializer.new(representor).to_media_type
|
8
|
+
if JSON.parse(serialized_representor) != JSON.parse(example)
|
9
|
+
puts example
|
10
|
+
puts "Example number #{index} can not be roundtriped!"
|
11
|
+
else
|
12
|
+
puts "Example number #{index} was roundtriped, HOORAY!"
|
13
|
+
end
|
14
|
+
|
15
|
+
rescue JSON::ParserError
|
16
|
+
puts example
|
17
|
+
puts "Example number #{index} has a JSON error!"
|
18
|
+
rescue TypeError => e
|
19
|
+
puts example
|
20
|
+
puts "Example number #{index} breaks our code!"
|
21
|
+
puts e
|
22
|
+
end
|
23
|
+
|
24
|
+
#TODO: Test Hal spec also
|
25
|
+
desc "Rake tast to test the implementation against the specs"
|
26
|
+
task :test_specs do
|
27
|
+
|
28
|
+
hale_spec = ''
|
29
|
+
hale_spec << open('https://raw.githubusercontent.com/mdsol/hale/master/README.md').read
|
30
|
+
|
31
|
+
examples = hale_spec.scan(/```json(.*?)```/m).flatten
|
32
|
+
|
33
|
+
examples.each_with_index do |example, index|
|
34
|
+
convert_to_representor(index+1, example)
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|