acfs 1.3.3 → 1.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +372 -0
- data/LICENSE +22 -0
- data/README.md +321 -0
- data/acfs.gemspec +38 -0
- data/lib/acfs.rb +51 -0
- data/lib/acfs/adapter/base.rb +26 -0
- data/lib/acfs/adapter/typhoeus.rb +82 -0
- data/lib/acfs/collection.rb +28 -0
- data/lib/acfs/collections/paginatable.rb +76 -0
- data/lib/acfs/configuration.rb +120 -0
- data/lib/acfs/errors.rb +147 -0
- data/lib/acfs/global.rb +101 -0
- data/lib/acfs/location.rb +76 -0
- data/lib/acfs/middleware/base.rb +24 -0
- data/lib/acfs/middleware/json.rb +31 -0
- data/lib/acfs/middleware/logger.rb +23 -0
- data/lib/acfs/middleware/msgpack.rb +32 -0
- data/lib/acfs/middleware/print.rb +23 -0
- data/lib/acfs/middleware/serializer.rb +41 -0
- data/lib/acfs/operation.rb +96 -0
- data/lib/acfs/request.rb +32 -0
- data/lib/acfs/request/callbacks.rb +54 -0
- data/lib/acfs/resource.rb +39 -0
- data/lib/acfs/resource/attributes.rb +270 -0
- data/lib/acfs/resource/attributes/base.rb +29 -0
- data/lib/acfs/resource/attributes/boolean.rb +39 -0
- data/lib/acfs/resource/attributes/date_time.rb +32 -0
- data/lib/acfs/resource/attributes/dict.rb +39 -0
- data/lib/acfs/resource/attributes/float.rb +33 -0
- data/lib/acfs/resource/attributes/integer.rb +29 -0
- data/lib/acfs/resource/attributes/list.rb +36 -0
- data/lib/acfs/resource/attributes/string.rb +26 -0
- data/lib/acfs/resource/attributes/uuid.rb +48 -0
- data/lib/acfs/resource/dirty.rb +37 -0
- data/lib/acfs/resource/initialization.rb +31 -0
- data/lib/acfs/resource/loadable.rb +35 -0
- data/lib/acfs/resource/locatable.rb +135 -0
- data/lib/acfs/resource/operational.rb +26 -0
- data/lib/acfs/resource/persistence.rb +258 -0
- data/lib/acfs/resource/query_methods.rb +266 -0
- data/lib/acfs/resource/service.rb +44 -0
- data/lib/acfs/resource/validation.rb +49 -0
- data/lib/acfs/response.rb +30 -0
- data/lib/acfs/response/formats.rb +27 -0
- data/lib/acfs/response/status.rb +33 -0
- data/lib/acfs/rspec.rb +13 -0
- data/lib/acfs/runner.rb +102 -0
- data/lib/acfs/service.rb +94 -0
- data/lib/acfs/service/middleware.rb +58 -0
- data/lib/acfs/service/middleware/stack.rb +65 -0
- data/lib/acfs/singleton_resource.rb +85 -0
- data/lib/acfs/stub.rb +199 -0
- data/lib/acfs/util.rb +22 -0
- data/lib/acfs/version.rb +16 -0
- data/lib/acfs/yard.rb +6 -0
- data/spec/acfs/adapter/typhoeus_spec.rb +55 -0
- data/spec/acfs/collection_spec.rb +157 -0
- data/spec/acfs/configuration_spec.rb +53 -0
- data/spec/acfs/global_spec.rb +140 -0
- data/spec/acfs/location_spec.rb +25 -0
- data/spec/acfs/middleware/json_spec.rb +79 -0
- data/spec/acfs/middleware/msgpack_spec.rb +62 -0
- data/spec/acfs/operation_spec.rb +12 -0
- data/spec/acfs/request/callbacks_spec.rb +48 -0
- data/spec/acfs/request_spec.rb +79 -0
- data/spec/acfs/resource/attributes/boolean_spec.rb +58 -0
- data/spec/acfs/resource/attributes/date_time_spec.rb +51 -0
- data/spec/acfs/resource/attributes/dict_spec.rb +77 -0
- data/spec/acfs/resource/attributes/float_spec.rb +61 -0
- data/spec/acfs/resource/attributes/integer_spec.rb +36 -0
- data/spec/acfs/resource/attributes/list_spec.rb +60 -0
- data/spec/acfs/resource/attributes/uuid_spec.rb +42 -0
- data/spec/acfs/resource/attributes_spec.rb +179 -0
- data/spec/acfs/resource/dirty_spec.rb +49 -0
- data/spec/acfs/resource/initialization_spec.rb +36 -0
- data/spec/acfs/resource/loadable_spec.rb +22 -0
- data/spec/acfs/resource/locatable_spec.rb +118 -0
- data/spec/acfs/resource/persistance_spec.rb +322 -0
- data/spec/acfs/resource/query_methods_spec.rb +548 -0
- data/spec/acfs/resource/validation_spec.rb +129 -0
- data/spec/acfs/response/formats_spec.rb +52 -0
- data/spec/acfs/response/status_spec.rb +71 -0
- data/spec/acfs/runner_spec.rb +95 -0
- data/spec/acfs/service/middleware_spec.rb +35 -0
- data/spec/acfs/service_spec.rb +48 -0
- data/spec/acfs/singleton_resource_spec.rb +17 -0
- data/spec/acfs/stub_spec.rb +345 -0
- data/spec/acfs_spec.rb +205 -0
- data/spec/fixtures/config.yml +14 -0
- data/spec/spec_helper.rb +42 -0
- data/spec/support/hash.rb +11 -0
- data/spec/support/response.rb +12 -0
- data/spec/support/service.rb +92 -0
- data/spec/support/shared/find_callbacks.rb +50 -0
- metadata +159 -26
data/lib/acfs/request.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'acfs/request/callbacks'
|
4
|
+
|
5
|
+
module Acfs
|
6
|
+
# Encapsulate all data required to make up a request to the
|
7
|
+
# underlaying http library.
|
8
|
+
#
|
9
|
+
class Request
|
10
|
+
attr_accessor :body, :format
|
11
|
+
attr_reader :url, :headers, :params, :data, :method, :operation
|
12
|
+
|
13
|
+
include Request::Callbacks
|
14
|
+
def initialize(url, **options, &block)
|
15
|
+
@url = URI.parse(url.to_s).tap do |_url|
|
16
|
+
@data = options.delete(:data) || nil
|
17
|
+
@format = options.delete(:format) || :json
|
18
|
+
@headers = options.delete(:headers) || {}
|
19
|
+
@params = options.delete(:params) || {}
|
20
|
+
@method = options.delete(:method) || :get
|
21
|
+
end.to_s
|
22
|
+
|
23
|
+
@operation = options.delete(:operation) || nil
|
24
|
+
|
25
|
+
on_complete(&block) if block_given?
|
26
|
+
end
|
27
|
+
|
28
|
+
def data?
|
29
|
+
!data.nil?
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Acfs
|
4
|
+
class Request
|
5
|
+
# Module containing callback handling for Requests.
|
6
|
+
# Current the only callback type is `on_complete`:
|
7
|
+
#
|
8
|
+
# request = Request.new 'URL'
|
9
|
+
# request.on_complete { |response| ... }
|
10
|
+
#
|
11
|
+
module Callbacks
|
12
|
+
# Add a new `on_complete` callback for this request.
|
13
|
+
#
|
14
|
+
# @example Set on_complete.
|
15
|
+
# request.on_complete { |response| print response.body }
|
16
|
+
#
|
17
|
+
# @param [ Block ] block The callback block to execute.
|
18
|
+
#
|
19
|
+
# @yield [ Acfs::Response ]
|
20
|
+
#
|
21
|
+
# @return [ Acfs::Request ] The request itself.
|
22
|
+
#
|
23
|
+
def on_complete(&block)
|
24
|
+
callbacks.insert 0, block if block_given?
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
# Return array of all callbacks.
|
29
|
+
#
|
30
|
+
# @return [ Array<Block> ] All callbacks.
|
31
|
+
#
|
32
|
+
def callbacks
|
33
|
+
@callbacks ||= []
|
34
|
+
end
|
35
|
+
|
36
|
+
# Trigger all callback for given response.
|
37
|
+
#
|
38
|
+
# @return [ Acfs::Request ] The request itself.
|
39
|
+
#
|
40
|
+
def complete!(response)
|
41
|
+
call_callback response, 0
|
42
|
+
self
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def call_callback(res, index)
|
48
|
+
return if index >= callbacks.size
|
49
|
+
|
50
|
+
callbacks[index].call(res, proc {|bres| call_callback bres, index + 1 })
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_model'
|
4
|
+
|
5
|
+
# @api public
|
6
|
+
#
|
7
|
+
class Acfs::Resource
|
8
|
+
require 'acfs/resource/initialization'
|
9
|
+
require 'acfs/resource/attributes'
|
10
|
+
require 'acfs/resource/dirty'
|
11
|
+
require 'acfs/resource/loadable'
|
12
|
+
require 'acfs/resource/locatable'
|
13
|
+
require 'acfs/resource/operational'
|
14
|
+
require 'acfs/resource/persistence'
|
15
|
+
require 'acfs/resource/query_methods'
|
16
|
+
require 'acfs/resource/service'
|
17
|
+
require 'acfs/resource/validation'
|
18
|
+
|
19
|
+
if ActiveModel::VERSION::MAJOR >= 4
|
20
|
+
include ActiveModel::Model
|
21
|
+
else
|
22
|
+
extend ActiveModel::Naming
|
23
|
+
extend ActiveModel::Translation
|
24
|
+
include ActiveModel::Conversion
|
25
|
+
include ActiveModel::Validations
|
26
|
+
end
|
27
|
+
|
28
|
+
include Initialization
|
29
|
+
|
30
|
+
include Attributes
|
31
|
+
include Loadable
|
32
|
+
include Persistence
|
33
|
+
include Locatable
|
34
|
+
include Operational
|
35
|
+
include QueryMethods
|
36
|
+
include Service
|
37
|
+
include Dirty
|
38
|
+
include Validation
|
39
|
+
end
|
@@ -0,0 +1,270 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Acfs::Resource
|
4
|
+
#
|
5
|
+
# = Acfs Attributes
|
6
|
+
#
|
7
|
+
# Allows to specify attributes of a class with default
|
8
|
+
# values and type safety.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# class User < Acfs::Resource
|
12
|
+
# attribute :name, :string, default: 'Anon'
|
13
|
+
# attribute :age, :integer
|
14
|
+
# attribute :special, My::Special::Type
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# For each attribute a setter and getter will be created and values will be
|
18
|
+
# type casted when set.
|
19
|
+
#
|
20
|
+
module Attributes
|
21
|
+
extend ActiveSupport::Concern
|
22
|
+
include ActiveModel::AttributeMethods
|
23
|
+
|
24
|
+
# @api public
|
25
|
+
#
|
26
|
+
# Write default attributes defined in resource class.
|
27
|
+
#
|
28
|
+
# @see #write_attributes
|
29
|
+
# @see ClassMethods#attributes
|
30
|
+
#
|
31
|
+
def initialize(*attrs)
|
32
|
+
write_attributes self.class.attributes
|
33
|
+
reset_changes
|
34
|
+
super
|
35
|
+
end
|
36
|
+
|
37
|
+
# @api public
|
38
|
+
#
|
39
|
+
# Returns ActiveModel compatible list of attributes and values.
|
40
|
+
#
|
41
|
+
# @example
|
42
|
+
# class User < Acfs::Resource
|
43
|
+
# attribute :name, type: String, default: 'Anon'
|
44
|
+
# end
|
45
|
+
# user = User.new(name: 'John')
|
46
|
+
# user.attributes # => { "name" => "John" }
|
47
|
+
#
|
48
|
+
# @return [HashWithIndifferentAccess{Symbol => Object}]
|
49
|
+
# Attributes and their values.
|
50
|
+
#
|
51
|
+
# rubocop:disable Naming/MemoizedInstanceVariableName
|
52
|
+
def attributes
|
53
|
+
@_attrs ||= HashWithIndifferentAccess.new
|
54
|
+
end
|
55
|
+
# rubocop:enable Naming/MemoizedInstanceVariableName
|
56
|
+
|
57
|
+
# @api public
|
58
|
+
#
|
59
|
+
# Update all attributes with given hash. Attribute values will be casted
|
60
|
+
# to defined attribute type.
|
61
|
+
#
|
62
|
+
# @example
|
63
|
+
# user.attributes = { :name => 'Adam' }
|
64
|
+
# user.name # => 'Adam'
|
65
|
+
#
|
66
|
+
# @param [Hash{String, Symbol => Object}, #each{|key, value|}]
|
67
|
+
# Attributes to set in resource.
|
68
|
+
# @see #write_attributes Delegates attributes hash to {#write_attributes}.
|
69
|
+
#
|
70
|
+
def attributes=(attributes)
|
71
|
+
write_attributes(attributes)
|
72
|
+
end
|
73
|
+
|
74
|
+
# @api public
|
75
|
+
#
|
76
|
+
# Read an attribute from instance variable.
|
77
|
+
#
|
78
|
+
# @param [Symbol, String] name Attribute name.
|
79
|
+
# @return [Object] Attribute value.
|
80
|
+
#
|
81
|
+
def read_attribute(name)
|
82
|
+
attributes[name.to_s]
|
83
|
+
end
|
84
|
+
|
85
|
+
# @api public
|
86
|
+
#
|
87
|
+
# Write a hash of attributes and values.
|
88
|
+
#
|
89
|
+
# If attribute value is a `Proc` it will be evaluated in the context
|
90
|
+
# of the resource after all non-proc attribute values are set. Values
|
91
|
+
# will be casted to defined attribute type.
|
92
|
+
#
|
93
|
+
# The behavior is used to apply default attributes from resource
|
94
|
+
# class definition.
|
95
|
+
#
|
96
|
+
# @example
|
97
|
+
# user.write_attributes name: 'john', email: ->{ "#{name}@example.org" }
|
98
|
+
# user.name # => 'john'
|
99
|
+
# user.email # => 'john@example.org'
|
100
|
+
#
|
101
|
+
# @param [Hash{String, Symbol => Object, Proc}, #each{|key, value|}]
|
102
|
+
# Attributes to write.
|
103
|
+
#
|
104
|
+
# @see #write_attribute Delegates attribute values to `#write_attribute`.
|
105
|
+
#
|
106
|
+
def write_attributes(attributes, **opts)
|
107
|
+
unless attributes.respond_to?(:each) && attributes.respond_to?(:keys)
|
108
|
+
return false
|
109
|
+
end
|
110
|
+
|
111
|
+
if opts.fetch(:unknown, :ignore) == :raise &&
|
112
|
+
(attributes.keys.map(&:to_s) - self.class.attributes.keys).any?
|
113
|
+
missing = attributes.keys - self.class.attributes.keys
|
114
|
+
missing.map!(&:inspect)
|
115
|
+
raise ArgumentError.new "Unknown attributes: #{missing.join(', ')}"
|
116
|
+
end
|
117
|
+
|
118
|
+
procs = {}
|
119
|
+
|
120
|
+
attributes.each do |key, _|
|
121
|
+
if attributes[key].is_a? Proc
|
122
|
+
procs[key] = attributes[key]
|
123
|
+
else
|
124
|
+
write_local_attribute(key, attributes[key], **opts)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
procs.each do |key, proc|
|
129
|
+
write_local_attribute(key, instance_exec(&proc), **opts)
|
130
|
+
end
|
131
|
+
|
132
|
+
true
|
133
|
+
end
|
134
|
+
|
135
|
+
# @api private
|
136
|
+
#
|
137
|
+
# Check if a public getter for attribute exists that should be called to
|
138
|
+
# write it or of {#write_attribute} should be called directly. This is
|
139
|
+
# necessary as {#write_attribute} should go though setters but can also
|
140
|
+
# handle unknown attribute that will not have a generated setter method.
|
141
|
+
#
|
142
|
+
def write_local_attribute(name, value, opts = {})
|
143
|
+
method = "#{name}="
|
144
|
+
if respond_to? method, true
|
145
|
+
public_send method, value
|
146
|
+
else
|
147
|
+
write_attribute name, value, opts
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# @api public
|
152
|
+
#
|
153
|
+
# Write single attribute with given value. Value will be casted
|
154
|
+
# to defined attribute type.
|
155
|
+
#
|
156
|
+
# @param [String, Symbol] name Attribute name.
|
157
|
+
# @param [Object] value Value to write.
|
158
|
+
# @raise [ArgumentError] If no attribute with given name is defined.
|
159
|
+
#
|
160
|
+
def write_attribute(name, value, opts = {})
|
161
|
+
attr_type = self.class.defined_attributes[name.to_s]
|
162
|
+
if attr_type
|
163
|
+
write_raw_attribute name, attr_type.cast(value), opts
|
164
|
+
else
|
165
|
+
write_raw_attribute name, value, opts
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# @api private
|
170
|
+
#
|
171
|
+
# Write an attribute without checking type or existence or casting
|
172
|
+
# value to attributes type. Value be stored in an instance variable
|
173
|
+
# named after attribute name.
|
174
|
+
#
|
175
|
+
# @param [String, Symbol] name Attribute name.
|
176
|
+
# @param [Object] value Attribute value.
|
177
|
+
#
|
178
|
+
def write_raw_attribute(name, value, _ = {})
|
179
|
+
attributes[name.to_s] = value
|
180
|
+
end
|
181
|
+
|
182
|
+
module ClassMethods
|
183
|
+
ATTR_CLASS_BASE = '::Acfs::Resource::Attributes'
|
184
|
+
|
185
|
+
#
|
186
|
+
# @api public
|
187
|
+
#
|
188
|
+
# Define a model attribute by name and type. Will create getter and
|
189
|
+
# setter for given attribute name. Existing methods will be overridden.
|
190
|
+
#
|
191
|
+
# Available types can be found in `Acfs::Model::Attributes::*`.
|
192
|
+
#
|
193
|
+
# @example
|
194
|
+
# class User < Acfs::Resource
|
195
|
+
# attribute :name, :string, default: 'Anon'
|
196
|
+
# attribute :email, :string, default: lambda{ "#{name}@example.org"}
|
197
|
+
# end
|
198
|
+
#
|
199
|
+
# @param [#to_sym] name Attribute name.
|
200
|
+
# @param [Symbol, String, Class] type Attribute
|
201
|
+
# type identifier or type class.
|
202
|
+
#
|
203
|
+
def attribute(name, type, **opts)
|
204
|
+
if type.is_a?(Symbol) || type.is_a?(String)
|
205
|
+
type = "#{ATTR_CLASS_BASE}::#{type.to_s.classify}".constantize
|
206
|
+
end
|
207
|
+
|
208
|
+
define_attribute(name.to_sym, type, **opts)
|
209
|
+
end
|
210
|
+
|
211
|
+
# @api public
|
212
|
+
#
|
213
|
+
# Return list of possible attributes and default
|
214
|
+
# values for this model class.
|
215
|
+
#
|
216
|
+
# @example
|
217
|
+
# class User < Acfs::Resource
|
218
|
+
# attribute :name, :string
|
219
|
+
# attribute :age, :integer, default: 25
|
220
|
+
# end
|
221
|
+
# User.attributes # => { "name": nil, "age": 25 }
|
222
|
+
#
|
223
|
+
# @return [Hash{String => Object, Proc}]
|
224
|
+
# Attributes with default values.
|
225
|
+
#
|
226
|
+
def attributes
|
227
|
+
defined_attributes.each_with_object({}) do |(key, attr), hash|
|
228
|
+
hash[key] = attr.default_value
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
def defined_attributes
|
233
|
+
if superclass.respond_to?(:defined_attributes)
|
234
|
+
superclass.defined_attributes.merge(local_attributes)
|
235
|
+
else
|
236
|
+
local_attributes
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
private
|
241
|
+
|
242
|
+
def local_attributes
|
243
|
+
@local_attributes ||= {}
|
244
|
+
end
|
245
|
+
|
246
|
+
def define_attribute(name, type, **opts)
|
247
|
+
name = name.to_s
|
248
|
+
attribute = type.new(**opts)
|
249
|
+
|
250
|
+
local_attributes[name] = attribute
|
251
|
+
define_attribute_method name
|
252
|
+
|
253
|
+
send :define_method, name do
|
254
|
+
read_attribute name
|
255
|
+
end
|
256
|
+
|
257
|
+
send :define_method, :"#{name}=" do |value|
|
258
|
+
write_attribute name, value
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
# Load attribute type classes.
|
266
|
+
#
|
267
|
+
Dir[File.join(__dir__, 'attributes/*.rb')].sort.each do |path|
|
268
|
+
filename = File.basename(path)
|
269
|
+
require "acfs/resource/attributes/#{filename}"
|
270
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Acfs::Resource::Attributes
|
4
|
+
class Base
|
5
|
+
attr_reader :default
|
6
|
+
|
7
|
+
def initialize(default: nil)
|
8
|
+
@default = default
|
9
|
+
end
|
10
|
+
|
11
|
+
def cast(value)
|
12
|
+
cast_value(value) unless value.nil?
|
13
|
+
end
|
14
|
+
|
15
|
+
def default_value
|
16
|
+
if default.respond_to? :call
|
17
|
+
default
|
18
|
+
else
|
19
|
+
cast default
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def cast_value(_value)
|
26
|
+
raise NotImplementedError
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Acfs::Resource::Attributes
|
4
|
+
# @api public
|
5
|
+
#
|
6
|
+
# Boolean attribute type. Use it in your model as an attribute type:
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# class User < Acfs::Resource
|
10
|
+
# attribute :name, :boolean
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# Given objects will be converted to string. The following strings
|
14
|
+
# are considered true, everything else false:
|
15
|
+
#
|
16
|
+
# true, on, yes
|
17
|
+
#
|
18
|
+
class Boolean < Base
|
19
|
+
FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE', 'off', 'OFF', 'no', 'NO'].to_set
|
20
|
+
|
21
|
+
# @api public
|
22
|
+
#
|
23
|
+
# Cast given object to boolean.
|
24
|
+
#
|
25
|
+
# @param [Object] value Object to cast.
|
26
|
+
# @return [TrueClass, FalseClass] Casted boolean.
|
27
|
+
#
|
28
|
+
def cast_value(value)
|
29
|
+
return true if value == true
|
30
|
+
return false if value == false
|
31
|
+
|
32
|
+
if value.blank?
|
33
|
+
nil
|
34
|
+
else
|
35
|
+
!FALSE_VALUES.include?(value)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|