yaks 0.9.0 → 0.10.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 +158 -56
- data/Rakefile +1 -3
- data/ataru_setup.rb +72 -0
- data/find_missing_tests.rb +34 -0
- data/lib/yaks.rb +8 -10
- data/lib/yaks/breaking_changes.rb +4 -6
- data/lib/yaks/builder.rb +1 -1
- data/lib/yaks/changelog.rb +1 -1
- data/lib/yaks/collection_mapper.rb +8 -5
- data/lib/yaks/collection_resource.rb +0 -1
- data/lib/yaks/config.rb +17 -7
- data/lib/yaks/configurable.rb +20 -14
- data/lib/yaks/default_policy.rb +82 -33
- data/lib/yaks/format.rb +7 -3
- data/lib/yaks/format/collection_json.rb +4 -4
- data/lib/yaks/format/hal.rb +1 -2
- data/lib/yaks/format/halo.rb +2 -4
- data/lib/yaks/format/json_api.rb +46 -27
- data/lib/yaks/html5_forms.rb +0 -2
- data/lib/yaks/mapper.rb +5 -5
- data/lib/yaks/mapper/association.rb +7 -7
- data/lib/yaks/mapper/association_mapper.rb +2 -0
- data/lib/yaks/mapper/attribute.rb +10 -4
- data/lib/yaks/mapper/config.rb +2 -2
- data/lib/yaks/mapper/form.rb +4 -10
- data/lib/yaks/mapper/form/config.rb +16 -17
- data/lib/yaks/mapper/form/dynamic_field.rb +1 -1
- data/lib/yaks/mapper/form/field.rb +16 -7
- data/lib/yaks/mapper/form/field/option.rb +5 -4
- data/lib/yaks/mapper/form/fieldset.rb +1 -1
- data/lib/yaks/mapper/form/legend.rb +18 -0
- data/lib/yaks/mapper/has_many.rb +1 -0
- data/lib/yaks/mapper/link.rb +7 -4
- data/lib/yaks/null_resource.rb +4 -5
- data/lib/yaks/pipeline.rb +2 -2
- data/lib/yaks/primitivize.rb +3 -2
- data/lib/yaks/reader/hal.rb +12 -13
- data/lib/yaks/reader/json_api.rb +50 -33
- data/lib/yaks/resource.rb +6 -7
- data/lib/yaks/resource/form.rb +2 -12
- data/lib/yaks/resource/form/field.rb +4 -3
- data/lib/yaks/resource/form/field/option.rb +1 -1
- data/lib/yaks/resource/form/fieldset.rb +1 -1
- data/lib/yaks/resource/form/legend.rb +18 -0
- data/lib/yaks/resource/has_fields.rb +13 -7
- data/lib/yaks/resource/link.rb +1 -1
- data/lib/yaks/runner.rb +5 -2
- data/lib/yaks/serializer.rb +2 -3
- data/lib/yaks/util.rb +7 -8
- data/lib/yaks/version.rb +1 -1
- data/spec/acceptance/acceptance_spec.rb +53 -38
- data/spec/acceptance/json_shared_examples.rb +45 -12
- data/spec/acceptance/models.rb +1 -1
- data/spec/integration/dynamic_form_fields_spec.rb +0 -1
- data/spec/integration/fieldset_spec.rb +18 -20
- data/spec/integration/map_to_resource_spec.rb +6 -6
- data/spec/json/{confucius.collection.json → confucius.collection_json.json} +0 -0
- data/spec/json/confucius.json_api.json +43 -27
- data/spec/json/list_of_quotes.collection_json.json +43 -0
- data/spec/json/list_of_quotes.hal.json +18 -0
- data/spec/json/list_of_quotes.json_api.json +25 -0
- data/spec/json/youtypeitwepostit.collection_json.json +45 -0
- data/spec/spec_helper.rb +4 -3
- data/spec/support/classes_for_policy_testing.rb +38 -14
- data/spec/support/deep_eql.rb +21 -18
- data/spec/support/pet_mapper.rb +2 -0
- data/spec/support/shared_contexts.rb +9 -9
- data/spec/unit/yaks/builder_spec.rb +41 -18
- data/spec/unit/yaks/collection_mapper_spec.rb +22 -19
- data/spec/unit/yaks/collection_resource_spec.rb +16 -8
- data/spec/unit/yaks/config_spec.rb +215 -19
- data/spec/unit/yaks/configurable_spec.rb +66 -7
- data/spec/unit/yaks/default_policy/derive_mapper_from_collection_spec.rb +47 -0
- data/spec/unit/yaks/default_policy/derive_mapper_from_item_spec.rb +114 -0
- data/spec/unit/yaks/default_policy/derive_mapper_from_object_spec.rb +29 -71
- data/spec/unit/yaks/default_policy_spec.rb +4 -5
- data/spec/unit/yaks/format/collection_json_spec.rb +35 -36
- data/spec/unit/yaks/format/hal_spec.rb +3 -3
- data/spec/unit/yaks/format/json_api_spec.rb +109 -68
- data/spec/unit/yaks/format_spec.rb +34 -0
- data/spec/unit/yaks/fp/callable_spec.rb +5 -3
- data/spec/unit/yaks/mapper/association_mapper_spec.rb +22 -4
- data/spec/unit/yaks/mapper/association_spec.rb +23 -11
- data/spec/unit/yaks/mapper/attribute_spec.rb +46 -7
- data/spec/unit/yaks/mapper/config_spec.rb +2 -3
- data/spec/unit/yaks/mapper/form/config_spec.rb +95 -0
- data/spec/unit/yaks/mapper/form/dynamic_field_spec.rb +30 -0
- data/spec/unit/yaks/mapper/form/field/option_spec.rb +48 -4
- data/spec/unit/yaks/mapper/form/field_spec.rb +43 -2
- data/spec/unit/yaks/mapper/form/fieldset_spec.rb +67 -8
- data/spec/unit/yaks/mapper/form/legend_spec.rb +52 -0
- data/spec/unit/yaks/mapper/form_spec.rb +84 -23
- data/spec/unit/yaks/mapper/has_many_spec.rb +39 -36
- data/spec/unit/yaks/mapper/has_one_spec.rb +28 -20
- data/spec/unit/yaks/mapper/link_spec.rb +68 -16
- data/spec/unit/yaks/mapper_spec.rb +118 -30
- data/spec/unit/yaks/null_resource_spec.rb +83 -52
- data/spec/unit/yaks/pipeline_spec.rb +101 -74
- data/spec/unit/yaks/primitivize_spec.rb +25 -6
- data/spec/unit/yaks/resource/form/field_spec.rb +5 -5
- data/spec/unit/yaks/resource/form/fieldset_spec.rb +7 -0
- data/spec/unit/yaks/resource/form/legend_spec.rb +8 -0
- data/spec/unit/yaks/resource/form_spec.rb +17 -37
- data/spec/unit/yaks/resource/has_fields_spec.rb +44 -3
- data/spec/unit/yaks/resource/link_spec.rb +11 -6
- data/spec/unit/yaks/resource_spec.rb +87 -98
- data/spec/unit/yaks/runner_spec.rb +112 -28
- data/spec/unit/yaks/serializer_spec.rb +1 -1
- data/spec/unit/yaks/util_spec.rb +30 -10
- data/spec/yaml/list_of_quotes.yaml +13 -0
- data/yaks.gemspec +21 -13
- metadata +129 -41
- data/lib/yaks/attributes.rb +0 -86
- data/lib/yaks/fp.rb +0 -26
- data/lib/yaks/identifier/link_relation.rb +0 -17
- data/resources/iana-link-relations.csv +0 -152
- data/spec/json/youtypeitwepostit.collection.json +0 -45
- data/spec/unit/yaks/attributes_spec.rb +0 -178
- data/spec/unit/yaks/fp_spec.rb +0 -29
@@ -2,8 +2,9 @@ module Yaks
|
|
2
2
|
class Resource
|
3
3
|
class Form
|
4
4
|
class Field
|
5
|
-
include Yaks::Mapper::Form::Field.attributes.add(:
|
5
|
+
include Yaks::Mapper::Form::Field.attributes.add(error: nil)
|
6
6
|
|
7
|
+
undef value
|
7
8
|
def value
|
8
9
|
if type.equal? :select
|
9
10
|
selected = options.find(&:selected)
|
@@ -30,9 +31,9 @@ module Yaks
|
|
30
31
|
options.each_with_object([]) do |option, new_opts|
|
31
32
|
new_opts << case option
|
32
33
|
when unset
|
33
|
-
option.
|
34
|
+
option.with selected: false
|
34
35
|
when set
|
35
|
-
option.
|
36
|
+
option.with selected: true
|
36
37
|
else
|
37
38
|
option
|
38
39
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Yaks
|
2
|
+
class Resource
|
3
|
+
class Form
|
4
|
+
class Legend
|
5
|
+
include Attribs.new(:label, :type)
|
6
|
+
|
7
|
+
def initialize(opts)
|
8
|
+
super(opts.merge(type: :legend))
|
9
|
+
end
|
10
|
+
|
11
|
+
# Up to 0.9.0 legends were represented as Form::Field
|
12
|
+
# instances with the label stored as name, hence this alias
|
13
|
+
# for compatibility
|
14
|
+
alias_method :name, :label
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -3,15 +3,21 @@ module Yaks
|
|
3
3
|
module HasFields
|
4
4
|
def map_fields(&block)
|
5
5
|
with(
|
6
|
-
fields:
|
7
|
-
if field.type.equal? :fieldset
|
8
|
-
field.map_fields(&block)
|
9
|
-
else
|
10
|
-
block.call(field)
|
11
|
-
end
|
12
|
-
end
|
6
|
+
fields: fields_flat(&block)
|
13
7
|
)
|
14
8
|
end
|
9
|
+
|
10
|
+
def fields_flat(&block)
|
11
|
+
return to_enum(__method__) unless block_given?
|
12
|
+
fields.map do |field|
|
13
|
+
next field if field.type.equal? :legend
|
14
|
+
if field.respond_to?(:map_fields)
|
15
|
+
field.map_fields(&block)
|
16
|
+
else
|
17
|
+
block.call(field)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
15
21
|
end
|
16
22
|
end
|
17
23
|
end
|
data/lib/yaks/resource/link.rb
CHANGED
data/lib/yaks/runner.rb
CHANGED
@@ -14,7 +14,11 @@ module Yaks
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def read
|
17
|
-
Pipeline.new([[:parse, serializer.inverse], [:
|
17
|
+
Pipeline.new([[:parse, serializer.inverse], [:read, formatter.inverse]]).insert_hooks(hooks).call(object, env)
|
18
|
+
end
|
19
|
+
|
20
|
+
def format
|
21
|
+
Pipeline.new([[:format, formatter], [:primitivize, primitivizer]]).insert_hooks(hooks).call(object, env)
|
18
22
|
end
|
19
23
|
|
20
24
|
def map
|
@@ -82,6 +86,5 @@ module Yaks
|
|
82
86
|
def hooks
|
83
87
|
config.hooks + options.fetch(:hooks, [])
|
84
88
|
end
|
85
|
-
|
86
89
|
end
|
87
90
|
end
|
data/lib/yaks/serializer.rb
CHANGED
@@ -12,7 +12,7 @@ module Yaks
|
|
12
12
|
module JSONWriter
|
13
13
|
extend Yaks::FP::Callable
|
14
14
|
|
15
|
-
def self.call(data,
|
15
|
+
def self.call(data, _env)
|
16
16
|
JSON.pretty_generate(data)
|
17
17
|
end
|
18
18
|
|
@@ -28,7 +28,7 @@ module Yaks
|
|
28
28
|
module JSONReader
|
29
29
|
extend Yaks::FP::Callable
|
30
30
|
|
31
|
-
def self.call(data,
|
31
|
+
def self.call(data, _env)
|
32
32
|
JSON.parse(data)
|
33
33
|
end
|
34
34
|
|
@@ -40,6 +40,5 @@ module Yaks
|
|
40
40
|
JSONWriter
|
41
41
|
end
|
42
42
|
end
|
43
|
-
|
44
43
|
end
|
45
44
|
end
|
data/lib/yaks/util.rb
CHANGED
@@ -7,30 +7,30 @@ module Yaks
|
|
7
7
|
|
8
8
|
def underscore(str)
|
9
9
|
str.gsub(/::/, '/')
|
10
|
-
.gsub(
|
10
|
+
.gsub(%r{(?<!^|/)([A-Z])(?=[a-z$])|(?<=[a-z])([A-Z])}, '_\1\2')
|
11
11
|
.tr("-", "_")
|
12
12
|
.downcase
|
13
13
|
end
|
14
14
|
|
15
15
|
def camelize(str)
|
16
|
-
str.gsub(
|
16
|
+
str.gsub(%r{/(.?)}) { "::#{ $1.upcase }" }
|
17
17
|
.gsub!(/(?:^|_)(.)/) { $1.upcase }
|
18
18
|
end
|
19
19
|
|
20
20
|
def slice_hash(hash, *keys)
|
21
|
-
keys.each_with_object({}) {|k,dest| dest[k] = hash[k] if hash.key?(k) }
|
21
|
+
keys.each_with_object({}) {|k, dest| dest[k] = hash[k] if hash.key?(k) }
|
22
22
|
end
|
23
23
|
|
24
24
|
def reject_keys(hash, *keys)
|
25
|
-
hash.keys.each_with_object({}) {|k,dest| dest[k] = hash[k] unless keys.include?(k) }
|
25
|
+
hash.keys.each_with_object({}) {|k, dest| dest[k] = hash[k] unless keys.include?(k) }
|
26
26
|
end
|
27
27
|
|
28
28
|
def symbolize_keys(hash)
|
29
|
-
hash.each_with_object({}) {|(k,v), hsh| hsh[k.to_sym] = v}
|
29
|
+
hash.each_with_object({}) {|(k, v), hsh| hsh[k.to_sym] = v}
|
30
30
|
end
|
31
31
|
|
32
32
|
def extract_options(args)
|
33
|
-
args.last.
|
33
|
+
args.last.instance_of?(Hash) ? [args[0..-2], args.last] : [args, {}]
|
34
34
|
end
|
35
35
|
|
36
36
|
# Turn what is maybe a Proc into its result (or itself)
|
@@ -46,7 +46,7 @@ module Yaks
|
|
46
46
|
# A proc or a plain value
|
47
47
|
# @param [Object] context
|
48
48
|
# (optional) A context used to instance_eval the proc
|
49
|
-
def Resolve(maybe_proc, context = nil)
|
49
|
+
def Resolve(maybe_proc, context = nil) # rubocop:disable Style/MethodName
|
50
50
|
if maybe_proc.respond_to?(:to_proc) && !maybe_proc.instance_of?(Symbol)
|
51
51
|
if context
|
52
52
|
if maybe_proc.arity > 0
|
@@ -71,6 +71,5 @@ module Yaks
|
|
71
71
|
end
|
72
72
|
end
|
73
73
|
end
|
74
|
-
|
75
74
|
end
|
76
75
|
end
|
data/lib/yaks/version.rb
CHANGED
@@ -2,59 +2,74 @@ require 'acceptance/models'
|
|
2
2
|
require 'acceptance/json_shared_examples'
|
3
3
|
|
4
4
|
RSpec.describe Yaks::Format::Hal do
|
5
|
-
|
6
|
-
format_options :hal, plural_links: ['http://literature.example.com/rels/quotes']
|
7
|
-
rel_template "http://literature.example.com/rel/{rel}"
|
8
|
-
skip :serialize
|
9
|
-
end
|
5
|
+
let(:format_name) { :hal }
|
10
6
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
7
|
+
context 'with a configured rel template' do
|
8
|
+
let(:yaks_config) {
|
9
|
+
Yaks.new do
|
10
|
+
format_options :hal, plural_links: ['http://literature.example.com/rels/quotes']
|
11
|
+
rel_template "http://literature.example.com/rel/{rel}"
|
12
|
+
end
|
13
|
+
}
|
14
|
+
|
15
|
+
include_examples 'JSON Writer', 'confucius'
|
16
|
+
include_examples 'JSON round trip', 'confucius'
|
17
17
|
end
|
18
18
|
|
19
|
-
|
20
|
-
|
19
|
+
context 'with a rel computed by a policy override' do
|
20
|
+
let(:yaks_config) {
|
21
|
+
Yaks.new do
|
22
|
+
format_options :hal, plural_links: ['http://literature.example.com/rels/quotes']
|
23
|
+
derive_rel_from_association do |association|
|
24
|
+
"http://literature.example.com/rel/#{association.name}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
}
|
21
28
|
|
22
|
-
|
23
|
-
|
29
|
+
include_examples 'JSON Writer', 'confucius'
|
30
|
+
include_examples 'JSON round trip', 'confucius'
|
31
|
+
include_examples 'JSON Writer', 'list_of_quotes'
|
32
|
+
include_examples 'JSON round trip', 'list_of_quotes'
|
33
|
+
end
|
24
34
|
end
|
25
35
|
|
26
36
|
RSpec.describe Yaks::Format::Halo do
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
37
|
+
let(:format_name) { :halo }
|
38
|
+
let(:yaks_config) {
|
39
|
+
Yaks.new do
|
40
|
+
default_format :halo
|
41
|
+
rel_template "http://literature.example.com/rel/{rel}"
|
42
|
+
end
|
43
|
+
}
|
32
44
|
|
33
|
-
include_examples 'JSON
|
45
|
+
include_examples 'JSON Writer', 'confucius'
|
34
46
|
end
|
35
47
|
|
36
48
|
RSpec.describe Yaks::Format::JsonAPI do
|
37
|
-
|
38
|
-
|
39
|
-
skip :serialize
|
40
|
-
end
|
49
|
+
let(:format_name) { :json_api }
|
50
|
+
let(:yaks_config) { Yaks.new }
|
41
51
|
|
42
|
-
include_examples 'JSON
|
43
|
-
include_examples 'JSON
|
52
|
+
include_examples 'JSON Writer', 'confucius'
|
53
|
+
# include_examples 'JSON Reader', 'confucius'
|
54
|
+
include_examples 'JSON round trip', 'confucius'
|
55
|
+
include_examples 'JSON Writer', 'list_of_quotes'
|
56
|
+
# include_examples 'JSON round trip', 'list_of_quotes'
|
44
57
|
end
|
45
58
|
|
46
59
|
RSpec.describe Yaks::Format::CollectionJson do
|
47
|
-
|
48
|
-
|
49
|
-
mapper_namespace Youtypeitwepostit
|
50
|
-
skip :serialize
|
51
|
-
end
|
60
|
+
let(:format_name) { :collection_json }
|
61
|
+
let(:yaks_config) { Yaks.new }
|
52
62
|
|
53
|
-
|
54
|
-
|
55
|
-
skip :serialize
|
56
|
-
end
|
63
|
+
include_examples 'JSON Writer', 'confucius'
|
64
|
+
include_examples 'JSON Writer', 'list_of_quotes'
|
57
65
|
|
58
|
-
|
59
|
-
|
66
|
+
context 'with a namespace' do
|
67
|
+
let(:yaks_config) {
|
68
|
+
Yaks.new do
|
69
|
+
mapper_namespace Youtypeitwepostit
|
70
|
+
end
|
71
|
+
}
|
72
|
+
|
73
|
+
include_examples 'JSON Writer', 'youtypeitwepostit'
|
74
|
+
end
|
60
75
|
end
|
@@ -1,19 +1,52 @@
|
|
1
|
-
RSpec.shared_examples_for 'JSON
|
2
|
-
|
3
|
-
|
1
|
+
RSpec.shared_examples_for 'JSON Writer' do |fixture_name|
|
2
|
+
describe 'Yaks::Resource => JSON' do
|
3
|
+
let(:object) { load_yaml_fixture(fixture_name) }
|
4
|
+
let(:json_fixture) { load_json_fixture("#{fixture_name}.#{format_name}") }
|
5
|
+
let(:serialized) {
|
6
|
+
yaks_config.call(object, hooks: [[:skip, :serialize]], format: format_name)
|
7
|
+
}
|
4
8
|
|
5
|
-
|
9
|
+
# before do
|
10
|
+
# puts "============================expected========================================="
|
11
|
+
# puts JSON.pretty_generate(expected)
|
12
|
+
# puts "=================yaks.call(object, format: #{format.inspect})================"
|
13
|
+
# puts JSON.pretty_generate(serialized)
|
14
|
+
# end
|
6
15
|
|
7
|
-
|
16
|
+
it 'should match the JSON fixture' do
|
17
|
+
expect(serialized).to deep_eql json_fixture
|
18
|
+
end
|
19
|
+
end
|
8
20
|
end
|
9
21
|
|
10
|
-
RSpec.shared_examples_for 'JSON
|
11
|
-
|
22
|
+
RSpec.shared_examples_for 'JSON Reader' do |fixture_name|
|
23
|
+
describe 'JSON => Yaks::Resource' do
|
24
|
+
let(:object) { load_yaml_fixture(fixture_name) }
|
25
|
+
let(:json_fixture) { load_json_fixture("#{fixture_name}.#{format_name}") }
|
26
|
+
let(:resource) {
|
27
|
+
yaks_config.read(json_fixture, hooks: [[:skip, :parse]], format: format_name)
|
28
|
+
}
|
12
29
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
30
|
+
it 'should equal the corresponding Yaks::Resource' do
|
31
|
+
# Comparing type+to_h to get better RSpec output upon failure
|
32
|
+
expect(resource).to be_a Yaks::Resource
|
33
|
+
expect(resource.to_h).to eql yaks.map(object).to_h
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
RSpec.shared_examples_for 'JSON round trip' do |fixture_name|
|
39
|
+
describe 'JSON => Yaks::Resource => JSON' do
|
40
|
+
let(:json_fixture) { load_json_fixture("#{fixture_name}.#{format_name}") }
|
41
|
+
let(:read_and_written) {
|
42
|
+
config = yaks_config.with(default_format: format_name)
|
43
|
+
config.format(
|
44
|
+
config.read(json_fixture, hooks: [[:skip, :parse]])
|
45
|
+
)
|
46
|
+
}
|
17
47
|
|
18
|
-
|
48
|
+
specify 'it should be identical' do
|
49
|
+
expect(read_and_written).to deep_eql json_fixture
|
50
|
+
end
|
51
|
+
end
|
19
52
|
end
|
data/spec/acceptance/models.rb
CHANGED
@@ -22,7 +22,7 @@ RSpec.describe 'dynamic form fields' do
|
|
22
22
|
fields: [
|
23
23
|
Yaks::Resource::Form::Fieldset.new(
|
24
24
|
fields: [
|
25
|
-
Yaks::Resource::Form::
|
25
|
+
Yaks::Resource::Form::Legend.new(label: "I am legend", type: :legend),
|
26
26
|
Yaks::Resource::Form::Field.new(name: :bar, type: :text)
|
27
27
|
]
|
28
28
|
)
|
@@ -36,25 +36,23 @@ RSpec.describe 'dynamic form fields' do
|
|
36
36
|
expect(
|
37
37
|
yaks.with(default_format: :halo, hooks: [[:skip, :serialize]]).call(:foo, mapper: mapper)
|
38
38
|
).to eql(
|
39
|
-
{
|
40
|
-
"
|
41
|
-
"
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
]
|
57
|
-
}
|
39
|
+
"_controls" => {
|
40
|
+
"foo" => {
|
41
|
+
"name" => "foo",
|
42
|
+
"fields" => [
|
43
|
+
{
|
44
|
+
"type" => "fieldset",
|
45
|
+
"fields" => [
|
46
|
+
{
|
47
|
+
"label" => "I am legend",
|
48
|
+
"type" => "legend"
|
49
|
+
}, {
|
50
|
+
"name" => "bar",
|
51
|
+
"type" => "text"
|
52
|
+
}
|
53
|
+
]
|
54
|
+
}
|
55
|
+
]
|
58
56
|
}
|
59
57
|
}
|
60
58
|
)
|