sp-duh 2.0.6
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/LICENSE +661 -0
- data/README.md +2 -0
- data/Rakefile +32 -0
- data/config/i18n/i18n.xlsx +0 -0
- data/config/initializers/active_record/connection_adapters_postgre_sql_adapter.rb +165 -0
- data/config/initializers/active_record/migration_without_transaction.rb +4 -0
- data/config/initializers/active_record/migrator.rb +34 -0
- data/config/initializers/rails/generators.rb +13 -0
- data/config/jsonapi/settings.yml +14 -0
- data/config/locales/pt.yml +15 -0
- data/lib/generators/accounting_migration/accounting_migration_generator.rb +10 -0
- data/lib/generators/accounting_migration/templates/migration.rb +42 -0
- data/lib/generators/accounting_payroll_migration/accounting_payroll_migration_generator.rb +10 -0
- data/lib/generators/accounting_payroll_migration/templates/migration.rb +73 -0
- data/lib/generators/sharded_migration/sharded_migration_generator.rb +10 -0
- data/lib/generators/sharded_migration/templates/migration.rb +45 -0
- data/lib/sp-duh.rb +32 -0
- data/lib/sp/duh.rb +180 -0
- data/lib/sp/duh/adapters/pg/text_decoder/json.rb +15 -0
- data/lib/sp/duh/adapters/pg/text_encoder/json.rb +15 -0
- data/lib/sp/duh/db/transfer/backup.rb +71 -0
- data/lib/sp/duh/db/transfer/restore.rb +89 -0
- data/lib/sp/duh/engine.rb +35 -0
- data/lib/sp/duh/exceptions.rb +70 -0
- data/lib/sp/duh/i18n/excel_loader.rb +26 -0
- data/lib/sp/duh/jsonapi/adapters/base.rb +168 -0
- data/lib/sp/duh/jsonapi/adapters/db.rb +36 -0
- data/lib/sp/duh/jsonapi/adapters/raw_db.rb +77 -0
- data/lib/sp/duh/jsonapi/configuration.rb +167 -0
- data/lib/sp/duh/jsonapi/doc/apidoc_documentation_format_generator.rb +286 -0
- data/lib/sp/duh/jsonapi/doc/generator.rb +32 -0
- data/lib/sp/duh/jsonapi/doc/schema_catalog_helper.rb +97 -0
- data/lib/sp/duh/jsonapi/doc/victor_pinus_metadata_format_parser.rb +374 -0
- data/lib/sp/duh/jsonapi/exceptions.rb +56 -0
- data/lib/sp/duh/jsonapi/model/base.rb +25 -0
- data/lib/sp/duh/jsonapi/model/concerns/attributes.rb +94 -0
- data/lib/sp/duh/jsonapi/model/concerns/model.rb +42 -0
- data/lib/sp/duh/jsonapi/model/concerns/persistence.rb +221 -0
- data/lib/sp/duh/jsonapi/model/concerns/serialization.rb +59 -0
- data/lib/sp/duh/jsonapi/parameters.rb +44 -0
- data/lib/sp/duh/jsonapi/resource_publisher.rb +28 -0
- data/lib/sp/duh/jsonapi/service.rb +110 -0
- data/lib/sp/duh/migrations.rb +47 -0
- data/lib/sp/duh/migrations/migrator.rb +41 -0
- data/lib/sp/duh/repl.rb +193 -0
- data/lib/sp/duh/version.rb +25 -0
- data/lib/tasks/db_utils.rake +98 -0
- data/lib/tasks/doc.rake +27 -0
- data/lib/tasks/i18n.rake +23 -0
- data/lib/tasks/oauth.rake +29 -0
- data/lib/tasks/transfer.rake +48 -0
- data/lib/tasks/xls2jrxml.rake +15 -0
- data/test/jsonapi/server.rb +67 -0
- data/test/tasks/test.rake +10 -0
- metadata +170 -0
@@ -0,0 +1,56 @@
|
|
1
|
+
module SP
|
2
|
+
module Duh
|
3
|
+
module JSONAPI
|
4
|
+
module Exceptions
|
5
|
+
|
6
|
+
# JSONAPI service and configuration errors
|
7
|
+
|
8
|
+
class ServiceSetupError < SP::Duh::Exceptions::GenericError ; ; end
|
9
|
+
class ServiceProtocolError < SP::Duh::Exceptions::GenericDetailedError ; ; end
|
10
|
+
class InvalidResourceConfigurationError < SP::Duh::Exceptions::GenericDetailedError ; ; end
|
11
|
+
class InvalidResourcePublisherError < SP::Duh::Exceptions::GenericDetailedError ; ; end
|
12
|
+
class DuplicateResourceError < SP::Duh::Exceptions::GenericDetailedError ; ; end
|
13
|
+
class SaveConfigurationError < SP::Duh::Exceptions::GenericError ; ; end
|
14
|
+
class InvalidJSONAPIKeyError < SP::Duh::Exceptions::GenericDetailedError ; ; end
|
15
|
+
|
16
|
+
# JSONAPI model querying errors
|
17
|
+
|
18
|
+
class GenericModelError < SP::Duh::Exceptions::GenericError
|
19
|
+
|
20
|
+
attr_reader :id
|
21
|
+
attr_reader :status
|
22
|
+
attr_reader :result
|
23
|
+
|
24
|
+
def initialize(result, nested = $!)
|
25
|
+
@result = result
|
26
|
+
errors = get_result_errors()
|
27
|
+
@status = (errors.map { |error| error[:status].to_i }.max) || 403
|
28
|
+
message = errors.first[:detail]
|
29
|
+
super(message, nested)
|
30
|
+
end
|
31
|
+
|
32
|
+
def internal_error
|
33
|
+
errors = get_result_errors()
|
34
|
+
if errors.length != 1
|
35
|
+
@result.to_json
|
36
|
+
else
|
37
|
+
errors.first[:meta]['internal-error'] if errors.first[:meta]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def inspect()
|
42
|
+
description = super()
|
43
|
+
description = description + " (#{internal_error})" if internal_error
|
44
|
+
description
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def get_result_errors() ; (result.is_a?(Hash) ? result : HashWithIndifferentAccess.new(JSON.parse(result)))[:errors] ; end
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'sp/duh/jsonapi/model/concerns/model'
|
2
|
+
|
3
|
+
module SP
|
4
|
+
module Duh
|
5
|
+
module JSONAPI
|
6
|
+
module Model
|
7
|
+
|
8
|
+
class Base
|
9
|
+
include ::ActiveRecord::Validations
|
10
|
+
include Concerns::Model
|
11
|
+
|
12
|
+
def self.inherited(child)
|
13
|
+
child.resource_name = child.name.demodulize.underscore.pluralize
|
14
|
+
child.autogenerated_id = true
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.i18n_scope
|
18
|
+
:activerecord
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module SP
|
2
|
+
module Duh
|
3
|
+
module JSONAPI
|
4
|
+
module Model
|
5
|
+
module Concerns
|
6
|
+
module Attributes
|
7
|
+
extend ::ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
|
14
|
+
def attributes
|
15
|
+
if !@attributes && superclass.respond_to?(:attributes)
|
16
|
+
@attributes = []
|
17
|
+
@attributes += superclass.attributes
|
18
|
+
end
|
19
|
+
@attributes = [] if !@attributes
|
20
|
+
@attributes
|
21
|
+
end
|
22
|
+
|
23
|
+
def attr_accessible(name)
|
24
|
+
attributes << name if !attributes.include?(name)
|
25
|
+
attr_accessor name
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize(new_attributes = nil)
|
30
|
+
assign_attributes(new_attributes) if new_attributes
|
31
|
+
yield self if block_given?
|
32
|
+
end
|
33
|
+
|
34
|
+
def attributes=(new_attributes)
|
35
|
+
return unless new_attributes.is_a?(Hash)
|
36
|
+
assign_attributes(new_attributes)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def assign_attributes(new_attributes)
|
42
|
+
return if new_attributes.blank?
|
43
|
+
|
44
|
+
new_attributes = new_attributes.stringify_keys
|
45
|
+
nested_parameter_attributes = []
|
46
|
+
|
47
|
+
new_attributes.each do |k, v|
|
48
|
+
if respond_to?("#{k}=")
|
49
|
+
if v.is_a?(Hash)
|
50
|
+
nested_parameter_attributes << [ k, v ]
|
51
|
+
else
|
52
|
+
send("#{k}=", v)
|
53
|
+
end
|
54
|
+
# else
|
55
|
+
# raise(ActiveRecord::UnknownAttributeError, "unknown attribute: #{k}")
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Assign any deferred nested attributes after the base attributes have been set
|
60
|
+
nested_parameter_attributes.each do |k,v|
|
61
|
+
send("#{k}=", v)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Returns an <tt>#inspect</tt>-like string for the value of the
|
66
|
+
# attribute +attr_name+. String attributes are truncated upto 50
|
67
|
+
# characters, and Date and Time attributes are returned in the
|
68
|
+
# <tt>:db</tt> format. Other attributes return the value of
|
69
|
+
# <tt>#inspect</tt> without modification.
|
70
|
+
#
|
71
|
+
# person = Person.create!(:name => "David Heinemeier Hansson " * 3)
|
72
|
+
#
|
73
|
+
# person.attribute_for_inspect(:name)
|
74
|
+
# # => '"David Heinemeier Hansson David Heinemeier Hansson D..."'
|
75
|
+
#
|
76
|
+
# person.attribute_for_inspect(:created_at)
|
77
|
+
# # => '"2009-01-12 04:48:57"'
|
78
|
+
def attribute_for_inspect(name)
|
79
|
+
value = self.send(name)
|
80
|
+
if value.is_a?(String) && value.length > 50
|
81
|
+
"#{value[0..50]}...".inspect
|
82
|
+
elsif value.is_a?(Date) || value.is_a?(Time)
|
83
|
+
%("#{value.to_s(:db)}")
|
84
|
+
else
|
85
|
+
value.inspect
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require 'active_record'
|
3
|
+
|
4
|
+
require 'sp/duh/jsonapi/model/concerns/attributes'
|
5
|
+
require 'sp/duh/jsonapi/model/concerns/serialization'
|
6
|
+
require 'sp/duh/jsonapi/model/concerns/persistence'
|
7
|
+
|
8
|
+
module SP
|
9
|
+
module Duh
|
10
|
+
module JSONAPI
|
11
|
+
module Model
|
12
|
+
module Concerns
|
13
|
+
module Model
|
14
|
+
extend ::ActiveSupport::Concern
|
15
|
+
|
16
|
+
include Attributes
|
17
|
+
include Serialization
|
18
|
+
include Persistence
|
19
|
+
|
20
|
+
included do
|
21
|
+
end
|
22
|
+
|
23
|
+
module ClassMethods
|
24
|
+
|
25
|
+
def inspect
|
26
|
+
"#{super}(#{self.attributes.join(', ')})"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Returns the contents of the record as a nicely formatted string.
|
31
|
+
def inspect
|
32
|
+
# attrs = self.class.attributes
|
33
|
+
inspection = self.class.attributes.collect { |name| "#{name}: #{attribute_for_inspect(name)}" }.compact.join(", ")
|
34
|
+
"#<#{self.class} #{inspection}>"
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,221 @@
|
|
1
|
+
module SP
|
2
|
+
module Duh
|
3
|
+
module JSONAPI
|
4
|
+
module Model
|
5
|
+
module Concerns
|
6
|
+
module Persistence
|
7
|
+
extend ::ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do
|
10
|
+
|
11
|
+
# Idem for data adapter configuration...
|
12
|
+
# In a similar way to ActiveRecord::Base.connection, the adapter should be defined at the base level and is inherited by all subclasses
|
13
|
+
class_attribute :adapter, instance_reader: false, instance_writer: false
|
14
|
+
|
15
|
+
self.autogenerated_id = true
|
16
|
+
|
17
|
+
attr_accessible :id
|
18
|
+
end
|
19
|
+
|
20
|
+
module ClassMethods
|
21
|
+
|
22
|
+
# Define resource configuration accessors at the class (and subclass) level (static).
|
23
|
+
# These attribute values are NOT inherited by subclasses, each subclass MUST define their own.
|
24
|
+
# Instances can access these attributes at the class level only.
|
25
|
+
attr_accessor :resource_name
|
26
|
+
attr_accessor :autogenerated_id
|
27
|
+
|
28
|
+
def find!(id, conditions = nil) ; get(id, conditions) ; end
|
29
|
+
|
30
|
+
def find_explicit!(exp_accounting_schema, exp_accounting_prefix, id, conditions = nil)
|
31
|
+
get_explicit(exp_accounting_schema, exp_accounting_prefix, id, conditions)
|
32
|
+
end
|
33
|
+
|
34
|
+
def find(id, conditions = nil)
|
35
|
+
begin
|
36
|
+
get(id, conditions)
|
37
|
+
rescue Exception => e
|
38
|
+
return nil
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def query!(condition) ; get_all(condition) ; end
|
43
|
+
def query_explicit!(exp_accounting_schema, exp_accounting_prefix, condition) ; get_all_explicit(exp_accounting_schema, exp_accounting_prefix, condition) ; end
|
44
|
+
|
45
|
+
def query(condition)
|
46
|
+
begin
|
47
|
+
get_all(condition)
|
48
|
+
rescue Exception => e
|
49
|
+
nil
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def first!(condition = "")
|
54
|
+
condition += (condition.blank? ? "" : "&") + "page[size]=1"
|
55
|
+
get_all(condition).first
|
56
|
+
end
|
57
|
+
|
58
|
+
def first(condition = "")
|
59
|
+
begin
|
60
|
+
condition += (condition.blank? ? "" : "&") + "page[size]=1"
|
61
|
+
get_all(condition).first
|
62
|
+
rescue Exception => e
|
63
|
+
nil
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def all! ; get_all("") ; end
|
68
|
+
|
69
|
+
def all
|
70
|
+
begin
|
71
|
+
get_all("")
|
72
|
+
rescue Exception => e
|
73
|
+
nil
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def get_explicit(exp_accounting_schema, exp_accounting_prefix, id, conditions = nil)
|
80
|
+
result = self.adapter.get_explicit!(exp_accounting_schema, exp_accounting_prefix, "#{self.resource_name}/#{id.to_s}", conditions)
|
81
|
+
jsonapi_result_to_instance(result[:data], result)
|
82
|
+
end
|
83
|
+
|
84
|
+
def get(id, conditions = nil)
|
85
|
+
result = self.adapter.get("#{self.resource_name}/#{id.to_s}", conditions)
|
86
|
+
jsonapi_result_to_instance(result[:data], result)
|
87
|
+
end
|
88
|
+
|
89
|
+
def get_all(condition)
|
90
|
+
got = []
|
91
|
+
result = self.adapter.get(self.resource_name, condition)
|
92
|
+
if result
|
93
|
+
got = result[:data].map do |item|
|
94
|
+
data = { data: item }
|
95
|
+
data.merge(included: result[:included]) if result[:included]
|
96
|
+
jsonapi_result_to_instance(item, data)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
got
|
100
|
+
end
|
101
|
+
|
102
|
+
def get_all_explicit(exp_accounting_schema, exp_accounting_prefix, condition)
|
103
|
+
got = []
|
104
|
+
result = self.adapter.get_explicit!(exp_accounting_schema, exp_accounting_prefix, self.resource_name, condition)
|
105
|
+
if result
|
106
|
+
got = result[:data].map do |item|
|
107
|
+
data = { data: item }
|
108
|
+
data.merge(included: result[:included]) if result[:included]
|
109
|
+
jsonapi_result_to_instance(item, data)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
got
|
113
|
+
end
|
114
|
+
|
115
|
+
def jsonapi_result_to_instance(result, data)
|
116
|
+
if result
|
117
|
+
instance = self.new(result.merge(result[:attributes]).except(:attributes))
|
118
|
+
instance.send :_data=, data
|
119
|
+
end
|
120
|
+
instance
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# Instance methods
|
125
|
+
|
126
|
+
def new_record?
|
127
|
+
if self.class.autogenerated_id || self.id.nil?
|
128
|
+
self.id.nil?
|
129
|
+
else
|
130
|
+
self.class.find(self.id).nil?
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def save!
|
135
|
+
if new_record?
|
136
|
+
create!
|
137
|
+
else
|
138
|
+
update!
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def save_explicit!(exp_accounting_schema, exp_accounting_prefix)
|
143
|
+
if new_record?
|
144
|
+
create!(exp_accounting_schema, exp_accounting_prefix)
|
145
|
+
else
|
146
|
+
update!(exp_accounting_schema, exp_accounting_prefix)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def destroy!
|
151
|
+
if !new_record?
|
152
|
+
self.class.adapter.delete(path_for_id)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def destroy_explicit!(exp_accounting_schema, exp_accounting_prefix)
|
157
|
+
if !new_record?
|
158
|
+
self.class.adapter.delete_explicit!(exp_accounting_schema, exp_accounting_prefix, path_for_id)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
alias :delete! :destroy!
|
163
|
+
|
164
|
+
def create!(exp_accounting_schema = nil, exp_accounting_prefix = nil)
|
165
|
+
if self.class.autogenerated_id
|
166
|
+
params = {
|
167
|
+
data: {
|
168
|
+
type: self.class.resource_name,
|
169
|
+
attributes: get_persistent_json.reject { |k,v| k == :id || v.nil? }
|
170
|
+
}
|
171
|
+
}
|
172
|
+
else
|
173
|
+
params = {
|
174
|
+
data: {
|
175
|
+
type: self.class.resource_name,
|
176
|
+
attributes: get_persistent_json.reject { |k,v| v.nil? }
|
177
|
+
}
|
178
|
+
}
|
179
|
+
end
|
180
|
+
result = if !exp_accounting_schema.blank? || !exp_accounting_prefix.blank?
|
181
|
+
self.class.adapter.post_explicit!(exp_accounting_schema, exp_accounting_prefix, self.class.resource_name, params)
|
182
|
+
else
|
183
|
+
self.class.adapter.post(self.class.resource_name, params)
|
184
|
+
end
|
185
|
+
# Set the id to the newly created id
|
186
|
+
self.id = result[:data][:id]
|
187
|
+
end
|
188
|
+
|
189
|
+
def update!(exp_accounting_schema = nil, exp_accounting_prefix = nil)
|
190
|
+
params = {
|
191
|
+
data: {
|
192
|
+
type: self.class.resource_name,
|
193
|
+
id: self.id.to_s,
|
194
|
+
attributes: get_persistent_json.reject { |k,v| k == :id }
|
195
|
+
}
|
196
|
+
}
|
197
|
+
result = if !exp_accounting_schema.blank? || !exp_accounting_prefix.blank?
|
198
|
+
self.class.adapter.patch_explicit!(exp_accounting_schema, exp_accounting_prefix, path_for_id, params)
|
199
|
+
else
|
200
|
+
self.class.adapter.patch(path_for_id, params)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def get_persistent_json
|
205
|
+
as_json.reject { |k| !k.in?(self.class.attributes) }
|
206
|
+
end
|
207
|
+
|
208
|
+
protected
|
209
|
+
|
210
|
+
attr_accessor :_data
|
211
|
+
|
212
|
+
private
|
213
|
+
|
214
|
+
def path_for_id ; "#{self.class.resource_name}/#{self.id.to_s}" ; end
|
215
|
+
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module SP
|
2
|
+
module Duh
|
3
|
+
module JSONAPI
|
4
|
+
module Model
|
5
|
+
module Concerns
|
6
|
+
module Serialization
|
7
|
+
extend ::ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do
|
10
|
+
end
|
11
|
+
|
12
|
+
def as_json(options = {})
|
13
|
+
root = ActiveRecord::Base.include_root_in_json
|
14
|
+
root = options[:root] if options.try(:key?, :root)
|
15
|
+
if root
|
16
|
+
root = self.class.name.underscore.gsub('/','_').to_sym
|
17
|
+
{ root => serializable_hash(options) }
|
18
|
+
else
|
19
|
+
serializable_hash(options)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def from_json(json)
|
24
|
+
root = ActiveRecord::Base.include_root_in_json
|
25
|
+
hash = ActiveSupport::JSON.decode(json)
|
26
|
+
hash = hash.values.first if root
|
27
|
+
self.attributes = hash
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
alias :read_attribute_for_serialization :send
|
34
|
+
|
35
|
+
def serializable_hash(options = {})
|
36
|
+
|
37
|
+
attribute_names = self.class.attributes.sort
|
38
|
+
if only = options[:only]
|
39
|
+
attribute_names &= Array.wrap(only).map(&:to_s)
|
40
|
+
elsif except = options[:except]
|
41
|
+
attribute_names -= Array.wrap(except).map(&:to_s)
|
42
|
+
end
|
43
|
+
|
44
|
+
hash = {}
|
45
|
+
attribute_names.each { |n| hash[n] = read_attribute_for_serialization(n) }
|
46
|
+
|
47
|
+
method_names = Array.wrap(options[:methods]).select { |n| respond_to?(n) }
|
48
|
+
method_names.each { |n| hash[n] = send(n) }
|
49
|
+
|
50
|
+
hash
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|