fast_serializer 1.0.0
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 +7 -0
- data/.gitignore +17 -0
- data/HISTORY.txt +3 -0
- data/MIT_LICENSE +20 -0
- data/README.md +148 -0
- data/Rakefile +18 -0
- data/VERSION +1 -0
- data/fast_serializer.gemspec +23 -0
- data/lib/fast_serializer.rb +37 -0
- data/lib/fast_serializer/array_serializer.rb +92 -0
- data/lib/fast_serializer/cache.rb +22 -0
- data/lib/fast_serializer/cache/active_support_cache.rb +21 -0
- data/lib/fast_serializer/serialization_context.rb +70 -0
- data/lib/fast_serializer/serialized_field.rb +78 -0
- data/lib/fast_serializer/serializer.rb +356 -0
- data/spec/array_serializer_spec.rb +59 -0
- data/spec/fast_serializer_spec.rb +28 -0
- data/spec/serialization_context_spec.rb +32 -0
- data/spec/serialized_field_spec.rb +71 -0
- data/spec/serializer_spec.rb +146 -0
- data/spec/spec_helper.rb +16 -0
- data/spec/support/test_models.rb +64 -0
- metadata +117 -0
@@ -0,0 +1,78 @@
|
|
1
|
+
module FastSerializer
|
2
|
+
# Data structure used internally for maintaining a field to be serialized.
|
3
|
+
class SerializedField
|
4
|
+
attr_reader :name
|
5
|
+
|
6
|
+
def initialize(name, optional: false, serializer: nil, serializer_options: nil, enumerable: false)
|
7
|
+
@name = name
|
8
|
+
@optional = !!optional
|
9
|
+
if serializer
|
10
|
+
@serializer = serializer
|
11
|
+
@serializer_options = serializer_options
|
12
|
+
@enumerable = enumerable
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def optional?
|
17
|
+
@optional
|
18
|
+
end
|
19
|
+
|
20
|
+
# Wrap a value in the serializer if one has been set. Otherwise just returns the raw value.
|
21
|
+
def serialize(value)
|
22
|
+
if value && @serializer
|
23
|
+
serializer = nil
|
24
|
+
if @enumerable
|
25
|
+
serializer = ArraySerializer.new(value, :serializer => @serializer, :serializer_options => @serializer_options)
|
26
|
+
else
|
27
|
+
serializer = @serializer.new(value, @serializer_options)
|
28
|
+
end
|
29
|
+
context = SerializationContext.current
|
30
|
+
if context
|
31
|
+
context.with_reference(value){ serializer.as_json }
|
32
|
+
else
|
33
|
+
serializer.as_json
|
34
|
+
end
|
35
|
+
else
|
36
|
+
serialize_value(value)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
# Convert the value to primitive data types: string, number, boolean, symbol, time, date, array, hash.
|
43
|
+
def serialize_value(value)
|
44
|
+
if value.is_a?(String) || value.is_a?(Numeric) || value == nil || value == true || value == false || value.is_a?(Time) || value.is_a?(Date) || value.is_a?(Symbol)
|
45
|
+
value
|
46
|
+
elsif value.is_a?(Hash)
|
47
|
+
hash = nil
|
48
|
+
value.each do |k, v|
|
49
|
+
val = serialize_value(v)
|
50
|
+
if val.object_id != v.object_id
|
51
|
+
hash = value.dup unless hash
|
52
|
+
hash[k] = val
|
53
|
+
end
|
54
|
+
end
|
55
|
+
hash || value
|
56
|
+
elsif value.is_a?(Enumerable)
|
57
|
+
array = nil
|
58
|
+
value.each_with_index do |v, i|
|
59
|
+
val = serialize_value(v)
|
60
|
+
if val.object_id != v.object_id
|
61
|
+
array = value.dup unless array
|
62
|
+
array[i] = val
|
63
|
+
end
|
64
|
+
end
|
65
|
+
array || value
|
66
|
+
elsif value.respond_to?(:as_json)
|
67
|
+
value.as_json
|
68
|
+
elsif value.respond_to?(:to_hash)
|
69
|
+
value.to_hash
|
70
|
+
elsif value.respond_to?(:to_h)
|
71
|
+
value.to_h
|
72
|
+
else
|
73
|
+
value
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,356 @@
|
|
1
|
+
module FastSerializer
|
2
|
+
# Models can include this module to define themselves as serializers. A serializer is used to wrap
|
3
|
+
# an object and output a hash version of that object suitable for serialization to JSON or other formats.
|
4
|
+
#
|
5
|
+
# To define what fields to serialize on the wrapped object, the serializer class must call the +serialize+
|
6
|
+
# class method:
|
7
|
+
#
|
8
|
+
# class PersonSerializer
|
9
|
+
# include FastSerializer::Serializer
|
10
|
+
# serialize :id, :name
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# This sample serializer will output an object as a hash with keys {:id, :name}. The values for each field
|
14
|
+
# is gotten by calling the corresponding method on the serializer object. By default, each serialized field
|
15
|
+
# will automatically define a method that simply delegates to the wrapped object. So if you need provide special
|
16
|
+
# handling for a field or serialize a virtual field that doesn't exist on the parent object, you just need to
|
17
|
+
# implement the method on the serializer.
|
18
|
+
#
|
19
|
+
# class PersonSerializer
|
20
|
+
# include FastSerializer::Serializer
|
21
|
+
# serialize :id, :name
|
22
|
+
#
|
23
|
+
# def name
|
24
|
+
# "#{object.first_name} #{object.last_name}"
|
25
|
+
# end
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# Serializers can implement their own options for controlling details about how to serialize the object.
|
29
|
+
#
|
30
|
+
# class PersonSerializer
|
31
|
+
# include FastSerializer::Serializer
|
32
|
+
# serialize :id, :name
|
33
|
+
#
|
34
|
+
# def name
|
35
|
+
# if option(:last_first)
|
36
|
+
# "#{object.last_name}, #{object.first_name}"
|
37
|
+
# else
|
38
|
+
# "#{object.first_name} #{object.last_name}"
|
39
|
+
# end
|
40
|
+
# end
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# serializer = PersonSerializer.new(person, :last_first => true)
|
44
|
+
#
|
45
|
+
# All serializers will honor options for :include (include optional fields), :exclude (exclude fields).
|
46
|
+
#
|
47
|
+
# Serializer can also be specified as cacheable. Cacheable serializer will store and fetch the serialized value
|
48
|
+
# from a cache. In order to user caching you must set the cache implementation. This can either be done on a
|
49
|
+
# global level (FastSerializer.cache) class level, or instance level (:cache option). Then you can specify
|
50
|
+
# serializers to be cacheable. This can be done on a class level with the +cacheable+ directive or on an
|
51
|
+
# instance level with the :cacheable option. A time to live can also be set at the same levels using +cache_ttl+.
|
52
|
+
#
|
53
|
+
# Serializers are designed to be reusable and must never have any internal state associated with them. Calling
|
54
|
+
# +as_json+ on a serializer multiple times must always return the same value.
|
55
|
+
#
|
56
|
+
# Serializing a nil object will result in nil rather than an empty hash.
|
57
|
+
module Serializer
|
58
|
+
|
59
|
+
def self.included(base)
|
60
|
+
base.extend(ClassMethods)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Return the wrapped object that is being serialized.
|
64
|
+
attr_reader :object
|
65
|
+
|
66
|
+
# Return the options hash (if any) that specified optional details about how to serialize the object.
|
67
|
+
attr_reader :options
|
68
|
+
|
69
|
+
module ClassMethods
|
70
|
+
# Define one or more fields to include in the serialized object. Field values will be gotten
|
71
|
+
# by calling the method of the same name on class including this module.
|
72
|
+
#
|
73
|
+
# Several options can be specified to control how the field is serialized.
|
74
|
+
#
|
75
|
+
# * as: Name to call the field in the serialized hash. Defaults to the same as the field name
|
76
|
+
# (withany ? stripped off the end for boolean fields). This option can only be specified
|
77
|
+
# for a single field.
|
78
|
+
#
|
79
|
+
# * optional: Boolean flag indicating if the field is optional in the serialized value (defaults to false).
|
80
|
+
# Optional fields are only included if the :include option to the +as_json+ method includes the field name.
|
81
|
+
#
|
82
|
+
# * delegate: Boolean flag indicating if the field call should be delegated to the wrapped object (defaults to true).
|
83
|
+
# When this is supplied, a method will be automatically defined on the serializer with the name of the field
|
84
|
+
# that simply then calls the same method on the wrapped object.
|
85
|
+
#
|
86
|
+
# * serializer: Class that should be used to serialize the field. If this option is specified, the field value will
|
87
|
+
# be serialized using the specified serializer class which should include this module. Otherwise, the +as_json+
|
88
|
+
# method will be called on the field class.
|
89
|
+
#
|
90
|
+
# * serializer_options: Options that should be used for serializing the field for when the :serializer option
|
91
|
+
# has been specified.
|
92
|
+
#
|
93
|
+
# * enumerable: Boolean flag indicating if the field is enumerable (defaults to false). This option is only
|
94
|
+
# used if the :serializer option has been set. If the field is marked as enumerable, then the value will be
|
95
|
+
# serialized as an array with each element wrapped in the specified serializer.
|
96
|
+
#
|
97
|
+
# Subclasses will inherit all of their parent classes serialized fields. Subclasses can override fields
|
98
|
+
# defined on the parent class by simply defining them again.
|
99
|
+
def serialize(*fields, as: nil, optional: false, delegate: true, serializer: nil, serializer_options: nil, enumerable: false)
|
100
|
+
if as && fields.size > 1
|
101
|
+
raise ArgumentError.new("Cannot specify :as argument with multiple fields to serialize")
|
102
|
+
end
|
103
|
+
|
104
|
+
fields.each do |field|
|
105
|
+
name = as
|
106
|
+
if name.nil? && field.to_s.end_with?("?".freeze)
|
107
|
+
name = field.to_s.chomp("?".freeze)
|
108
|
+
end
|
109
|
+
|
110
|
+
field = field.to_sym
|
111
|
+
attribute = (name || field).to_sym
|
112
|
+
add_field(attribute, optional: optional, serializer: serializer, serializer_options: serializer_options, enumerable: enumerable)
|
113
|
+
|
114
|
+
if delegate && !method_defined?(attribute)
|
115
|
+
define_delegate(attribute, field)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Remove a field from being serialized. This can be useful in subclasses if they need to remove a
|
121
|
+
# field defined by the parent class.
|
122
|
+
def remove(*fields)
|
123
|
+
remove_fields = fields.collect(&:to_sym)
|
124
|
+
field_list = []
|
125
|
+
serializable_fields.each do |existing_field|
|
126
|
+
field_list << existing_field unless remove_fields.include?(existing_field.name)
|
127
|
+
end
|
128
|
+
@serializable_fields = field_list.freeze
|
129
|
+
end
|
130
|
+
|
131
|
+
# Specify the cacheability of the serializer.
|
132
|
+
#
|
133
|
+
# You can specify the cacheable state (defaults to true) of the class. Subclasses will inherit the
|
134
|
+
# cacheable state of their parent class, so if you have non-cacheable serializer subclassing a
|
135
|
+
# cacheable parent class, you can call <tt>cacheable false</tt> to override the parent behavior.
|
136
|
+
#
|
137
|
+
# You can also specify the cache time to live (ttl) in seconds and the cache implementation to use.
|
138
|
+
# Both of these values are inherited on subclasses.
|
139
|
+
def cacheable(cacheable = true, ttl: nil, cache: nil)
|
140
|
+
@cacheable = cacheable
|
141
|
+
self.cache_ttl = ttl if ttl
|
142
|
+
self.cache = cache if cache
|
143
|
+
end
|
144
|
+
|
145
|
+
# Return true if the serializer class is cacheable.
|
146
|
+
def cacheable?
|
147
|
+
unless defined?(@cacheable)
|
148
|
+
@cacheable = superclass.cacheable? if superclass.respond_to?(:cacheable?)
|
149
|
+
end
|
150
|
+
!!@cacheable
|
151
|
+
end
|
152
|
+
|
153
|
+
# Return the time to live in seconds for a cacheable serializer.
|
154
|
+
def cache_ttl
|
155
|
+
if defined?(@cache_ttl)
|
156
|
+
@cache_ttl
|
157
|
+
elsif superclass.respond_to?(:cache_ttl)
|
158
|
+
superclass.cache_ttl
|
159
|
+
else
|
160
|
+
nil
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# Set the time to live on a cacheable serializer.
|
165
|
+
def cache_ttl=(value)
|
166
|
+
@cache_ttl = value
|
167
|
+
end
|
168
|
+
|
169
|
+
# Get the cache implemtation used to store cacheable serializers.
|
170
|
+
def cache
|
171
|
+
if defined?(@cache)
|
172
|
+
@cache
|
173
|
+
elsif superclass.respond_to?(:cache)
|
174
|
+
superclass.cache
|
175
|
+
else
|
176
|
+
FastSerializer.cache
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
# Set the cache implementation used to store cacheable serializers.
|
181
|
+
def cache=(cache)
|
182
|
+
@cache = cache
|
183
|
+
end
|
184
|
+
|
185
|
+
# :nodoc:
|
186
|
+
def new(object, options = nil)
|
187
|
+
context = SerializationContext.current
|
188
|
+
if context
|
189
|
+
# If there's a context in scope this will load duplicate entries from the context rather than creating new instances.
|
190
|
+
context.load(self, object, options)
|
191
|
+
else
|
192
|
+
super
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
# Return a list of the SerializedFields defined for the class.
|
197
|
+
def serializable_fields
|
198
|
+
unless defined?(@serializable_fields) && @serializable_fields
|
199
|
+
fields = superclass.send(:serializable_fields).dup if superclass.respond_to?(:serializable_fields)
|
200
|
+
fields ||= []
|
201
|
+
@serializable_fields = fields.freeze
|
202
|
+
end
|
203
|
+
@serializable_fields
|
204
|
+
end
|
205
|
+
|
206
|
+
private
|
207
|
+
|
208
|
+
# Add a field to be serialized.
|
209
|
+
def add_field(name, optional:, serializer:, serializer_options:, enumerable:)
|
210
|
+
name = name.to_sym
|
211
|
+
field = SerializedField.new(name, optional: optional, serializer: serializer, serializer_options: serializer_options, enumerable: enumerable)
|
212
|
+
|
213
|
+
# Add the field to the frozen list of fields.
|
214
|
+
field_list = []
|
215
|
+
added = false
|
216
|
+
serializable_fields.each do |existing_field|
|
217
|
+
if existing_field.name == name
|
218
|
+
field_list << field
|
219
|
+
else
|
220
|
+
field_list << existing_field
|
221
|
+
end
|
222
|
+
end
|
223
|
+
field_list << field unless added
|
224
|
+
@serializable_fields = field_list.freeze
|
225
|
+
end
|
226
|
+
|
227
|
+
# Define a delegate method name +attribute+ that invokes the +field+ method on the wrapped object.
|
228
|
+
def define_delegate(attribute, field)
|
229
|
+
define_method(attribute){ object.send(field) }
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
# Create a new serializer for the specified object.
|
234
|
+
#
|
235
|
+
# Options can be passed in to control how the object is serialized. Options supported by all Serializers:
|
236
|
+
#
|
237
|
+
# * :include - Field or array of optional field names that should be included in the serialized object.
|
238
|
+
# * :exclude - Field or array of field names that should be excluded from the serialized object.
|
239
|
+
# * :cacheable - Override the cacheable behavior set on the class.
|
240
|
+
# * :cache_ttl - Override the cache ttl set on the class.
|
241
|
+
# * :cache - Override the cache implementation set on the class.
|
242
|
+
def initialize(object, options = nil)
|
243
|
+
@object = object
|
244
|
+
@options = options
|
245
|
+
@_serialized = nil
|
246
|
+
end
|
247
|
+
|
248
|
+
# Serialize the wrapped object into a format suitable for passing to a JSON parser.
|
249
|
+
def as_json(*args)
|
250
|
+
return nil unless object
|
251
|
+
unless @_serialized
|
252
|
+
@_serialized = (cacheable? ? load_from_cache : load_hash).freeze
|
253
|
+
end
|
254
|
+
@_serialized
|
255
|
+
end
|
256
|
+
|
257
|
+
alias :to_hash :as_json
|
258
|
+
alias :to_h :as_json
|
259
|
+
|
260
|
+
# Convert the wrapped object to JSON format.
|
261
|
+
def to_json(options = nil)
|
262
|
+
if defined?(MultiJson)
|
263
|
+
MultiJson.dump(as_json)
|
264
|
+
else
|
265
|
+
JSON.dump(as_json)
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
# Fetch the specified option from the options hash.
|
270
|
+
def option(name)
|
271
|
+
@options[name] if @options
|
272
|
+
end
|
273
|
+
|
274
|
+
# Return true if this serializer is cacheable.
|
275
|
+
def cacheable?
|
276
|
+
option(:cacheable) || self.class.cacheable?
|
277
|
+
end
|
278
|
+
|
279
|
+
# Return the cache implementation where this serializer can be stored.
|
280
|
+
def cache
|
281
|
+
option(:cache) || self.class.cache
|
282
|
+
end
|
283
|
+
|
284
|
+
# Return the time to live in seconds this serializer can be cached for.
|
285
|
+
def cache_ttl
|
286
|
+
option(:cache_ttl) || self.class.cache_ttl
|
287
|
+
end
|
288
|
+
|
289
|
+
# Returns a array of the elements that make this serializer unique. The
|
290
|
+
# key is an array made up of the serializer class name, wrapped object, and
|
291
|
+
# serialization options hash.
|
292
|
+
def cache_key
|
293
|
+
[self.class.name, object, options]
|
294
|
+
end
|
295
|
+
|
296
|
+
# :nodoc:
|
297
|
+
def ==(other)
|
298
|
+
other.instance_of?(self.class) && @object == other.object && @options == other.options
|
299
|
+
end
|
300
|
+
alias_method :eql?, :==
|
301
|
+
|
302
|
+
protected
|
303
|
+
|
304
|
+
# Load the hash that will represent the wrapped object as a serialized object.
|
305
|
+
def load_hash
|
306
|
+
hash = {}
|
307
|
+
include_fields = included_optional_fields
|
308
|
+
excluded_fields = excluded_regular_fields
|
309
|
+
SerializationContext.use do
|
310
|
+
self.class.serializable_fields.each do |field|
|
311
|
+
name = field.name
|
312
|
+
if field.optional?
|
313
|
+
next unless include_fields && include_fields.include?(name)
|
314
|
+
end
|
315
|
+
next if excluded_fields && excluded_fields.include?(name)
|
316
|
+
value = field.serialize(send(name))
|
317
|
+
hash[name] = value
|
318
|
+
end
|
319
|
+
end
|
320
|
+
hash
|
321
|
+
end
|
322
|
+
|
323
|
+
# Load the hash that will represent the wrapped object as a serialized object from a cache.
|
324
|
+
def load_from_cache
|
325
|
+
if cache
|
326
|
+
cache.fetch(self, cache_ttl) do
|
327
|
+
load_hash
|
328
|
+
end
|
329
|
+
else
|
330
|
+
load_hash
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
private
|
335
|
+
|
336
|
+
# Return a list of optional fields to be included in the output from the :include option.
|
337
|
+
def included_optional_fields
|
338
|
+
included_fields = option(:include)
|
339
|
+
if included_fields
|
340
|
+
Array(included_fields).collect(&:to_sym)
|
341
|
+
else
|
342
|
+
nil
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
# Return a list of fields to be excluded from the output from the :exclude option.
|
347
|
+
def excluded_regular_fields
|
348
|
+
excluded_fields = option(:exclude)
|
349
|
+
if excluded_fields
|
350
|
+
Array(excluded_fields).collect(&:to_sym)
|
351
|
+
else
|
352
|
+
nil
|
353
|
+
end
|
354
|
+
end
|
355
|
+
end
|
356
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe FastSerializer::ArraySerializer do
|
4
|
+
|
5
|
+
it "should serialize an array of regular objects" do
|
6
|
+
array = [1, 2, 3]
|
7
|
+
serializer = FastSerializer::ArraySerializer.new(array)
|
8
|
+
expect(serializer.as_json).to eq array
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should serialize any Enumerable" do
|
12
|
+
hash = {:a => 1, :b => 2}
|
13
|
+
serializer = FastSerializer::ArraySerializer.new(hash)
|
14
|
+
expect(serializer.as_json).to eq hash.to_a
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should serializer an array of objects using a specific serializer" do
|
18
|
+
model_1 = SimpleModel.new(:id => 1, :name => "foo")
|
19
|
+
model_2 = SimpleModel.new(:id => 2, :name => "bar")
|
20
|
+
serializer = FastSerializer::ArraySerializer.new([model_1, model_2], :serializer => SimpleSerializer)
|
21
|
+
expect(JSON.load(serializer.to_json)).to eq [
|
22
|
+
{"id" => 1, "name" => "foo", "validated" => false},
|
23
|
+
{"id" => 2, "name" => "bar", "validated" => false}
|
24
|
+
]
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should serializer an array of objects using a specific serializer with options" do
|
28
|
+
model_1 = SimpleModel.new(:id => 1, :name => "foo")
|
29
|
+
model_2 = SimpleModel.new(:id => 2, :name => "bar")
|
30
|
+
serializer = FastSerializer::ArraySerializer.new([model_1, model_2], :serializer => SimpleSerializer, :serializer_options => {:include => :description})
|
31
|
+
expect(JSON.load(serializer.to_json)).to eq [
|
32
|
+
{"id" => 1, "name" => "foo", "validated" => false, "description" => nil},
|
33
|
+
{"id" => 2, "name" => "bar", "validated" => false, "description" => nil}
|
34
|
+
]
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should not respond to_hash methods" do
|
38
|
+
array = [1, 2, 3]
|
39
|
+
serializer = FastSerializer::ArraySerializer.new(array)
|
40
|
+
expect(serializer.respond_to?(:to_hash)).to eq false
|
41
|
+
expect(serializer.respond_to?(:to_h)).to eq false
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should respond to to_a" do
|
45
|
+
array = [1, 2, 3]
|
46
|
+
serializer = FastSerializer::ArraySerializer.new(array)
|
47
|
+
expect(serializer.to_a).to eq array
|
48
|
+
end
|
49
|
+
|
50
|
+
it "should pull cacheable serializers from a cache" do
|
51
|
+
model_1 = SimpleModel.new(:id => 1, :name => "foo")
|
52
|
+
model_2 = SimpleModel.new(:id => 2, :name => "bar")
|
53
|
+
serializer = FastSerializer::ArraySerializer.new([model_1, model_2], serializer: CachedSerializer)
|
54
|
+
expect(serializer.cacheable?).to eq true
|
55
|
+
already_cached_json = CachedSerializer.new(model_1).as_json
|
56
|
+
expect(serializer.as_json.collect(&:object_id)).to eq [already_cached_json.object_id, CachedSerializer.new(model_2).as_json.object_id]
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|