active_model_serializers 0.10.0.rc2 → 0.10.0.rc3
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.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/.rubocop.yml +82 -0
- data/.rubocop_todo.yml +315 -0
- data/.simplecov +99 -0
- data/.travis.yml +8 -0
- data/CHANGELOG.md +9 -3
- data/Gemfile +39 -8
- data/README.md +55 -31
- data/Rakefile +29 -2
- data/active_model_serializers.gemspec +37 -13
- data/appveyor.yml +25 -0
- data/docs/README.md +29 -0
- data/docs/general/adapters.md +110 -0
- data/docs/general/configuration_options.md +11 -0
- data/docs/general/getting_started.md +73 -0
- data/docs/howto/add_pagination_links.md +112 -0
- data/docs/howto/add_root_key.md +51 -0
- data/docs/howto/outside_controller_use.md +42 -0
- data/lib/action_controller/serialization.rb +24 -33
- data/lib/active_model/serializable_resource.rb +70 -0
- data/lib/active_model/serializer.rb +50 -131
- data/lib/active_model/serializer/adapter.rb +84 -21
- data/lib/active_model/serializer/adapter/flatten_json.rb +9 -9
- data/lib/active_model/serializer/adapter/fragment_cache.rb +10 -13
- data/lib/active_model/serializer/adapter/json.rb +25 -28
- data/lib/active_model/serializer/adapter/json/fragment_cache.rb +2 -12
- data/lib/active_model/serializer/adapter/json_api.rb +100 -98
- data/lib/active_model/serializer/adapter/json_api/fragment_cache.rb +4 -14
- data/lib/active_model/serializer/adapter/json_api/pagination_links.rb +50 -0
- data/lib/active_model/serializer/adapter/null.rb +2 -8
- data/lib/active_model/serializer/array_serializer.rb +22 -17
- data/lib/active_model/serializer/association.rb +20 -0
- data/lib/active_model/serializer/associations.rb +97 -0
- data/lib/active_model/serializer/belongs_to_reflection.rb +10 -0
- data/lib/active_model/serializer/collection_reflection.rb +7 -0
- data/lib/active_model/serializer/configuration.rb +1 -0
- data/lib/active_model/serializer/fieldset.rb +7 -7
- data/lib/active_model/serializer/has_many_reflection.rb +10 -0
- data/lib/active_model/serializer/has_one_reflection.rb +10 -0
- data/lib/active_model/serializer/lint.rb +129 -0
- data/lib/active_model/serializer/railtie.rb +7 -0
- data/lib/active_model/serializer/reflection.rb +74 -0
- data/lib/active_model/serializer/singular_reflection.rb +7 -0
- data/lib/active_model/serializer/utils.rb +35 -0
- data/lib/active_model/serializer/version.rb +1 -1
- data/lib/active_model_serializers.rb +28 -14
- data/lib/generators/serializer/serializer_generator.rb +7 -7
- data/lib/generators/serializer/templates/{serializer.rb → serializer.rb.erb} +2 -2
- data/lib/tasks/rubocop.rake +0 -0
- data/test/action_controller/adapter_selector_test.rb +3 -3
- data/test/action_controller/explicit_serializer_test.rb +9 -9
- data/test/action_controller/json_api/linked_test.rb +179 -0
- data/test/action_controller/json_api/pagination_test.rb +116 -0
- data/test/action_controller/serialization_scope_name_test.rb +10 -6
- data/test/action_controller/serialization_test.rb +149 -112
- data/test/active_record_test.rb +9 -0
- data/test/adapter/fragment_cache_test.rb +11 -1
- data/test/adapter/json/belongs_to_test.rb +4 -5
- data/test/adapter/json/collection_test.rb +30 -21
- data/test/adapter/json/has_many_test.rb +20 -9
- data/test/adapter/json_api/belongs_to_test.rb +38 -38
- data/test/adapter/json_api/collection_test.rb +22 -23
- data/test/adapter/json_api/has_many_embed_ids_test.rb +2 -2
- data/test/adapter/json_api/has_many_explicit_serializer_test.rb +4 -4
- data/test/adapter/json_api/has_many_test.rb +54 -19
- data/test/adapter/json_api/has_one_test.rb +28 -8
- data/test/adapter/json_api/json_api_test.rb +37 -0
- data/test/adapter/json_api/linked_test.rb +75 -75
- data/test/adapter/json_api/pagination_links_test.rb +115 -0
- data/test/adapter/json_api/resource_type_config_test.rb +59 -0
- data/test/adapter/json_test.rb +18 -5
- data/test/adapter_test.rb +10 -11
- data/test/array_serializer_test.rb +63 -5
- data/test/capture_warnings.rb +65 -0
- data/test/fixtures/active_record.rb +56 -0
- data/test/fixtures/poro.rb +60 -29
- data/test/generators/scaffold_controller_generator_test.rb +1 -2
- data/test/generators/serializer_generator_test.rb +17 -12
- data/test/lint_test.rb +37 -0
- data/test/logger_test.rb +18 -0
- data/test/poro_test.rb +9 -0
- data/test/serializable_resource_test.rb +27 -0
- data/test/serializers/adapter_for_test.rb +123 -3
- data/test/serializers/association_macros_test.rb +36 -0
- data/test/serializers/associations_test.rb +70 -47
- data/test/serializers/attribute_test.rb +28 -4
- data/test/serializers/attributes_test.rb +8 -14
- data/test/serializers/cache_test.rb +58 -31
- data/test/serializers/fieldset_test.rb +3 -4
- data/test/serializers/meta_test.rb +42 -28
- data/test/serializers/root_test.rb +21 -0
- data/test/serializers/serializer_for_test.rb +1 -1
- data/test/support/rails_app.rb +21 -0
- data/test/support/serialization_testing.rb +13 -0
- data/test/support/simplecov.rb +6 -0
- data/test/support/stream_capture.rb +50 -0
- data/test/support/test_case.rb +5 -0
- data/test/test_helper.rb +41 -29
- data/test/utils/include_args_to_hash_test.rb +79 -0
- metadata +123 -17
- data/test/action_controller/json_api_linked_test.rb +0 -179
- data/test/action_controller/rescue_from_test.rb +0 -32
- data/test/serializers/urls_test.rb +0 -26
@@ -1,12 +1,12 @@
|
|
1
|
-
|
2
|
-
class Serializer
|
3
|
-
class Adapter
|
4
|
-
class FlattenJson < Json
|
1
|
+
class ActiveModel::Serializer::Adapter::FlattenJson < ActiveModel::Serializer::Adapter::Json
|
5
2
|
def serializable_hash(options = {})
|
6
|
-
super
|
7
|
-
|
3
|
+
super.each_value.first
|
4
|
+
end
|
5
|
+
|
6
|
+
private
|
7
|
+
|
8
|
+
# no-op: FlattenJson adapter does not include meta data, because it does not support root.
|
9
|
+
def include_meta(json)
|
10
|
+
json
|
8
11
|
end
|
9
|
-
end
|
10
|
-
end
|
11
|
-
end
|
12
12
|
end
|
@@ -1,8 +1,4 @@
|
|
1
|
-
|
2
|
-
class Serializer
|
3
|
-
class Adapter
|
4
|
-
class FragmentCache
|
5
|
-
|
1
|
+
class ActiveModel::Serializer::Adapter::FragmentCache
|
6
2
|
attr_reader :serializer
|
7
3
|
|
8
4
|
def initialize(adapter, serializer, options)
|
@@ -35,7 +31,7 @@ module ActiveModel
|
|
35
31
|
|
36
32
|
def cached_attributes(klass, serializers)
|
37
33
|
attributes = serializer.class._attributes
|
38
|
-
cached_attributes = (klass._cache_only) ? klass._cache_only : attributes.reject {|attr| klass._cache_except.include?(attr) }
|
34
|
+
cached_attributes = (klass._cache_only) ? klass._cache_only : attributes.reject { |attr| klass._cache_except.include?(attr) }
|
39
35
|
non_cached_attributes = attributes - cached_attributes
|
40
36
|
|
41
37
|
cached_attributes.each do |attribute|
|
@@ -54,13 +50,13 @@ module ActiveModel
|
|
54
50
|
end
|
55
51
|
|
56
52
|
def fragment_serializer(name, klass)
|
57
|
-
cached = "#{name
|
58
|
-
non_cached = "#{name
|
53
|
+
cached = "#{to_valid_const_name(name)}CachedSerializer"
|
54
|
+
non_cached = "#{to_valid_const_name(name)}NonCachedSerializer"
|
59
55
|
|
60
56
|
Object.const_set cached, Class.new(ActiveModel::Serializer) unless Object.const_defined?(cached)
|
61
57
|
Object.const_set non_cached, Class.new(ActiveModel::Serializer) unless Object.const_defined?(non_cached)
|
62
58
|
|
63
|
-
klass._cache_options
|
59
|
+
klass._cache_options ||= {}
|
64
60
|
klass._cache_options[:key] = klass._cache_key if klass._cache_key
|
65
61
|
|
66
62
|
cached.constantize.cache(klass._cache_options)
|
@@ -68,11 +64,12 @@ module ActiveModel
|
|
68
64
|
cached.constantize.fragmented(serializer)
|
69
65
|
non_cached.constantize.fragmented(serializer)
|
70
66
|
|
71
|
-
serializers = {cached: cached, non_cached: non_cached}
|
67
|
+
serializers = { cached: cached, non_cached: non_cached }
|
72
68
|
cached_attributes(klass, serializers)
|
73
69
|
serializers
|
74
70
|
end
|
75
|
-
|
76
|
-
|
77
|
-
|
71
|
+
|
72
|
+
def to_valid_const_name(name)
|
73
|
+
name.gsub('::', '_')
|
74
|
+
end
|
78
75
|
end
|
@@ -1,50 +1,47 @@
|
|
1
|
-
|
1
|
+
class ActiveModel::Serializer::Adapter::Json < ActiveModel::Serializer::Adapter
|
2
|
+
extend ActiveSupport::Autoload
|
3
|
+
autoload :FragmentCache
|
2
4
|
|
3
|
-
|
4
|
-
|
5
|
-
class Adapter
|
6
|
-
class Json < Adapter
|
7
|
-
def serializable_hash(options = {})
|
5
|
+
def serializable_hash(options = nil)
|
6
|
+
options ||= {}
|
8
7
|
if serializer.respond_to?(:each)
|
9
|
-
|
8
|
+
result = serializer.map { |s| FlattenJson.new(s).serializable_hash(options) }
|
10
9
|
else
|
11
|
-
|
10
|
+
hash = {}
|
12
11
|
|
13
|
-
|
12
|
+
core = cache_check(serializer) do
|
14
13
|
serializer.attributes(options)
|
15
14
|
end
|
16
15
|
|
17
|
-
serializer.
|
18
|
-
|
19
|
-
|
20
|
-
|
16
|
+
serializer.associations.each do |association|
|
17
|
+
serializer = association.serializer
|
18
|
+
opts = association.options
|
19
|
+
|
20
|
+
if serializer.respond_to?(:each)
|
21
|
+
array_serializer = serializer
|
22
|
+
hash[association.key] = array_serializer.map do |item|
|
21
23
|
cache_check(item) do
|
22
24
|
item.attributes(opts)
|
23
25
|
end
|
24
26
|
end
|
25
27
|
else
|
26
|
-
|
27
|
-
|
28
|
-
|
28
|
+
hash[association.key] =
|
29
|
+
if serializer && serializer.object
|
30
|
+
cache_check(serializer) do
|
31
|
+
serializer.attributes(options)
|
32
|
+
end
|
33
|
+
elsif opts[:virtual_value]
|
34
|
+
opts[:virtual_value]
|
29
35
|
end
|
30
|
-
elsif opts[:virtual_value]
|
31
|
-
@hash[name] = opts[:virtual_value]
|
32
|
-
else
|
33
|
-
@hash[name] = nil
|
34
|
-
end
|
35
36
|
end
|
36
37
|
end
|
37
|
-
|
38
|
+
result = core.merge hash
|
38
39
|
end
|
39
40
|
|
40
|
-
{ root =>
|
41
|
+
{ root => result }
|
41
42
|
end
|
42
43
|
|
43
44
|
def fragment_cache(cached_hash, non_cached_hash)
|
44
|
-
Json::FragmentCache.new().fragment_cache(cached_hash, non_cached_hash)
|
45
|
+
ActiveModel::Serializer::Adapter::Json::FragmentCache.new().fragment_cache(cached_hash, non_cached_hash)
|
45
46
|
end
|
46
|
-
|
47
|
-
end
|
48
|
-
end
|
49
|
-
end
|
50
47
|
end
|
@@ -1,15 +1,5 @@
|
|
1
|
-
|
2
|
-
class Serializer
|
3
|
-
class Adapter
|
4
|
-
class Json < Adapter
|
5
|
-
class FragmentCache
|
6
|
-
|
1
|
+
class ActiveModel::Serializer::Adapter::Json::FragmentCache
|
7
2
|
def fragment_cache(cached_hash, non_cached_hash)
|
8
3
|
non_cached_hash.merge cached_hash
|
9
4
|
end
|
10
|
-
|
11
|
-
end
|
12
|
-
end
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|
5
|
+
end
|
@@ -1,156 +1,158 @@
|
|
1
|
-
|
1
|
+
class ActiveModel::Serializer::Adapter::JsonApi < ActiveModel::Serializer::Adapter
|
2
|
+
extend ActiveSupport::Autoload
|
3
|
+
autoload :PaginationLinks
|
4
|
+
autoload :FragmentCache
|
2
5
|
|
3
|
-
module ActiveModel
|
4
|
-
class Serializer
|
5
|
-
class Adapter
|
6
|
-
class JsonApi < Adapter
|
7
6
|
def initialize(serializer, options = {})
|
8
7
|
super
|
9
|
-
@
|
10
|
-
|
11
|
-
if fields
|
8
|
+
@included = ActiveModel::Serializer::Utils.include_args_to_hash(@options[:include])
|
9
|
+
fields = options.delete(:fields)
|
10
|
+
if fields
|
12
11
|
@fieldset = ActiveModel::Serializer::Fieldset.new(fields, serializer.json_key)
|
13
12
|
else
|
14
13
|
@fieldset = options[:fieldset]
|
15
14
|
end
|
16
15
|
end
|
17
16
|
|
18
|
-
def serializable_hash(options =
|
17
|
+
def serializable_hash(options = nil)
|
18
|
+
options ||= {}
|
19
19
|
if serializer.respond_to?(:each)
|
20
|
-
serializer
|
21
|
-
result = self.class.new(s, @options.merge(fieldset: @fieldset)).serializable_hash
|
22
|
-
@hash[:data] << result[:data]
|
23
|
-
|
24
|
-
if result[:included]
|
25
|
-
@hash[:included] ||= []
|
26
|
-
@hash[:included] |= result[:included]
|
27
|
-
end
|
28
|
-
end
|
20
|
+
serializable_hash_for_collection(serializer, options)
|
29
21
|
else
|
30
|
-
|
31
|
-
add_resource_relationships(@hash[:data], serializer)
|
22
|
+
serializable_hash_for_single_resource(serializer, options)
|
32
23
|
end
|
33
|
-
@hash
|
34
24
|
end
|
35
25
|
|
36
26
|
def fragment_cache(cached_hash, non_cached_hash)
|
37
27
|
root = false if @options.include?(:include)
|
38
|
-
JsonApi::FragmentCache.new
|
28
|
+
ActiveModel::Serializer::Adapter::JsonApi::FragmentCache.new.fragment_cache(root, cached_hash, non_cached_hash)
|
39
29
|
end
|
40
30
|
|
41
31
|
private
|
42
32
|
|
43
|
-
def
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
def add_relationship(resource, name, serializer, val=nil)
|
50
|
-
resource[:relationships] ||= {}
|
51
|
-
resource[:relationships][name] = { data: nil }
|
33
|
+
def serializable_hash_for_collection(serializer, options)
|
34
|
+
hash = { data: [] }
|
35
|
+
serializer.each do |s|
|
36
|
+
result = self.class.new(s, @options.merge(fieldset: @fieldset)).serializable_hash(options)
|
37
|
+
hash[:data] << result[:data]
|
52
38
|
|
53
|
-
|
54
|
-
|
39
|
+
if result[:included]
|
40
|
+
hash[:included] ||= []
|
41
|
+
hash[:included] |= result[:included]
|
42
|
+
end
|
55
43
|
end
|
56
|
-
end
|
57
44
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
serializers = Array(serializers)
|
45
|
+
if serializer.paginated?
|
46
|
+
hash[:links] ||= {}
|
47
|
+
hash[:links].update(links_for(serializer, options))
|
62
48
|
end
|
63
|
-
resource_path = [parent, resource_name].compact.join('.')
|
64
|
-
if include_assoc?(resource_path)
|
65
|
-
@hash[:included] ||= []
|
66
49
|
|
67
|
-
|
68
|
-
|
50
|
+
hash
|
51
|
+
end
|
69
52
|
|
70
|
-
|
53
|
+
def serializable_hash_for_single_resource(serializer, options)
|
54
|
+
primary_data = primary_data_for(serializer, options)
|
55
|
+
relationships = relationships_for(serializer)
|
56
|
+
included = included_for(serializer)
|
57
|
+
hash = { data: primary_data }
|
58
|
+
hash[:data][:relationships] = relationships if relationships.any?
|
59
|
+
hash[:included] = included if included.any?
|
71
60
|
|
72
|
-
|
73
|
-
|
74
|
-
end
|
61
|
+
hash
|
62
|
+
end
|
75
63
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
64
|
+
def resource_identifier_type_for(serializer)
|
65
|
+
if ActiveModel::Serializer.config.jsonapi_resource_type == :singular
|
66
|
+
serializer.object.class.model_name.singular
|
67
|
+
else
|
68
|
+
serializer.object.class.model_name.plural
|
80
69
|
end
|
81
70
|
end
|
82
71
|
|
83
|
-
def
|
84
|
-
if serializer.respond_to?(:
|
85
|
-
|
86
|
-
serializer.each do |object|
|
87
|
-
result << resource_object_for(object, options)
|
88
|
-
end
|
72
|
+
def resource_identifier_id_for(serializer)
|
73
|
+
if serializer.respond_to?(:id)
|
74
|
+
serializer.id
|
89
75
|
else
|
90
|
-
|
76
|
+
serializer.object.id
|
91
77
|
end
|
92
|
-
result
|
93
78
|
end
|
94
79
|
|
95
|
-
def
|
96
|
-
|
97
|
-
|
80
|
+
def resource_identifier_for(serializer)
|
81
|
+
type = resource_identifier_type_for(serializer)
|
82
|
+
id = resource_identifier_id_for(serializer)
|
98
83
|
|
99
|
-
|
100
|
-
|
84
|
+
{ id: id.to_s, type: type }
|
85
|
+
end
|
101
86
|
|
102
|
-
|
103
|
-
|
104
|
-
type: attributes.delete(:type)
|
105
|
-
}
|
87
|
+
def resource_object_for(serializer, options = {})
|
88
|
+
options[:fields] = @fieldset && @fieldset.fields_for(serializer)
|
106
89
|
|
90
|
+
cache_check(serializer) do
|
91
|
+
result = resource_identifier_for(serializer)
|
92
|
+
attributes = serializer.attributes(options).except(:id)
|
107
93
|
result[:attributes] = attributes if attributes.any?
|
108
94
|
result
|
109
95
|
end
|
110
96
|
end
|
111
97
|
|
112
|
-
def
|
113
|
-
|
114
|
-
|
98
|
+
def primary_data_for(serializer, options)
|
99
|
+
if serializer.respond_to?(:each)
|
100
|
+
serializer.map { |s| resource_object_for(s, options) }
|
101
|
+
else
|
102
|
+
resource_object_for(serializer, options)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def relationship_value_for(serializer, options = {})
|
107
|
+
if serializer.respond_to?(:each)
|
108
|
+
serializer.map { |s| resource_identifier_for(s) }
|
109
|
+
else
|
110
|
+
if options[:virtual_value]
|
111
|
+
options[:virtual_value]
|
112
|
+
elsif serializer && serializer.object
|
113
|
+
resource_identifier_for(serializer)
|
114
|
+
end
|
115
|
+
end
|
115
116
|
end
|
116
117
|
|
117
|
-
def
|
118
|
-
|
119
|
-
check_assoc("#{assoc}.")
|
118
|
+
def relationships_for(serializer)
|
119
|
+
Hash[serializer.associations.map { |association| [association.key, { data: relationship_value_for(association.serializer, association.options) }] }]
|
120
120
|
end
|
121
121
|
|
122
|
-
def
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
s.match(/^#{assoc.gsub('.', '\.')}/)
|
122
|
+
def included_for(serializer)
|
123
|
+
included = @included.flat_map do |inc|
|
124
|
+
association = serializer.associations.find { |assoc| assoc.key == inc.first }
|
125
|
+
_included_for(association.serializer, inc.second) if association
|
127
126
|
end
|
127
|
+
|
128
|
+
included.uniq
|
128
129
|
end
|
129
130
|
|
130
|
-
def
|
131
|
-
|
131
|
+
def _included_for(serializer, includes)
|
132
|
+
if serializer.respond_to?(:each)
|
133
|
+
serializer.flat_map { |s| _included_for(s, includes) }.uniq
|
134
|
+
else
|
135
|
+
return [] unless serializer && serializer.object
|
132
136
|
|
133
|
-
|
134
|
-
|
137
|
+
primary_data = primary_data_for(serializer, @options)
|
138
|
+
relationships = relationships_for(serializer)
|
139
|
+
primary_data[:relationships] = relationships if relationships.any?
|
135
140
|
|
136
|
-
|
137
|
-
add_relationships(attrs, name, association)
|
138
|
-
else
|
139
|
-
if opts[:virtual_value]
|
140
|
-
add_relationship(attrs, name, nil, opts[:virtual_value])
|
141
|
-
else
|
142
|
-
add_relationship(attrs, name, association)
|
143
|
-
end
|
144
|
-
end
|
141
|
+
included = [primary_data]
|
145
142
|
|
146
|
-
|
147
|
-
|
148
|
-
|
143
|
+
includes.each do |inc|
|
144
|
+
association = serializer.associations.find { |assoc| assoc.key == inc.first }
|
145
|
+
if association
|
146
|
+
included.concat(_included_for(association.serializer, inc.second))
|
147
|
+
included.uniq!
|
149
148
|
end
|
150
149
|
end
|
150
|
+
|
151
|
+
included
|
151
152
|
end
|
152
153
|
end
|
153
|
-
|
154
|
-
|
155
|
-
|
154
|
+
|
155
|
+
def links_for(serializer, options)
|
156
|
+
JsonApi::PaginationLinks.new(serializer.object, options[:context]).serializable_hash(options)
|
157
|
+
end
|
156
158
|
end
|
@@ -1,23 +1,13 @@
|
|
1
|
-
|
2
|
-
class Serializer
|
3
|
-
class Adapter
|
4
|
-
class JsonApi < Adapter
|
5
|
-
class FragmentCache
|
6
|
-
|
1
|
+
class ActiveModel::Serializer::Adapter::JsonApi::FragmentCache
|
7
2
|
def fragment_cache(root, cached_hash, non_cached_hash)
|
8
3
|
hash = {}
|
9
4
|
core_cached = cached_hash.first
|
10
5
|
core_non_cached = non_cached_hash.first
|
11
|
-
no_root_cache = cached_hash.delete_if {|key, value| key == core_cached[0] }
|
12
|
-
no_root_non_cache = non_cached_hash.delete_if {|key, value| key == core_non_cached[0] }
|
6
|
+
no_root_cache = cached_hash.delete_if { |key, value| key == core_cached[0] }
|
7
|
+
no_root_non_cache = non_cached_hash.delete_if { |key, value| key == core_non_cached[0] }
|
13
8
|
cached_resource = (core_cached[1]) ? core_cached[1].deep_merge(core_non_cached[1]) : core_non_cached[1]
|
14
9
|
hash = (root) ? { root => cached_resource } : cached_resource
|
15
10
|
|
16
11
|
hash.deep_merge no_root_non_cache.deep_merge no_root_cache
|
17
12
|
end
|
18
|
-
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
13
|
+
end
|