yaks 0.8.3 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +12 -0
- data/lib/yaks.rb +4 -2
- data/lib/yaks/attributes.rb +1 -1
- data/lib/yaks/builder.rb +10 -10
- data/lib/yaks/collection_mapper.rb +1 -1
- data/lib/yaks/config.rb +1 -1
- data/lib/yaks/configurable.rb +41 -2
- data/lib/yaks/format/json_api.rb +21 -35
- data/lib/yaks/mapper.rb +2 -1
- data/lib/yaks/mapper/form.rb +6 -29
- data/lib/yaks/mapper/form/config.rb +37 -3
- data/lib/yaks/mapper/form/dynamic_field.rb +17 -0
- data/lib/yaks/mapper/form/field.rb +13 -10
- data/lib/yaks/mapper/form/field/option.rb +2 -1
- data/lib/yaks/mapper/form/fieldset.rb +7 -29
- data/lib/yaks/reader/hal.rb +0 -1
- data/lib/yaks/reader/json_api.rb +49 -0
- data/lib/yaks/version.rb +1 -1
- data/spec/acceptance/acceptance_spec.rb +1 -0
- data/spec/json/confucius.json_api.json +19 -20
- data/spec/unit/yaks/attributes_spec.rb +37 -1
- data/spec/unit/yaks/builder_spec.rb +34 -3
- data/spec/unit/yaks/collection_mapper_spec.rb +20 -1
- data/spec/unit/yaks/collection_resource_spec.rb +7 -0
- data/spec/unit/yaks/configurable_spec.rb +84 -0
- data/spec/unit/yaks/format/json_api_spec.rb +91 -2
- data/spec/unit/yaks/mapper/form/field_spec.rb +63 -5
- data/spec/unit/yaks/mapper/form/fieldset_spec.rb +30 -0
- data/spec/unit/yaks/mapper/form_spec.rb +25 -1
- metadata +8 -2
@@ -0,0 +1,17 @@
|
|
1
|
+
module Yaks
|
2
|
+
class Mapper
|
3
|
+
class Form
|
4
|
+
class DynamicField
|
5
|
+
include Attributes.new(:block)
|
6
|
+
|
7
|
+
def self.create(_opts = nil, &block)
|
8
|
+
new(block: block)
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_resource_fields(mapper)
|
12
|
+
Config.build_with_object(mapper.object, &block).to_resource_fields(mapper)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -5,13 +5,16 @@ module Yaks
|
|
5
5
|
include Attributes.new(
|
6
6
|
:name,
|
7
7
|
label: nil,
|
8
|
-
options: [].freeze
|
8
|
+
options: [].freeze,
|
9
|
+
if: nil
|
9
10
|
).add(HTML5Forms::FIELD_OPTIONS)
|
10
11
|
|
11
12
|
Builder = Builder.new(self) do
|
12
|
-
def_set :name
|
13
|
-
def_set :label
|
13
|
+
def_set :name, :label
|
14
14
|
def_add :option, create: Option, append_to: :options
|
15
|
+
HTML5Forms::FIELD_OPTIONS.each do |option, _|
|
16
|
+
def_set option
|
17
|
+
end
|
15
18
|
end
|
16
19
|
|
17
20
|
def self.create(*args)
|
@@ -24,18 +27,18 @@ module Yaks
|
|
24
27
|
|
25
28
|
# Convert to a Resource::Form::Field, expanding any dynamic
|
26
29
|
# values
|
27
|
-
def
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
30
|
+
def to_resource_fields(mapper)
|
31
|
+
return [] if self.if && !mapper.expand_value(self.if)
|
32
|
+
[ Resource::Form::Field.new(
|
33
|
+
(resource_attributes - [:if]).each_with_object({}) do |attr, attrs|
|
34
|
+
attrs[attr] = mapper.expand_value(public_send(attr))
|
35
|
+
end.merge(options: resource_options(mapper))) ]
|
33
36
|
end
|
34
37
|
|
35
38
|
def resource_options(mapper)
|
36
39
|
# make sure all empty options arrays are the same instance,
|
37
40
|
# makes for prettier #pp
|
38
|
-
options.empty? ? options : options.map {|opt| opt.to_resource_field_option(mapper) }
|
41
|
+
options.empty? ? options : options.map {|opt| opt.to_resource_field_option(mapper) }.compact
|
39
42
|
end
|
40
43
|
|
41
44
|
# All attributes that can be converted 1-to-1 to
|
@@ -4,13 +4,14 @@ module Yaks
|
|
4
4
|
class Field
|
5
5
|
# <option>, as used in a <select>
|
6
6
|
class Option
|
7
|
-
include Attributes.new(:value, :label, selected: false, disabled: false)
|
7
|
+
include Attributes.new(:value, :label, selected: false, disabled: false, if: nil)
|
8
8
|
|
9
9
|
def self.create(value, opts = {})
|
10
10
|
new(opts.merge(value: value))
|
11
11
|
end
|
12
12
|
|
13
13
|
def to_resource_field_option(mapper)
|
14
|
+
return if self.if && !mapper.expand_value(self.if)
|
14
15
|
Resource::Form::Field::Option.new(
|
15
16
|
value: mapper.expand_value(value),
|
16
17
|
label: mapper.expand_value(label),
|
@@ -3,39 +3,17 @@ module Yaks
|
|
3
3
|
class Form
|
4
4
|
class Fieldset
|
5
5
|
extend Forwardable
|
6
|
-
include
|
6
|
+
include Attributes.new(:config)
|
7
7
|
|
8
|
-
def_delegators :config, :fields
|
8
|
+
def_delegators :config, :fields
|
9
9
|
|
10
|
-
|
11
|
-
|
12
|
-
def_add :fieldset, create: Fieldset, append_to: :fields
|
13
|
-
HTML5Forms::INPUT_TYPES.each do |type|
|
14
|
-
def_add(type,
|
15
|
-
create: Field::Builder,
|
16
|
-
append_to: :fields,
|
17
|
-
defaults: { type: type }
|
18
|
-
)
|
19
|
-
end
|
20
|
-
def_forward :dynamic
|
10
|
+
def self.create(options = {}, &block)
|
11
|
+
new(config: Config.build(options, &block))
|
21
12
|
end
|
22
13
|
|
23
|
-
def
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
def to_resource(mapper)
|
28
|
-
config = dynamic_blocks.inject(self.config) do |config, block|
|
29
|
-
ConfigBuilder.build(config, mapper.object, &block)
|
30
|
-
end
|
31
|
-
|
32
|
-
resource_fields = resource_fields(config.fields, mapper)
|
33
|
-
|
34
|
-
Resource::Form::Fieldset.new(fields: resource_fields)
|
35
|
-
end
|
36
|
-
|
37
|
-
def resource_fields(fields, mapper)
|
38
|
-
fields.map { |field| field.to_resource(mapper) }
|
14
|
+
def to_resource_fields(mapper)
|
15
|
+
return [] if config.if && !mapper.expand_value(config.if)
|
16
|
+
[ Resource::Form::Fieldset.new(fields: config.to_resource_fields(mapper)) ]
|
39
17
|
end
|
40
18
|
end
|
41
19
|
end
|
data/lib/yaks/reader/hal.rb
CHANGED
@@ -7,7 +7,6 @@ module Yaks
|
|
7
7
|
links = convert_links(attributes.delete('_links') || {})
|
8
8
|
embedded = convert_embedded(attributes.delete('_embedded') || {})
|
9
9
|
|
10
|
-
|
11
10
|
Resource.new(
|
12
11
|
type: attributes.delete('type') || type_from_links(links),
|
13
12
|
attributes: Util.symbolize_keys(attributes),
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Yaks
|
2
|
+
module Reader
|
3
|
+
class JsonAPI
|
4
|
+
def call(parsed_json, env = {})
|
5
|
+
attributes = parsed_json['data'].first.dup
|
6
|
+
links = attributes.delete('links') || {}
|
7
|
+
linked = parsed_json['linked'].nil? ? {} : parsed_json['linked'].dup
|
8
|
+
embedded = convert_embedded(links, linked)
|
9
|
+
Resource.new(
|
10
|
+
type: Util.singularize(attributes.delete('type')[/\w+$/]),
|
11
|
+
attributes: Util.symbolize_keys(attributes),
|
12
|
+
subresources: embedded,
|
13
|
+
links: []
|
14
|
+
)
|
15
|
+
end
|
16
|
+
|
17
|
+
def convert_embedded(links, linked)
|
18
|
+
links.flat_map do |rel, link_data|
|
19
|
+
# If this is a compound link, the link_data will contain either
|
20
|
+
# * 'type' and 'id' for a one to one
|
21
|
+
# * 'type' and 'ids' for a homogeneous to-many relationship
|
22
|
+
# * 'data' being an array where each member has 'type' and 'id' for heterogeneous
|
23
|
+
if !link_data['type'].nil? && !link_data['id'].nil?
|
24
|
+
resource = linked.find{ |item| (item['id'] == link_data['id']) && (item['type'] == link_data['type']) }
|
25
|
+
call({'data' => [resource], 'linked' => linked}).with(rels: [rel])
|
26
|
+
elsif !link_data['type'].nil? && !link_data['ids'].nil?
|
27
|
+
resources = linked.select{ |item| (link_data['ids'].include? item['id']) && (item['type'] == link_data['type']) }
|
28
|
+
members = resources.map { |r|
|
29
|
+
call({'data' => [r], 'linked' => linked})
|
30
|
+
}
|
31
|
+
CollectionResource.new(
|
32
|
+
members: members,
|
33
|
+
type: link_data['type'],
|
34
|
+
rels: [rel]
|
35
|
+
)
|
36
|
+
elsif link_data['data'].present?
|
37
|
+
CollectionResource.new(
|
38
|
+
members: link_data['data'].map { |link|
|
39
|
+
resource = linked.find{ |item| (item['id'] == link['id']) && (item['type'] == link['type']) }
|
40
|
+
call({'data' => [resource], 'linked' => linked})
|
41
|
+
},
|
42
|
+
rels: [rel]
|
43
|
+
)
|
44
|
+
end
|
45
|
+
end.compact
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/lib/yaks/version.rb
CHANGED
@@ -1,51 +1,50 @@
|
|
1
1
|
{
|
2
|
-
"
|
2
|
+
"data": [
|
3
3
|
{
|
4
4
|
"id": 9,
|
5
|
+
"type": "scholars",
|
5
6
|
"href": "http://literature.example.com/authors/kongzi",
|
6
7
|
"name": "孔子",
|
7
8
|
"pinyin": "Kongzi",
|
8
9
|
"latinized": "Confucius",
|
9
10
|
"links": {
|
10
|
-
"works": [11,12]
|
11
|
+
"works": {"type": "works", "ids": [11,12]}
|
11
12
|
}
|
12
13
|
}
|
13
14
|
],
|
14
|
-
"linked":
|
15
|
-
"works": [
|
15
|
+
"linked": [
|
16
16
|
{
|
17
17
|
"id": 11,
|
18
|
+
"type": "works",
|
18
19
|
"href": "http://literature.example.com/work/11",
|
19
20
|
"chinese_name": "論語",
|
20
21
|
"english_name": "Analects",
|
21
22
|
"links": {
|
22
|
-
"quotes": [17, 18],
|
23
|
-
"era": 99
|
23
|
+
"quotes": {"type": "quotes", "ids": [17, 18]},
|
24
|
+
"era": {"type": "erae", "id": 99}
|
24
25
|
}
|
25
26
|
},
|
26
|
-
{
|
27
|
-
"id": 12,
|
28
|
-
"href": "http://literature.example.com/work/12",
|
29
|
-
"chinese_name": "易經",
|
30
|
-
"english_name": "Commentaries to the Yi-jing",
|
31
|
-
"links": {}
|
32
|
-
}
|
33
|
-
],
|
34
|
-
"quotes": [
|
35
27
|
{
|
36
28
|
"id": 17,
|
29
|
+
"type": "quotes",
|
37
30
|
"chinese": "廄焚。子退朝,曰:“傷人乎?” 不問馬。"
|
38
31
|
},
|
39
32
|
{
|
40
33
|
"id": 18,
|
34
|
+
"type": "quotes",
|
41
35
|
"chinese": "子曰:“其恕乎!己所不欲、勿施於人。”"
|
42
|
-
}
|
43
|
-
],
|
44
|
-
"erae": [
|
36
|
+
},
|
45
37
|
{
|
46
38
|
"id": 99,
|
39
|
+
"type": "erae",
|
47
40
|
"name": "Zhou Dynasty"
|
41
|
+
},
|
42
|
+
{
|
43
|
+
"id": 12,
|
44
|
+
"type": "works",
|
45
|
+
"href": "http://literature.example.com/work/12",
|
46
|
+
"chinese_name": "易經",
|
47
|
+
"english_name": "Commentaries to the Yi-jing"
|
48
48
|
}
|
49
|
-
|
50
|
-
}
|
49
|
+
]
|
51
50
|
}
|
@@ -38,7 +38,7 @@ RSpec.describe Yaks::Attributes do
|
|
38
38
|
end
|
39
39
|
|
40
40
|
context 'when extending' do
|
41
|
-
subject { Class.new(super()) { include attributes.add(baz: 7) } }
|
41
|
+
subject { Class.new(super()) { include attributes.add(baz: 7, bar: 4) } }
|
42
42
|
|
43
43
|
it 'should make the new attributes available' do
|
44
44
|
expect(subject.new(foo: 3, baz: 6).baz).to equal 6
|
@@ -48,6 +48,14 @@ RSpec.describe Yaks::Attributes do
|
|
48
48
|
expect(subject.new(foo: 3, baz: 6).foo).to equal 3
|
49
49
|
end
|
50
50
|
|
51
|
+
it 'should take new default values' do
|
52
|
+
expect(subject.new(foo: 3, baz: 6).bar).to equal 4
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'should make sure attribute names are uniq' do
|
56
|
+
expect(subject.attributes.names.length).to equal 3
|
57
|
+
end
|
58
|
+
|
51
59
|
context 'without any defaults' do
|
52
60
|
subject { Class.new(super()) { include attributes.add(:bax) } }
|
53
61
|
|
@@ -60,6 +68,34 @@ RSpec.describe Yaks::Attributes do
|
|
60
68
|
end
|
61
69
|
end
|
62
70
|
end
|
71
|
+
|
72
|
+
context 'when removing an attribute with a default' do
|
73
|
+
subject { Class.new(super()) { include attributes.remove(:bar) } }
|
74
|
+
|
75
|
+
it 'should still recognize attributes that were kept' do
|
76
|
+
expect(subject.new(foo: 2).foo).to equal 2
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'should no longer recognize the old attributes' do
|
80
|
+
expect { subject.new(foo: 3, bar: 3).bar }.to raise_error
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
context 'when removing an attribute without a default' do
|
85
|
+
subject { Class.new(super()) { include attributes.remove(:foo) } }
|
86
|
+
|
87
|
+
it 'should still recognize attributes that were kept' do
|
88
|
+
expect(subject.new(bar: 2).bar).to equal 2
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'should no longer recognize the old attributes' do
|
92
|
+
expect { subject.new(foo: 3).foo }.to raise_error
|
93
|
+
end
|
94
|
+
|
95
|
+
it 'should keep the defaults' do
|
96
|
+
expect(subject.new.bar).to equal 3
|
97
|
+
end
|
98
|
+
end
|
63
99
|
end
|
64
100
|
|
65
101
|
RSpec.describe Yaks::Attributes::InstanceMethods do
|
@@ -13,13 +13,12 @@ RSpec.describe Yaks::Builder do
|
|
13
13
|
def wrong_type(x, y)
|
14
14
|
"foo #{x} #{y}"
|
15
15
|
end
|
16
|
-
|
17
16
|
end
|
18
17
|
|
19
18
|
subject do
|
20
|
-
Yaks::Builder.new(Buildable) do
|
19
|
+
Yaks::Builder.new(Buildable, [:finalize]) do
|
21
20
|
def_set :foo, :bar
|
22
|
-
def_forward :
|
21
|
+
def_forward :wrong_type, :update
|
23
22
|
end
|
24
23
|
end
|
25
24
|
|
@@ -36,4 +35,36 @@ RSpec.describe Yaks::Builder do
|
|
36
35
|
expect( subject.create(3, 4) { finalize } ).to eql Buildable.new(foo: 7, bar: 8)
|
37
36
|
end
|
38
37
|
|
38
|
+
context 'with no methods to forward' do
|
39
|
+
subject do
|
40
|
+
Yaks::Builder.new(Buildable)
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'should still work' do
|
44
|
+
expect(subject.create(3,4)).to eql Buildable.new(foo: 3, bar: 4)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe '#build' do
|
49
|
+
it 'should pass on the initial state if no block is given' do
|
50
|
+
expect(subject.build(:foo)).to equal :foo
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'should pass any extra args to the block' do
|
54
|
+
expect(subject.build(Buildable.new(foo: 1, bar: 2), 9) {|f| foo(f)}).to eql(
|
55
|
+
Buildable.new(foo: 9, bar: 2)
|
56
|
+
)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe '#inspect' do
|
61
|
+
subject do
|
62
|
+
Yaks::Builder.new(Buildable, [:foo, :bar])
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'should show the class and methods' do
|
66
|
+
expect(subject.inspect).to eql '#<Builder Buildable [:foo, :bar]>'
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
39
70
|
end
|
@@ -3,12 +3,13 @@ RSpec.describe Yaks::CollectionMapper do
|
|
3
3
|
|
4
4
|
subject(:mapper) { mapper_class.new(context) }
|
5
5
|
let(:mapper_class) { described_class }
|
6
|
+
let(:mapper_stack) { [] }
|
6
7
|
|
7
8
|
let(:context) {
|
8
9
|
{ item_mapper: item_mapper,
|
9
10
|
policy: policy,
|
10
11
|
env: {},
|
11
|
-
mapper_stack:
|
12
|
+
mapper_stack: mapper_stack }
|
12
13
|
}
|
13
14
|
|
14
15
|
let(:collection) { [] }
|
@@ -25,6 +26,24 @@ RSpec.describe Yaks::CollectionMapper do
|
|
25
26
|
)
|
26
27
|
end
|
27
28
|
|
29
|
+
it 'should accept a second "env" argument' do
|
30
|
+
expect(mapper.call(collection, {})).to be_a Yaks::CollectionResource
|
31
|
+
end
|
32
|
+
|
33
|
+
context 'when at the top of the stack' do
|
34
|
+
it 'should have a "collection" rel derived from the type' do
|
35
|
+
expect(mapper.call(collection).rels).to eql ['rel:the_types']
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
context 'when not at the top of the stack' do
|
40
|
+
let(:mapper_stack) { [ :foo ]}
|
41
|
+
|
42
|
+
it 'should not have a rel' do
|
43
|
+
expect(mapper.call(collection).rels).to eql []
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
28
47
|
context 'with members' do
|
29
48
|
let(:collection) { [boingboing, wassup]}
|
30
49
|
let(:item_mapper) { PetMapper }
|
@@ -52,6 +52,13 @@ RSpec.describe Yaks::CollectionResource do
|
|
52
52
|
its(:rels) { should eq ['http://api.example.org/rels/orders'] }
|
53
53
|
|
54
54
|
its(:subresources) { should eql [] }
|
55
|
+
end
|
56
|
+
|
57
|
+
describe '#seq' do
|
58
|
+
let(:init_opts) { { members: [1,2,3] } }
|
55
59
|
|
60
|
+
it 'iterates over the members' do
|
61
|
+
expect(subject.seq.map(&:next)).to eql [2,3,4]
|
62
|
+
end
|
56
63
|
end
|
57
64
|
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
class Kitten
|
2
|
+
include Yaks::Attributes.new(:furriness)
|
3
|
+
|
4
|
+
def self.create(opts, &block)
|
5
|
+
level = opts[:fur_level]
|
6
|
+
level = block.call(level) if block
|
7
|
+
new(furriness: level)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class Hanky
|
12
|
+
include Yaks::Attributes.new(:stickyness, :size, :color)
|
13
|
+
|
14
|
+
def self.create(sticky, opts = {})
|
15
|
+
new(stickyness: sticky, size: opts[:size], color: opts[:color])
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
RSpec.describe Yaks::Configurable do
|
20
|
+
let(:suffix) { SecureRandom.hex(16) }
|
21
|
+
subject do
|
22
|
+
eval %Q<
|
23
|
+
class TestConfigurable#{suffix}
|
24
|
+
class Config
|
25
|
+
include Yaks::Attributes.new(color: 'blue', taste: 'sour', contents: [])
|
26
|
+
end
|
27
|
+
extend Yaks::Configurable
|
28
|
+
|
29
|
+
def_add :kitten, create: Kitten, append_to: :contents, defaults: {fur_level: 7}
|
30
|
+
def_add :cat, create: Kitten, append_to: :contents
|
31
|
+
def_add :hanky, create: Hanky, append_to: :contents, defaults: {size: 12, color: :red}
|
32
|
+
end
|
33
|
+
>
|
34
|
+
Kernel.const_get("TestConfigurable#{suffix}")
|
35
|
+
end
|
36
|
+
|
37
|
+
describe '.extended' do
|
38
|
+
it 'should initialize an empty config object' do
|
39
|
+
expect(subject.config).to eql subject::Config.new
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe '#def_add' do
|
44
|
+
it 'should add' do
|
45
|
+
subject.kitten(fur_level: 9)
|
46
|
+
expect(subject.config.contents).to eql [Kitten.new(furriness: 9)]
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'should use defaults if configured' do
|
50
|
+
subject.kitten
|
51
|
+
expect(subject.config.contents).to eql [Kitten.new(furriness: 7)]
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'should work without defaults configured' do
|
55
|
+
subject.cat(fur_level: 3)
|
56
|
+
expect(subject.config.contents).to eql [Kitten.new(furriness: 3)]
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'should pass on a block' do
|
60
|
+
subject.cat(fur_level: 3) {|l| l+3}
|
61
|
+
expect(subject.config.contents).to eql [Kitten.new(furriness: 6)]
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'should work with a create with positional arguments - defaults' do
|
65
|
+
subject.hanky(3)
|
66
|
+
expect(subject.config.contents).to eql [Hanky.new(stickyness: 3, size: 12, color: :red)]
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'should work with a create with positional arguments' do
|
70
|
+
subject.hanky(5, size: 15)
|
71
|
+
expect(subject.config.contents).to eql [Hanky.new(stickyness: 5, size: 15, color: :red)]
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
|
76
|
+
describe '#def_set' do
|
77
|
+
it 'should set' do
|
78
|
+
end
|
79
|
+
end
|
80
|
+
describe '#def_forward' do
|
81
|
+
it 'should forward' do
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|