lite-command 1.5.0 → 2.0.1
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/.rubocop.yml +23 -4
- data/CHANGELOG.md +8 -0
- data/Gemfile +1 -1
- data/Gemfile.lock +124 -100
- data/README.md +85 -231
- data/Rakefile +2 -2
- data/bin/console +3 -3
- data/lib/generators/rails/command_generator.rb +5 -5
- data/lib/generators/rails/templates/command.rb.tt +1 -1
- data/lib/lite/command/base.rb +49 -0
- data/lib/lite/command/context.rb +32 -0
- data/lib/lite/command/fault.rb +54 -0
- data/lib/lite/command/internals/callable.rb +158 -0
- data/lib/lite/command/internals/executable.rb +83 -0
- data/lib/lite/command/internals/resultable.rb +78 -0
- data/lib/lite/command/version.rb +1 -1
- data/lib/lite/command.rb +9 -13
- data/lite-command.gemspec +27 -29
- metadata +12 -55
- data/lib/lite/command/complex.rb +0 -52
- data/lib/lite/command/exceptions.rb +0 -10
- data/lib/lite/command/extensions/errors.rb +0 -88
- data/lib/lite/command/extensions/memoize.rb +0 -17
- data/lib/lite/command/extensions/propagation.rb +0 -37
- data/lib/lite/command/procedure.rb +0 -51
- data/lib/lite/command/simple.rb +0 -19
- data/lib/lite/command/states.rb +0 -31
data/README.md
CHANGED
@@ -4,7 +4,6 @@
|
|
4
4
|
[](https://travis-ci.org/drexed/lite-command)
|
5
5
|
|
6
6
|
Lite::Command provides an API for building simple and complex command based service objects.
|
7
|
-
It provides extensions for handling errors and memoization to improve your object workflow productivity.
|
8
7
|
|
9
8
|
## Installation
|
10
9
|
|
@@ -25,273 +24,128 @@ Or install it yourself as:
|
|
25
24
|
## Table of Contents
|
26
25
|
|
27
26
|
* [Setup](#setup)
|
28
|
-
* [
|
29
|
-
* [
|
30
|
-
* [
|
31
|
-
* [
|
27
|
+
* [Execution](#execution)
|
28
|
+
* [Context](#context)
|
29
|
+
* [Internals](#Internals)
|
30
|
+
* [Generator](#generator)
|
32
31
|
|
33
32
|
## Setup
|
34
33
|
|
35
|
-
|
36
|
-
|
37
|
-
```erb
|
38
|
-
app/commands/[NAME]_command.rb
|
39
|
-
```
|
40
|
-
|
41
|
-
If a `ApplicationCommand` file in the `app/commands` directory is available, the
|
42
|
-
generator will create file that inherit from `ApplicationCommand` if not it will
|
43
|
-
fallback to `Lite::Command::Complex`.
|
44
|
-
|
45
|
-
## Simple
|
46
|
-
|
47
|
-
Simple commands are a traditional command service call objects.
|
48
|
-
It only exposes a `call` method that returns a value.
|
49
|
-
|
50
|
-
**Setup**
|
34
|
+
Defining a command is as simple as adding a call method.
|
51
35
|
|
52
36
|
```ruby
|
53
|
-
class CalculatePower < Lite::Command::
|
37
|
+
class CalculatePower < Lite::Command::Base
|
54
38
|
|
55
|
-
|
56
|
-
|
57
|
-
a**b
|
39
|
+
def call
|
40
|
+
# TODO: implement calculator
|
58
41
|
end
|
59
42
|
|
60
43
|
end
|
61
44
|
```
|
62
45
|
|
63
|
-
|
64
|
-
|
65
|
-
```ruby
|
66
|
-
CalculatePower.execute(2, 2) #=> 4
|
67
|
-
|
68
|
-
# - or -
|
69
|
-
|
70
|
-
CalculatePower.call(2, 3) #=> 8
|
71
|
-
```
|
72
|
-
|
73
|
-
## Complex
|
74
|
-
|
75
|
-
Complex commands are powerful command service call objects.
|
76
|
-
It can be extended to use error, memoization, and propagation mixins.
|
77
|
-
|
78
|
-
**Setup**
|
79
|
-
|
80
|
-
```ruby
|
81
|
-
class SearchMovies < Lite::Command::Complex
|
82
|
-
|
83
|
-
attr_reader :name
|
84
|
-
|
85
|
-
def initialize(name)
|
86
|
-
@name = name
|
87
|
-
end
|
88
|
-
|
89
|
-
# NOTE: This `execute` instance method is required to use with call
|
90
|
-
def execute
|
91
|
-
{ generate_fingerprint => movies_by_name }
|
92
|
-
end
|
93
|
-
|
94
|
-
private
|
95
|
-
|
96
|
-
def movies_by_name
|
97
|
-
HTTP.get("http://movies.com?title=#{name}")
|
98
|
-
end
|
46
|
+
## Execution
|
99
47
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
end
|
105
|
-
```
|
48
|
+
Executing a command can be done as an instance or class call.
|
49
|
+
It returns the command instance in a forzen state.
|
50
|
+
These will never call will never raise an execption, but will
|
51
|
+
be kept track of in its internal state.
|
106
52
|
|
107
|
-
**
|
53
|
+
**NOTE:** Class calls is the prefered format due to its readability.
|
108
54
|
|
109
55
|
```ruby
|
110
|
-
|
111
|
-
|
112
|
-
# - or -
|
113
|
-
|
114
|
-
command = SearchMovies.new('Toy Story')
|
115
|
-
command.called? #=> false
|
116
|
-
command.call #=> { 'fingerprint_1' => [ 'Toy Story 1', ... ] }
|
117
|
-
command.called? #=> true
|
56
|
+
# Class call
|
57
|
+
CalculatePower.call(..args)
|
118
58
|
|
119
|
-
#
|
59
|
+
# Instance call
|
60
|
+
caculator = CalculatePower.new(..args).call
|
120
61
|
|
121
|
-
|
122
|
-
command.called? #=> true
|
123
|
-
command.call #=> { 'fingerprint_1' => [ 'Toy Story 1', ... ] }
|
62
|
+
#=> <CalculatePower ...>
|
124
63
|
```
|
125
64
|
|
126
|
-
|
65
|
+
Commands can be called with a `!` bang method to raise a
|
66
|
+
`Lite::Command::Fault` based exception or the original
|
67
|
+
`StandardError` based exception.
|
127
68
|
|
128
69
|
```ruby
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
command.call #=> { 'fingerprint_1' => [ 'Toy Story 1', ... ] }
|
133
|
-
command.result #=> { 'fingerprint_1' => [ 'Toy Story 1', ... ] }
|
134
|
-
|
135
|
-
command.recall! #=> Clears the `call`, `cache`, `errors` variables and then re-performs the call
|
136
|
-
command.result #=> { 'fingerprint_2' => [ 'Toy Story 2', ... ] }
|
70
|
+
CalculatePower.call!(..args)
|
71
|
+
#=> raises Lite::Command::Fault
|
137
72
|
```
|
138
73
|
|
139
|
-
##
|
140
|
-
|
141
|
-
Procedures are used to run a collection of commands. It uses the the complex procedure API
|
142
|
-
so it has access to all the methods. The `execute` method is already defined to
|
143
|
-
handle most common procedure steps. It can be use directly or subclassed.
|
74
|
+
## Context
|
144
75
|
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
class SearchChannels < Lite::Command::Procedure; end
|
149
|
-
```
|
76
|
+
Accessing the call arguments can be done through its internal context.
|
77
|
+
It can be used as internal storage to be accessed by it self and any
|
78
|
+
of its children commands.
|
150
79
|
|
151
80
|
```ruby
|
152
|
-
|
153
|
-
|
154
|
-
procedure = SearchChannels.call(*commands)
|
155
|
-
procedure.result #=> ['disney: #3', 'espn: #59', 'mtv: #212']
|
156
|
-
procedure.steps #=> [<DisneyChannel @result="...">, <EspnChannel @result="...">, <MtvChannel @result="...">]
|
157
|
-
|
158
|
-
# - or -
|
159
|
-
|
160
|
-
# If the errors extension is added you can stop the procedure at first failure.
|
161
|
-
procedure = SearchChannels.new(*commands)
|
162
|
-
procedure.exit_on_failure = true
|
163
|
-
procedure.call
|
164
|
-
procedure.result #=> ['disney: #3']
|
165
|
-
procedure.failed_steps #=> [{ index: 1, step: 2, name: 'ErrorChannel', args: [current_station], errors: ['field error message'] }]
|
166
|
-
```
|
167
|
-
|
168
|
-
## Extensions
|
81
|
+
class CalculatePower < Lite::Command::Base
|
169
82
|
|
170
|
-
|
171
|
-
|
172
|
-
### Errors (optional)
|
173
|
-
|
174
|
-
Learn more about using [Lite::Errors](https://github.com/drexed/lite-errors)
|
175
|
-
|
176
|
-
**Setup**
|
177
|
-
|
178
|
-
```ruby
|
179
|
-
class SearchMovies < Lite::Command::Complex
|
180
|
-
include Lite::Command::Extensions::Errors
|
181
|
-
|
182
|
-
# ... ommited ...
|
183
|
-
|
184
|
-
private
|
185
|
-
|
186
|
-
# Add a explicit and/or exception errors to the error pool
|
187
|
-
def generate_fingerprint
|
188
|
-
if movies_by_name.nil?
|
189
|
-
errors.add(:fingerprint, 'invalid md5 request value')
|
190
|
-
else
|
191
|
-
Digest::MD5.hexdigest(movies_by_name)
|
192
|
-
end
|
193
|
-
rescue ArgumentError => exception
|
194
|
-
merge_exception!(exception, key: :custom_error_key)
|
83
|
+
def call
|
84
|
+
context.result = context.a ** context.b
|
195
85
|
end
|
196
86
|
|
197
87
|
end
|
198
|
-
```
|
199
|
-
|
200
|
-
**Instance Callers**
|
201
|
-
|
202
|
-
```ruby
|
203
|
-
command = SearchMovies.call('Toy Story')
|
204
|
-
command.errors #=> Lite::Errors::Messages object
|
205
|
-
|
206
|
-
command.validate! #=> Raises Lite::Command::ValidationError if it has any errors
|
207
|
-
command.valid? #=> Alias for validate!
|
208
|
-
|
209
|
-
command.errored? #=> false
|
210
|
-
command.success? #=> true
|
211
|
-
command.failure? #=> Checks that it has been called and has errors
|
212
|
-
command.status #=> :failure
|
213
|
-
|
214
|
-
command.result! #=> Raises Lite::Command::ValidationError if it has any errors, if not it returns the result
|
215
|
-
|
216
|
-
# Use the following to merge errors from other commands or models
|
217
|
-
# with the default direction being `:from`
|
218
|
-
command.merge_errors!(command_2)
|
219
|
-
user_model.merge_errors!(command, direction: :to)
|
220
|
-
```
|
221
|
-
|
222
|
-
**Block Callers**
|
223
|
-
|
224
|
-
```ruby
|
225
|
-
# Useful for controllers or actions that depend on states.
|
226
|
-
SearchMovies.perform('Toy Story') do |result, success, failure|
|
227
|
-
success.call { redirect_to(movie_path, notice: "Movie can be found at: #{result}") }
|
228
|
-
failure.call { redirect_to(root_path, notice: "Movie cannot be found at: #{result}") }
|
229
|
-
end
|
230
|
-
```
|
231
|
-
|
232
|
-
### Propagation (optional)
|
233
|
-
|
234
|
-
Propagation methods help you perform an action on an object. If successful is
|
235
|
-
returns the result else it adds the object errors to the form object. Available
|
236
|
-
propagation methods are:
|
237
|
-
- `assign_and_return!(object, params)`
|
238
|
-
- `create_and_return!(klass, params)`
|
239
|
-
- `update_and_return!(object, params)`
|
240
|
-
- `destroy_and_return!(object)`
|
241
|
-
- `archive_and_return!(object)` (if using Lite::Archive)
|
242
|
-
- `save_and_return!(object)`
|
243
|
-
|
244
|
-
**Setup**
|
245
|
-
|
246
|
-
```ruby
|
247
|
-
class SearchMovies < Lite::Command::Complex
|
248
|
-
include Lite::Command::Extensions::Errors
|
249
|
-
include Lite::Command::Extensions::Propagation
|
250
|
-
|
251
|
-
# ... ommited ...
|
252
|
-
|
253
|
-
def execute
|
254
|
-
create_and_return!(User, name: 'John Doe')
|
255
|
-
end
|
256
88
|
|
257
|
-
|
89
|
+
command = CalculatePower.call(a: 2, b: 3)
|
90
|
+
command.context.result #=> 8
|
258
91
|
```
|
259
92
|
|
260
|
-
|
261
|
-
|
262
|
-
|
93
|
+
## Internals
|
94
|
+
|
95
|
+
#### States
|
96
|
+
State represents the state of the executable code. Once `execute`
|
97
|
+
is ran, it will always `complete` or `dnf` if a fault is thrown by a
|
98
|
+
child command.
|
99
|
+
|
100
|
+
- `pending`
|
101
|
+
- Command objects that have been initialized.
|
102
|
+
- `executing`
|
103
|
+
- Command objects actively executing code.
|
104
|
+
- `complete`
|
105
|
+
- Command objects that executed to completion.
|
106
|
+
- `dnf`
|
107
|
+
- Command objects that could NOT be executed to completion.
|
108
|
+
This could be as a result of a fault/exception on the
|
109
|
+
object itself or one of its children.
|
110
|
+
|
111
|
+
#### Statuses
|
112
|
+
|
113
|
+
Status represents the state of the callable code. If no fault
|
114
|
+
is thrown then a status of `success` is returned even if `call`
|
115
|
+
has not been executed. The list of status include (by severity):
|
116
|
+
|
117
|
+
- `success`
|
118
|
+
- No fault or exception
|
119
|
+
- `noop`
|
120
|
+
- Noop represents skipping completion of call execution early
|
121
|
+
an unsatisfied condition or logic check where there is no
|
122
|
+
point on proceeding.
|
123
|
+
- **eg:** account is sample: skip since its a non-alterable record
|
124
|
+
- `invalid`
|
125
|
+
- Invalid represents a stoppage of call execution due to
|
126
|
+
missing, bad, or corrupt data.
|
127
|
+
- **eg:** user not found: stop since rest of the call cant be executed
|
128
|
+
- `failure`
|
129
|
+
- Failure represents a stoppage of call execution due to
|
130
|
+
an unsatisfied condition or logic check where it blocks
|
131
|
+
proceeding any further.
|
132
|
+
- **eg:** record not found: stop since there is nothing todo
|
133
|
+
- `error`
|
134
|
+
- Error represents a caught exception for a call execution
|
135
|
+
that could not complete.
|
136
|
+
- **eg:** ApiServerError: stop since there was a 3rd party issue
|
137
|
+
|
138
|
+
## Generator
|
263
139
|
|
264
|
-
|
265
|
-
|
266
|
-
```ruby
|
267
|
-
class SearchMovies < Lite::Command::Complex
|
268
|
-
include Lite::Command::Extensions::Memoize
|
269
|
-
|
270
|
-
# ... ommited ...
|
271
|
-
|
272
|
-
private
|
273
|
-
|
274
|
-
# Sets the value in the cache
|
275
|
-
# Subsequent method calls gets the cached value
|
276
|
-
# This saves you the extra external HTTP.get call
|
277
|
-
def movies_by_name
|
278
|
-
cache.memoize { HTTP.get("http://movies.com?title=#{name}") }
|
279
|
-
end
|
280
|
-
|
281
|
-
# Gets the value in the cache
|
282
|
-
def generate_fingerprint
|
283
|
-
Digest::MD5.hexdigest(movies_by_name)
|
284
|
-
end
|
140
|
+
`rails g command NAME` will generate the following file:
|
285
141
|
|
286
|
-
|
142
|
+
```erb
|
143
|
+
app/commands/[NAME]_command.rb
|
287
144
|
```
|
288
145
|
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
command = SearchMovies.call('Toy Story')
|
293
|
-
command.cache #=> Lite::Memoize::Instance object
|
294
|
-
```
|
146
|
+
If a `ApplicationCommand` file in the `app/commands` directory is available, the
|
147
|
+
generator will create file that inherit from `ApplicationCommand` if not it will
|
148
|
+
fallback to `Lite::Command::Base`.
|
295
149
|
|
296
150
|
## Development
|
297
151
|
|
data/Rakefile
CHANGED
data/bin/console
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
require
|
5
|
-
require
|
4
|
+
require "bundler/setup"
|
5
|
+
require "lite/command"
|
6
6
|
|
7
7
|
# You can add fixtures and/or initialization code here to make experimenting
|
8
8
|
# with your gem easier. You can also use a different console, if you like.
|
@@ -11,5 +11,5 @@ require 'lite/command'
|
|
11
11
|
# require "pry"
|
12
12
|
# Pry.start
|
13
13
|
|
14
|
-
require
|
14
|
+
require "irb"
|
15
15
|
IRB.start(__FILE__)
|
@@ -3,12 +3,12 @@
|
|
3
3
|
module Rails
|
4
4
|
class CommandGenerator < Rails::Generators::NamedBase
|
5
5
|
|
6
|
-
source_root File.expand_path(
|
7
|
-
check_class_collision suffix:
|
6
|
+
source_root File.expand_path("../templates", __FILE__)
|
7
|
+
check_class_collision suffix: "Command"
|
8
8
|
|
9
9
|
def copy_files
|
10
|
-
path = File.join(
|
11
|
-
template(
|
10
|
+
path = File.join("app", "commands", class_path, "#{file_name}_command.rb")
|
11
|
+
template("command.rb.tt", path)
|
12
12
|
end
|
13
13
|
|
14
14
|
private
|
@@ -18,7 +18,7 @@ module Rails
|
|
18
18
|
end
|
19
19
|
|
20
20
|
def remove_possible_suffix(name)
|
21
|
-
name.sub(/_?command$/i,
|
21
|
+
name.sub(/_?command$/i, "")
|
22
22
|
end
|
23
23
|
|
24
24
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
<% module_namespacing do -%>
|
4
|
-
class <%= class_name %>Command < <%= ApplicationCommand.to_s rescue 'Lite::Command::
|
4
|
+
class <%= class_name %>Command < <%= ApplicationCommand.to_s rescue 'Lite::Command::Base' %>
|
5
5
|
end
|
6
6
|
<% end -%>
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lite
|
4
|
+
module Command
|
5
|
+
class Base
|
6
|
+
|
7
|
+
def self.inherited(base)
|
8
|
+
super
|
9
|
+
|
10
|
+
base.include Lite::Command::Internals::Callable
|
11
|
+
base.include Lite::Command::Internals::Executable
|
12
|
+
base.include Lite::Command::Internals::Resultable
|
13
|
+
|
14
|
+
base.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
15
|
+
# eg: Users::ResetPassword::Fault
|
16
|
+
class #{base}::Fault < Lite::Command::Fault; end
|
17
|
+
RUBY
|
18
|
+
|
19
|
+
FAULTS.each do |f|
|
20
|
+
base.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
21
|
+
# eg: Users::ResetPassword::Noop < Users::ResetPassword::Fault
|
22
|
+
class #{base}::#{f.capitalize} < #{base}::Fault; end
|
23
|
+
RUBY
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
attr_reader :context
|
28
|
+
|
29
|
+
def initialize(context = {})
|
30
|
+
@context = Lite::Command::Context.build(context)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def additional_result_data
|
36
|
+
{} # Define in your class to add additional info to result hash
|
37
|
+
end
|
38
|
+
|
39
|
+
def on_before_execution
|
40
|
+
# Define in your class to run code before execution
|
41
|
+
end
|
42
|
+
|
43
|
+
def on_after_execution
|
44
|
+
# Define in your class to run code after execution
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ostruct" unless defined?(OpenStruct)
|
4
|
+
|
5
|
+
module Lite
|
6
|
+
module Command
|
7
|
+
class Context < OpenStruct
|
8
|
+
|
9
|
+
def self.init(attributes = {})
|
10
|
+
# To save memory and speed up the access to an attribute, the accessor methods
|
11
|
+
# of an attribute are lazy loaded at certain points. This means that the methods
|
12
|
+
# are defined only when a set of defined actions are triggered. This allows context
|
13
|
+
# to only define the minimum amount of required methods to make your data structure work
|
14
|
+
os = new(attributes)
|
15
|
+
os.methods(false)
|
16
|
+
os
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.build(attributes = {})
|
20
|
+
return attributes if attributes.is_a?(self) && !attributes.frozen?
|
21
|
+
|
22
|
+
init(attributes.to_h)
|
23
|
+
end
|
24
|
+
|
25
|
+
def merge!(attributes = {})
|
26
|
+
attrs = attributes.is_a?(self.class) ? attributes.to_h : attributes
|
27
|
+
attrs.each { |k, v| self[k] = v }
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lite
|
4
|
+
module Command
|
5
|
+
|
6
|
+
# Fault represent a stoppage of a call execution. This error should
|
7
|
+
# not be raised directly since it wont provide any context. Use
|
8
|
+
# `Noop`, `Invalid`, `Failure`, and `Error` to signify severity.
|
9
|
+
class Fault < StandardError
|
10
|
+
|
11
|
+
attr_reader :faulter, :thrower, :reason
|
12
|
+
|
13
|
+
def initialize(faulter, thrower, reason)
|
14
|
+
super(reason)
|
15
|
+
|
16
|
+
@faulter = faulter
|
17
|
+
@thrower = thrower
|
18
|
+
@reason = reason
|
19
|
+
end
|
20
|
+
|
21
|
+
def fault_klass
|
22
|
+
@fault_klass ||= self.class.name.split("::").last
|
23
|
+
end
|
24
|
+
|
25
|
+
def fault_name
|
26
|
+
@fault_name ||= fault_klass.downcase
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
# Noop represents skipping completion of call execution early
|
32
|
+
# an unsatisfied condition or logic check where there is no
|
33
|
+
# point on proceeding.
|
34
|
+
# eg: account is sample: skip since its a non-alterable record
|
35
|
+
class Noop < Fault; end
|
36
|
+
|
37
|
+
# Invalid represents a stoppage of call execution due to
|
38
|
+
# missing, bad, or corrupt data.
|
39
|
+
# eg: user not found: stop since rest of the call cant be executed
|
40
|
+
class Invalid < Fault; end
|
41
|
+
|
42
|
+
# Failure represents a stoppage of call execution due to
|
43
|
+
# an unsatisfied condition or logic check where it blocks
|
44
|
+
# proceeding any further.
|
45
|
+
# eg: record not found: stop since there is nothing todo
|
46
|
+
class Failure < Fault; end
|
47
|
+
|
48
|
+
# Error represents a caught exception for a call execution
|
49
|
+
# that could not complete.
|
50
|
+
# eg: ApiServerError: stop since there was a 3rd party issue
|
51
|
+
class Error < Fault; end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|