appfuel 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.codeclimate.yml +25 -0
- data/.gitignore +10 -0
- data/.rspec +2 -0
- data/.rubocop.yml +1156 -0
- data/.travis.yml +19 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +9 -0
- data/README.md +38 -0
- data/Rakefile +6 -0
- data/appfuel.gemspec +42 -0
- data/bin/console +7 -0
- data/bin/setup +8 -0
- data/lib/appfuel.rb +210 -0
- data/lib/appfuel/application.rb +4 -0
- data/lib/appfuel/application/app_container.rb +223 -0
- data/lib/appfuel/application/container_class_registration.rb +22 -0
- data/lib/appfuel/application/container_key.rb +201 -0
- data/lib/appfuel/application/qualify_container_key.rb +76 -0
- data/lib/appfuel/application/root.rb +140 -0
- data/lib/appfuel/cli_msg_request.rb +19 -0
- data/lib/appfuel/configuration.rb +14 -0
- data/lib/appfuel/configuration/definition_dsl.rb +175 -0
- data/lib/appfuel/configuration/file_loader.rb +61 -0
- data/lib/appfuel/configuration/populate.rb +95 -0
- data/lib/appfuel/configuration/search.rb +45 -0
- data/lib/appfuel/db_model.rb +16 -0
- data/lib/appfuel/domain.rb +7 -0
- data/lib/appfuel/domain/criteria.rb +436 -0
- data/lib/appfuel/domain/domain_name_parser.rb +44 -0
- data/lib/appfuel/domain/dsl.rb +247 -0
- data/lib/appfuel/domain/entity.rb +242 -0
- data/lib/appfuel/domain/entity_collection.rb +87 -0
- data/lib/appfuel/domain/expr.rb +127 -0
- data/lib/appfuel/domain/value_object.rb +7 -0
- data/lib/appfuel/errors.rb +104 -0
- data/lib/appfuel/feature.rb +2 -0
- data/lib/appfuel/feature/action_loader.rb +25 -0
- data/lib/appfuel/feature/initializer.rb +43 -0
- data/lib/appfuel/handler.rb +6 -0
- data/lib/appfuel/handler/action.rb +17 -0
- data/lib/appfuel/handler/base.rb +103 -0
- data/lib/appfuel/handler/command.rb +18 -0
- data/lib/appfuel/handler/inject_dsl.rb +88 -0
- data/lib/appfuel/handler/validator_dsl.rb +256 -0
- data/lib/appfuel/initialize.rb +70 -0
- data/lib/appfuel/initialize/initializer.rb +68 -0
- data/lib/appfuel/msg_request.rb +207 -0
- data/lib/appfuel/predicates.rb +10 -0
- data/lib/appfuel/presenter.rb +18 -0
- data/lib/appfuel/presenter/base.rb +7 -0
- data/lib/appfuel/repository.rb +73 -0
- data/lib/appfuel/repository/base.rb +72 -0
- data/lib/appfuel/repository/initializer.rb +19 -0
- data/lib/appfuel/repository/mapper.rb +203 -0
- data/lib/appfuel/repository/mapping_dsl.rb +210 -0
- data/lib/appfuel/repository/mapping_entry.rb +76 -0
- data/lib/appfuel/repository/mapping_registry.rb +121 -0
- data/lib/appfuel/repository_runner.rb +60 -0
- data/lib/appfuel/request.rb +53 -0
- data/lib/appfuel/response.rb +96 -0
- data/lib/appfuel/response_handler.rb +79 -0
- data/lib/appfuel/root_module.rb +31 -0
- data/lib/appfuel/run_error.rb +9 -0
- data/lib/appfuel/storage.rb +3 -0
- data/lib/appfuel/storage/db.rb +4 -0
- data/lib/appfuel/storage/db/active_record_model.rb +42 -0
- data/lib/appfuel/storage/db/mapper.rb +213 -0
- data/lib/appfuel/storage/db/migration_initializer.rb +42 -0
- data/lib/appfuel/storage/db/migration_runner.rb +15 -0
- data/lib/appfuel/storage/db/migration_tasks.rb +18 -0
- data/lib/appfuel/storage/db/repository.rb +231 -0
- data/lib/appfuel/storage/db/repository_query.rb +13 -0
- data/lib/appfuel/storage/file.rb +1 -0
- data/lib/appfuel/storage/file/base.rb +32 -0
- data/lib/appfuel/storage/memory.rb +2 -0
- data/lib/appfuel/storage/memory/mapper.rb +30 -0
- data/lib/appfuel/storage/memory/repository.rb +37 -0
- data/lib/appfuel/types.rb +53 -0
- data/lib/appfuel/validation.rb +80 -0
- data/lib/appfuel/validation/validator.rb +59 -0
- data/lib/appfuel/validation/validator_pipe.rb +47 -0
- data/lib/appfuel/version.rb +3 -0
- metadata +335 -0
@@ -0,0 +1,87 @@
|
|
1
|
+
module Appfuel
|
2
|
+
module Domain
|
3
|
+
# Currently this only answers the use case where a collection of active
|
4
|
+
# record models are converted into a collection of domain entities via
|
5
|
+
# a entity loader.
|
6
|
+
#
|
7
|
+
# NOTE: There is no ability yet to add or track the entity state
|
8
|
+
#
|
9
|
+
class EntityCollection
|
10
|
+
include Enumerable
|
11
|
+
attr_reader :domain_name, :domain_basename, :entity_loader
|
12
|
+
|
13
|
+
def initialize(domain_name, entity_loader = nil)
|
14
|
+
unless Types.key?(domain_name)
|
15
|
+
fail "#{domain_name} is not a registered type"
|
16
|
+
end
|
17
|
+
|
18
|
+
@pager = nil
|
19
|
+
@list = []
|
20
|
+
@loaded = false
|
21
|
+
|
22
|
+
parts = domain_name.split('.')
|
23
|
+
@domain_name = domain_name
|
24
|
+
@domain_basename = parts.last
|
25
|
+
@is_global = parts.size == 1
|
26
|
+
|
27
|
+
self.entity_loader = entity_loader if entity_loader
|
28
|
+
end
|
29
|
+
|
30
|
+
def collection?
|
31
|
+
true
|
32
|
+
end
|
33
|
+
|
34
|
+
def global?
|
35
|
+
@is_global
|
36
|
+
end
|
37
|
+
|
38
|
+
def all
|
39
|
+
load_entities
|
40
|
+
@list
|
41
|
+
end
|
42
|
+
|
43
|
+
def first
|
44
|
+
load_entities
|
45
|
+
@list.first
|
46
|
+
end
|
47
|
+
|
48
|
+
def each
|
49
|
+
load_entities
|
50
|
+
return @list.each unless block_given?
|
51
|
+
|
52
|
+
@list.each {|entity| yield entity}
|
53
|
+
end
|
54
|
+
|
55
|
+
def pager
|
56
|
+
load_entities
|
57
|
+
@pager
|
58
|
+
end
|
59
|
+
|
60
|
+
def to_a
|
61
|
+
list = []
|
62
|
+
each do |entity|
|
63
|
+
list << entity.to_h
|
64
|
+
end
|
65
|
+
list
|
66
|
+
end
|
67
|
+
|
68
|
+
def entity_loader?
|
69
|
+
!@entity_loader.nil?
|
70
|
+
end
|
71
|
+
|
72
|
+
def entity_loader=(loader)
|
73
|
+
fail "Entity loader must implement call" unless loader.respond_to?(:call)
|
74
|
+
@entity_loader = loader
|
75
|
+
end
|
76
|
+
|
77
|
+
protected
|
78
|
+
|
79
|
+
def load_entities
|
80
|
+
return false if @loaded || !entity_loader?
|
81
|
+
data = entity_loader.call
|
82
|
+
@list = data[:list]
|
83
|
+
@pager = data[:pager]
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
module Appfuel
|
2
|
+
module Domain
|
3
|
+
# Domain expressions are used mostly by the criteria to describe filter
|
4
|
+
# conditions. The class represents a basic expression like "id eq 6", the
|
5
|
+
# problem with this expression is that we need additional information in
|
6
|
+
# order to properly map it to something like a db expression. This call
|
7
|
+
# ensures that additional information exists. Most importantly we need
|
8
|
+
# a fully qualified domain name in the form of "feature.domain".
|
9
|
+
class Expr
|
10
|
+
include DomainNameParser
|
11
|
+
OPS = {
|
12
|
+
eq: '=',
|
13
|
+
gt: '>',
|
14
|
+
gteq: '>=',
|
15
|
+
lt: '<',
|
16
|
+
lteq: '<=',
|
17
|
+
in: 'IN',
|
18
|
+
like: 'LIKE',
|
19
|
+
ilike: 'ILIKE',
|
20
|
+
between: 'BETWEEN'
|
21
|
+
}
|
22
|
+
attr_reader :feature, :domain_basename, :domain_name, :domain_attr, :value
|
23
|
+
|
24
|
+
# Assign the fully qualified domain name, its basename and its attribute
|
25
|
+
# along with the operator and value. Operator and value are assumed to
|
26
|
+
# be the first key => value pair of the hash.
|
27
|
+
#
|
28
|
+
# @example
|
29
|
+
# feature domain
|
30
|
+
# Expr.new('foo.bar', 'id', eq: 6)
|
31
|
+
#
|
32
|
+
# or
|
33
|
+
# global domain
|
34
|
+
# Expr.new('bar', 'name', like: '%Bob%')
|
35
|
+
#
|
36
|
+
#
|
37
|
+
# @param domain [String] fully qualified domain name
|
38
|
+
# @param domain_attr [String, Symbol] attribute name
|
39
|
+
# @param data [Hash] holds operator and value
|
40
|
+
# @option data [Symbol] the key is the operator and value is the value
|
41
|
+
#
|
42
|
+
# @return [Expr]
|
43
|
+
def initialize(domain, domain_attr, data)
|
44
|
+
fail "operator value pair must exist in a hash" unless data.is_a?(Hash)
|
45
|
+
@feature, @domain_basename, @domain_name = parse_domain_name(domain)
|
46
|
+
|
47
|
+
operator, value = data.first
|
48
|
+
@domain_attr = domain_attr.to_s
|
49
|
+
self.op = operator
|
50
|
+
self.value = value
|
51
|
+
|
52
|
+
fail "domain name can not be empty" if @domain_name.empty?
|
53
|
+
fail "domain attribute can not be empty" if @domain_attr.empty?
|
54
|
+
end
|
55
|
+
|
56
|
+
def feature?
|
57
|
+
!@feature.nil?
|
58
|
+
end
|
59
|
+
|
60
|
+
def global?
|
61
|
+
!feature?
|
62
|
+
end
|
63
|
+
|
64
|
+
# @return [Bool]
|
65
|
+
def negated?
|
66
|
+
@negated
|
67
|
+
end
|
68
|
+
|
69
|
+
def expr_string
|
70
|
+
data = yield domain_name, domain_attr, OPS[op]
|
71
|
+
lvalue = data[0]
|
72
|
+
operator = data[1]
|
73
|
+
rvalue = data[2]
|
74
|
+
|
75
|
+
operator = "NOT #{operator}" if negated?
|
76
|
+
"#{lvalue} #{operator} #{rvalue}"
|
77
|
+
end
|
78
|
+
|
79
|
+
def to_s
|
80
|
+
"#{domain_name}.#{domain_attr} #{OPS[op]} #{value}"
|
81
|
+
end
|
82
|
+
|
83
|
+
def op
|
84
|
+
negated? ? "not_#{@op}".to_sym : @op
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def op=(value)
|
90
|
+
negated, value = value.to_s.split('_')
|
91
|
+
@negated = false
|
92
|
+
if negated == 'not'
|
93
|
+
@negated = true
|
94
|
+
else
|
95
|
+
value = negated
|
96
|
+
end
|
97
|
+
value = value.to_sym
|
98
|
+
unless supported_op?(value)
|
99
|
+
fail "op has to be one of [#{OPS.keys.join(',')}]"
|
100
|
+
end
|
101
|
+
@op = value
|
102
|
+
end
|
103
|
+
|
104
|
+
def value=(data)
|
105
|
+
case op
|
106
|
+
when :in
|
107
|
+
unless data.is_a?(Array)
|
108
|
+
fail ":in operator must have an array as a value"
|
109
|
+
end
|
110
|
+
when :range
|
111
|
+
unless data.is_a?(Range)
|
112
|
+
fail ":range operator must have a range as a value"
|
113
|
+
end
|
114
|
+
when :gt, :gteq, :lt, :lteq
|
115
|
+
unless data.is_a?(Numeric)
|
116
|
+
fail ":gt, :gteq, :lt, :lteq operators expect a numeric value"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
@value = data
|
120
|
+
end
|
121
|
+
|
122
|
+
def supported_op?(op)
|
123
|
+
OPS.keys.include?(op)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
module Appfuel
|
2
|
+
# Feature handler, action handler, command handler all use this class.
|
3
|
+
# Presenters and validators will have there errors tranformed into this.
|
4
|
+
# Errors are a basic hash structure where each key has an array of strings
|
5
|
+
# that represent error messages.
|
6
|
+
#
|
7
|
+
# Example
|
8
|
+
# messages: {
|
9
|
+
# name: [
|
10
|
+
# 'must be present',
|
11
|
+
# 'can not be blank',
|
12
|
+
# 'can not be Bob'
|
13
|
+
# ]
|
14
|
+
# }
|
15
|
+
class Errors
|
16
|
+
include Enumerable
|
17
|
+
attr_reader :messages
|
18
|
+
|
19
|
+
def initialize(messages = {})
|
20
|
+
@messages = messages || {}
|
21
|
+
@messages.stringify_keys! unless @messages.empty?
|
22
|
+
end
|
23
|
+
|
24
|
+
# Defined to use Enumerable so that we can treat errors
|
25
|
+
# as an iterator
|
26
|
+
def each
|
27
|
+
messages.each do|key, msgs|
|
28
|
+
yield key, msgs
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Add an error message to a given key
|
33
|
+
#
|
34
|
+
# @param key Symbol key for this message
|
35
|
+
# @param msg String the message to be stored
|
36
|
+
def add(key, msg)
|
37
|
+
key = key.to_s
|
38
|
+
msg = msg.to_s
|
39
|
+
messages[key] = [] unless messages.key?(key)
|
40
|
+
messages[key] << msg unless messages[key].include?(msg)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Formats the list of messages for each key
|
44
|
+
#
|
45
|
+
# Example
|
46
|
+
# messages: {
|
47
|
+
# name: [
|
48
|
+
# ' must be present ',
|
49
|
+
# ' can not be blank ',
|
50
|
+
# ' can not be Bob '
|
51
|
+
# ]
|
52
|
+
# }
|
53
|
+
#
|
54
|
+
# note: spaces are used only for readability
|
55
|
+
# name: must be present \n can not be blank \n can not be Bob \n \n
|
56
|
+
#
|
57
|
+
# @param msg_separator String separates each message default \n
|
58
|
+
# @param list_separator String separates each list of messages
|
59
|
+
# @return String
|
60
|
+
def format(msg_separator = "\n", list_separator = "\n")
|
61
|
+
msg = ''
|
62
|
+
each do |key, list|
|
63
|
+
msg << "#{key}: #{list.join(msg_separator)}#{list_separator}"
|
64
|
+
end
|
65
|
+
msg
|
66
|
+
end
|
67
|
+
|
68
|
+
def delete(key)
|
69
|
+
messages.delete(key.to_s)
|
70
|
+
end
|
71
|
+
|
72
|
+
def [](key)
|
73
|
+
messages[key.to_s]
|
74
|
+
end
|
75
|
+
|
76
|
+
def size
|
77
|
+
messages.length
|
78
|
+
end
|
79
|
+
|
80
|
+
def values
|
81
|
+
messages.values
|
82
|
+
end
|
83
|
+
|
84
|
+
def keys
|
85
|
+
messages.keys
|
86
|
+
end
|
87
|
+
|
88
|
+
def clear
|
89
|
+
messages.clear
|
90
|
+
end
|
91
|
+
|
92
|
+
def empty?
|
93
|
+
messages.empty?
|
94
|
+
end
|
95
|
+
|
96
|
+
def to_h
|
97
|
+
{errors: messages}
|
98
|
+
end
|
99
|
+
|
100
|
+
def to_s
|
101
|
+
format
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Appfuel
|
2
|
+
module Feature
|
3
|
+
# Loads an action from the container using its fully qualified namespace.
|
4
|
+
# This class has been abstracted out because its Appfuel's implementation
|
5
|
+
# of loading an action. This action loader is injected into the container
|
6
|
+
# during setup which allows the client to use their own if this basic
|
7
|
+
# lookup mehtod does not work for them.
|
8
|
+
#
|
9
|
+
# The idea is that all actions, commands and repositories auto register
|
10
|
+
# themselves into the container based on a namespace derived inpart by
|
11
|
+
# their own ruby namespace.
|
12
|
+
class ActionLoader
|
13
|
+
# @raises RuntimeError when key is not found
|
14
|
+
# @param namespace [String] fully qualifed container namespace
|
15
|
+
# @param container [Dry::Container] application container
|
16
|
+
# @return [Appfuel::Handler::Action]
|
17
|
+
def call(namespace, container)
|
18
|
+
unless container.key?(namespace)
|
19
|
+
fail "[ActionLoader] Could not load action at #{namespace}"
|
20
|
+
end
|
21
|
+
container[namespace]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Appfuel
|
2
|
+
module Feature
|
3
|
+
# Run a given feature's initializers. Each feature can declare any number
|
4
|
+
# of initializers just as the application does. This allow dependencies
|
5
|
+
# and vendor code to be initialized only when the feature is used.
|
6
|
+
class Initializer
|
7
|
+
# Ensure the correct namespaces are registered so that the initializer
|
8
|
+
# dsl will work and then require the feature and run its intializers
|
9
|
+
# unless instructed not too. Initializers are only run once.
|
10
|
+
#
|
11
|
+
# @param name [String] name of the feature as found in the container
|
12
|
+
# @param container [Dry::Container] application container
|
13
|
+
# @return [Boolean]
|
14
|
+
def call(name, container)
|
15
|
+
name = name.to_s.underscore
|
16
|
+
feature_key = "features.#{name}"
|
17
|
+
unless container.key?(feature_key)
|
18
|
+
Appfuel.setup_container_dependencies(feature_key, container)
|
19
|
+
end
|
20
|
+
|
21
|
+
unless require_feature_disabled?(container, feature_key)
|
22
|
+
require "#{container[:features_path]}/#{name}"
|
23
|
+
end
|
24
|
+
|
25
|
+
return false if initialized?(container, feature_key)
|
26
|
+
|
27
|
+
Appfuel.run_initializers(feature_key, container)
|
28
|
+
true
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
def require_feature_disabled?(container, feature_key)
|
33
|
+
disable_key = "#{feature_key}.disable_require"
|
34
|
+
container.key?(disable_key) && container[disable_key] == true
|
35
|
+
end
|
36
|
+
|
37
|
+
def initialized?(container, feature_key)
|
38
|
+
init_key = "#{feature_key}.initialized"
|
39
|
+
container.key?(init_key) && container[init_key] == true
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Appfuel
|
2
|
+
module Handler
|
3
|
+
class Action < Base
|
4
|
+
class << self
|
5
|
+
|
6
|
+
# In order to reduce the length of namespaces actions are not required
|
7
|
+
# to be inside an Actions namespace, but, it is namespaced with in the
|
8
|
+
# application container, so we adjust for that here.
|
9
|
+
#
|
10
|
+
# @return [String]
|
11
|
+
def container_relative_key
|
12
|
+
"actions.#{super}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module Appfuel
|
2
|
+
module Handler
|
3
|
+
class Base
|
4
|
+
extend ValidatorDsl
|
5
|
+
extend InjectDsl
|
6
|
+
include Appfuel::Application::AppContainer
|
7
|
+
|
8
|
+
# Class level interfaces used by the framwork to register and run
|
9
|
+
# handlers
|
10
|
+
class << self
|
11
|
+
|
12
|
+
# Register the extending class with the application container
|
13
|
+
#
|
14
|
+
# @param klass [Class] the handler class that is inheriting this
|
15
|
+
# @return nil
|
16
|
+
def inherited(klass)
|
17
|
+
register_container_class(klass)
|
18
|
+
end
|
19
|
+
|
20
|
+
def response_handler
|
21
|
+
@response_handler ||= ResponseHandler.new
|
22
|
+
end
|
23
|
+
|
24
|
+
# Run will validate all inputs; returning on input failures, resolving
|
25
|
+
# declared dependencies, then delegate to the handlers call method with
|
26
|
+
# its valid inputs and resolved dependencies. Finally it ensure every
|
27
|
+
# response is a Response object.
|
28
|
+
#
|
29
|
+
# @param inputs [Hash] inputs to be validated
|
30
|
+
# @return [Response]
|
31
|
+
def run(inputs = {}, container = Dry::Container.new)
|
32
|
+
begin
|
33
|
+
response = resolve_inputs(inputs)
|
34
|
+
return response if response.failure?
|
35
|
+
valid_inputs = response.ok
|
36
|
+
|
37
|
+
resolve_dependencies(container)
|
38
|
+
handler = self.new(container)
|
39
|
+
result = handler.call(valid_inputs)
|
40
|
+
result = create_response(result) unless response?(result)
|
41
|
+
rescue RunError => e
|
42
|
+
result = e.response
|
43
|
+
rescue StandardError => e
|
44
|
+
result = error(e)
|
45
|
+
end
|
46
|
+
|
47
|
+
result
|
48
|
+
end
|
49
|
+
|
50
|
+
def error(*args)
|
51
|
+
response_handler.error(*args)
|
52
|
+
end
|
53
|
+
|
54
|
+
def ok(value = nil)
|
55
|
+
response_handler.ok(value)
|
56
|
+
end
|
57
|
+
|
58
|
+
def response?(value)
|
59
|
+
response_handler.response?(value)
|
60
|
+
end
|
61
|
+
|
62
|
+
def create_response(data)
|
63
|
+
response_handler.create_response(data)
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
attr_reader :data
|
69
|
+
|
70
|
+
def initialize(container = Dry::Container.new)
|
71
|
+
@data = container
|
72
|
+
end
|
73
|
+
|
74
|
+
def call(inputs, data = {})
|
75
|
+
fail "Concrete handlers must implement their own call"
|
76
|
+
end
|
77
|
+
|
78
|
+
def ok(value = nil)
|
79
|
+
self.class.ok(value)
|
80
|
+
end
|
81
|
+
|
82
|
+
def error(*args)
|
83
|
+
self.class.error(*args)
|
84
|
+
end
|
85
|
+
|
86
|
+
def present(name, data, inputs = {})
|
87
|
+
return data if inputs[:raw] == true
|
88
|
+
|
89
|
+
key = qualify_container_key(name, 'presenters')
|
90
|
+
container = self.class.app_container
|
91
|
+
unless container.key?(key)
|
92
|
+
unless data.respond_to?(:to_h)
|
93
|
+
fail "data must implement :to_h for generic presentation"
|
94
|
+
end
|
95
|
+
|
96
|
+
return data.to_h
|
97
|
+
end
|
98
|
+
|
99
|
+
container[key].call(data, inputs)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|