lite-command 1.5.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +23 -4
- data/CHANGELOG.md +4 -0
- data/Gemfile +1 -1
- data/Gemfile.lock +127 -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 +28 -29
- metadata +13 -42
- 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
|
[![Build Status](https://travis-ci.org/drexed/lite-command.svg?branch=master)](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
|