qonfig 0.0.0 → 0.12.0
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/.gitignore +6 -2
- data/.jrubyrc +1 -0
- data/.rspec +1 -1
- data/.rubocop.yml +15 -0
- data/.travis.yml +43 -4
- data/CHANGELOG.md +121 -0
- data/Gemfile +4 -2
- data/LICENSE.txt +1 -1
- data/README.md +1060 -19
- data/Rakefile +18 -4
- data/bin/console +5 -11
- data/bin/rspec +55 -0
- data/bin/setup +1 -0
- data/gemfiles/with_external_deps.gemfile +8 -0
- data/gemfiles/without_external_deps.gemfile +5 -0
- data/lib/qonfig.rb +22 -2
- data/lib/qonfig/command_set.rb +67 -0
- data/lib/qonfig/commands.rb +15 -0
- data/lib/qonfig/commands/add_nested_option.rb +45 -0
- data/lib/qonfig/commands/add_option.rb +41 -0
- data/lib/qonfig/commands/base.rb +12 -0
- data/lib/qonfig/commands/compose.rb +37 -0
- data/lib/qonfig/commands/expose_yaml.rb +159 -0
- data/lib/qonfig/commands/load_from_env.rb +95 -0
- data/lib/qonfig/commands/load_from_env/value_converter.rb +84 -0
- data/lib/qonfig/commands/load_from_json.rb +56 -0
- data/lib/qonfig/commands/load_from_self.rb +73 -0
- data/lib/qonfig/commands/load_from_yaml.rb +58 -0
- data/lib/qonfig/configurable.rb +116 -0
- data/lib/qonfig/data_set.rb +213 -0
- data/lib/qonfig/data_set/class_builder.rb +27 -0
- data/lib/qonfig/data_set/validator.rb +7 -0
- data/lib/qonfig/dsl.rb +122 -0
- data/lib/qonfig/errors.rb +111 -0
- data/lib/qonfig/loaders.rb +9 -0
- data/lib/qonfig/loaders/basic.rb +38 -0
- data/lib/qonfig/loaders/json.rb +24 -0
- data/lib/qonfig/loaders/yaml.rb +24 -0
- data/lib/qonfig/plugins.rb +65 -0
- data/lib/qonfig/plugins/abstract.rb +13 -0
- data/lib/qonfig/plugins/access_mixin.rb +38 -0
- data/lib/qonfig/plugins/registry.rb +125 -0
- data/lib/qonfig/plugins/toml.rb +26 -0
- data/lib/qonfig/plugins/toml/commands/expose_toml.rb +146 -0
- data/lib/qonfig/plugins/toml/commands/load_from_toml.rb +49 -0
- data/lib/qonfig/plugins/toml/data_set.rb +19 -0
- data/lib/qonfig/plugins/toml/dsl.rb +27 -0
- data/lib/qonfig/plugins/toml/loaders/toml.rb +24 -0
- data/lib/qonfig/plugins/toml/tomlrb_fixes.rb +92 -0
- data/lib/qonfig/plugins/toml/uploaders/toml.rb +25 -0
- data/lib/qonfig/settings.rb +457 -0
- data/lib/qonfig/settings/builder.rb +18 -0
- data/lib/qonfig/settings/key_guard.rb +71 -0
- data/lib/qonfig/settings/lock.rb +60 -0
- data/lib/qonfig/uploaders.rb +10 -0
- data/lib/qonfig/uploaders/base.rb +18 -0
- data/lib/qonfig/uploaders/file.rb +55 -0
- data/lib/qonfig/uploaders/json.rb +35 -0
- data/lib/qonfig/uploaders/yaml.rb +93 -0
- data/lib/qonfig/version.rb +7 -1
- data/qonfig.gemspec +29 -17
- metadata +122 -16
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# @api private
|
4
|
+
# @since 0.2.0
|
5
|
+
class Qonfig::Commands::LoadFromENV < Qonfig::Commands::Base
|
6
|
+
require_relative 'load_from_env/value_converter'
|
7
|
+
|
8
|
+
# @return [Boolean]
|
9
|
+
#
|
10
|
+
# @api private
|
11
|
+
# @since 0.2.0
|
12
|
+
attr_reader :convert_values
|
13
|
+
|
14
|
+
# @return [Regexp]
|
15
|
+
#
|
16
|
+
# @api private
|
17
|
+
# @since 0.2.0
|
18
|
+
attr_reader :prefix_pattern
|
19
|
+
|
20
|
+
# @return [Boolean]
|
21
|
+
#
|
22
|
+
# @api private
|
23
|
+
# @since 0.2.0
|
24
|
+
attr_reader :trim_prefix
|
25
|
+
|
26
|
+
# @return [Regexp]
|
27
|
+
#
|
28
|
+
# @api private
|
29
|
+
# @since 0.2.0
|
30
|
+
attr_reader :trim_pattern
|
31
|
+
|
32
|
+
# @option convert_values [Boolean]
|
33
|
+
# @opion prefix [NilClass, String, Regexp]
|
34
|
+
#
|
35
|
+
# @raise [Qonfig::ArgumentError]
|
36
|
+
#
|
37
|
+
# @api private
|
38
|
+
# @since 0.2.0
|
39
|
+
def initialize(convert_values: false, prefix: nil, trim_prefix: false)
|
40
|
+
unless convert_values.is_a?(FalseClass) || convert_values.is_a?(TrueClass)
|
41
|
+
raise Qonfig::ArgumentError, ':convert_values option should be a boolean'
|
42
|
+
end
|
43
|
+
|
44
|
+
unless prefix.is_a?(NilClass) || prefix.is_a?(String) || prefix.is_a?(Regexp)
|
45
|
+
raise Qonfig::ArgumentError, ':prefix option should be a nil / string / regexp'
|
46
|
+
end
|
47
|
+
|
48
|
+
unless trim_prefix.is_a?(FalseClass) || trim_prefix.is_a?(TrueClass)
|
49
|
+
raise Qonfig::ArgumentError, ':trim_refix options should be a boolean'
|
50
|
+
end
|
51
|
+
|
52
|
+
@convert_values = convert_values
|
53
|
+
@prefix_pattern = prefix.is_a?(Regexp) ? prefix : /\A#{Regexp.escape(prefix.to_s)}.*\z/m
|
54
|
+
@trim_prefix = trim_prefix
|
55
|
+
@trim_pattern = prefix.is_a?(Regexp) ? prefix : /\A(#{Regexp.escape(prefix.to_s)})/m
|
56
|
+
end
|
57
|
+
|
58
|
+
# @param settings [Qonfig::Settings]
|
59
|
+
# @return [void]
|
60
|
+
#
|
61
|
+
# @api private
|
62
|
+
# @since 0.2.0
|
63
|
+
def call(settings)
|
64
|
+
env_data = extract_env_data
|
65
|
+
|
66
|
+
env_based_settings = build_data_set_class(env_data).new.settings
|
67
|
+
|
68
|
+
settings.__append_settings__(env_based_settings)
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
# @return [Hash]
|
74
|
+
#
|
75
|
+
# @api private
|
76
|
+
# @since 0.2.0
|
77
|
+
def extract_env_data
|
78
|
+
ENV.each_with_object({}) do |(key, value), env_data|
|
79
|
+
next unless key.match(prefix_pattern)
|
80
|
+
key = key.sub(trim_pattern, '') if trim_prefix
|
81
|
+
env_data[key] = value
|
82
|
+
end.tap do |env_data|
|
83
|
+
ValueConverter.convert_values!(env_data) if convert_values
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# @param env_data [Hash]
|
88
|
+
# @return [Class<Qonfig::DataSet>]
|
89
|
+
#
|
90
|
+
# @api private
|
91
|
+
# @since 0.2.0
|
92
|
+
def build_data_set_class(env_data)
|
93
|
+
Qonfig::DataSet::ClassBuilder.build_from_hash(env_data)
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Qonfig
|
4
|
+
# @api private
|
5
|
+
# @since 0.2.0
|
6
|
+
module Commands::LoadFromENV::ValueConverter
|
7
|
+
# @return [Regexp]
|
8
|
+
#
|
9
|
+
# @api private
|
10
|
+
# @since 0.2.0
|
11
|
+
INTEGER_PATTERN = /\A\d+\z/.freeze
|
12
|
+
|
13
|
+
# @return [Regexp]
|
14
|
+
#
|
15
|
+
# @api private
|
16
|
+
# @since 0.2.0
|
17
|
+
FLOAT_PATTERN = /\A\d+\.\d+\z/.freeze
|
18
|
+
|
19
|
+
# @return [Regexp]
|
20
|
+
#
|
21
|
+
# @api private
|
22
|
+
# @since 0.2.0
|
23
|
+
TRUE_PATTERN = /\A(t|true)\z/i.freeze
|
24
|
+
|
25
|
+
# @return [Regexp]
|
26
|
+
#
|
27
|
+
# @api private
|
28
|
+
# @since 0.2.0
|
29
|
+
FALSE_PATTERN = /\A(f|false)\z/i.freeze
|
30
|
+
|
31
|
+
# @return [Regexp]
|
32
|
+
#
|
33
|
+
# @api private
|
34
|
+
# @since 0.2.0
|
35
|
+
ARRAY_PATTERN = /\A[^'"].*\s*,\s*.*[^'"]\z/.freeze
|
36
|
+
|
37
|
+
# @return [Regexp]
|
38
|
+
#
|
39
|
+
# @api private
|
40
|
+
# @since 0.2.0
|
41
|
+
QUOTED_STRING_PATTERN = /\A['"].*['"]\z/.freeze
|
42
|
+
|
43
|
+
class << self
|
44
|
+
# @param env_data [Hash]
|
45
|
+
# @return [void]
|
46
|
+
#
|
47
|
+
# @api private
|
48
|
+
# @since 0.2.0
|
49
|
+
def convert_values!(env_data)
|
50
|
+
env_data.each_pair do |key, value|
|
51
|
+
env_data[key] = convert_value(value)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
# @param value [Object]
|
58
|
+
# @return [Object]
|
59
|
+
#
|
60
|
+
# @api private
|
61
|
+
# @since 0.2.0
|
62
|
+
def convert_value(value)
|
63
|
+
return value unless value.is_a?(String)
|
64
|
+
|
65
|
+
case value
|
66
|
+
when INTEGER_PATTERN
|
67
|
+
Integer(value)
|
68
|
+
when FLOAT_PATTERN
|
69
|
+
Float(value)
|
70
|
+
when TRUE_PATTERN
|
71
|
+
true
|
72
|
+
when FALSE_PATTERN
|
73
|
+
false
|
74
|
+
when ARRAY_PATTERN
|
75
|
+
value.split(/\s*,\s*/).map(&method(:convert_value))
|
76
|
+
when QUOTED_STRING_PATTERN
|
77
|
+
value.gsub(/(\A['"]|['"]\z)/, '')
|
78
|
+
else
|
79
|
+
value
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# @api private
|
4
|
+
# @since 0.5.0
|
5
|
+
class Qonfig::Commands::LoadFromJSON < Qonfig::Commands::Base
|
6
|
+
# @return [String]
|
7
|
+
#
|
8
|
+
# @api private
|
9
|
+
# @since 0.5.0
|
10
|
+
attr_reader :file_path
|
11
|
+
|
12
|
+
# @return [Boolean]
|
13
|
+
#
|
14
|
+
# @api private
|
15
|
+
# @sicne 0.5.0
|
16
|
+
attr_reader :strict
|
17
|
+
|
18
|
+
# @param file_path [String]
|
19
|
+
# @option strict [Boolean]
|
20
|
+
#
|
21
|
+
# @api private
|
22
|
+
# @since 0.5.0
|
23
|
+
def initialize(file_path, strict: true)
|
24
|
+
@file_path = file_path
|
25
|
+
@strict = strict
|
26
|
+
end
|
27
|
+
|
28
|
+
# @param settings [Qonfig::Settings]
|
29
|
+
# @return [void]
|
30
|
+
#
|
31
|
+
# @api private
|
32
|
+
# @since 0.5.0
|
33
|
+
def call(settings)
|
34
|
+
json_data = Qonfig::Loaders::JSON.load_file(file_path, fail_on_unexist: strict)
|
35
|
+
|
36
|
+
raise(
|
37
|
+
Qonfig::IncompatibleJSONStructureError,
|
38
|
+
'JSON object should have a hash-like structure'
|
39
|
+
) unless json_data.is_a?(Hash)
|
40
|
+
|
41
|
+
json_based_settings = build_data_set_class(json_data).new.settings
|
42
|
+
|
43
|
+
settings.__append_settings__(json_based_settings)
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
# @param json_data [Hash]
|
49
|
+
# @return [Class<Qonfig::DataSet>]
|
50
|
+
#
|
51
|
+
# @api private
|
52
|
+
# @since 0.5.0
|
53
|
+
def build_data_set_class(json_data)
|
54
|
+
Qonfig::DataSet::ClassBuilder.build_from_hash(json_data)
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# @api private
|
4
|
+
# @since 0.2.0
|
5
|
+
class Qonfig::Commands::LoadFromSelf < Qonfig::Commands::Base
|
6
|
+
# @return [String]
|
7
|
+
#
|
8
|
+
# @api private
|
9
|
+
# @since 0.2.0
|
10
|
+
attr_reader :caller_location
|
11
|
+
|
12
|
+
# @param caller_location [String]
|
13
|
+
#
|
14
|
+
# @api private
|
15
|
+
# @since 0.2.0
|
16
|
+
def initialize(caller_location)
|
17
|
+
@caller_location = caller_location
|
18
|
+
end
|
19
|
+
|
20
|
+
# @param settings [Qonfig::Settings]
|
21
|
+
# @return [void]
|
22
|
+
#
|
23
|
+
# @api private
|
24
|
+
# @since 0.2.0
|
25
|
+
def call(settings)
|
26
|
+
yaml_data = load_self_placed_yaml_data
|
27
|
+
|
28
|
+
yaml_based_settings = build_data_set_klass(yaml_data).new.settings
|
29
|
+
|
30
|
+
settings.__append_settings__(yaml_based_settings)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
# @return [Hash]
|
36
|
+
#
|
37
|
+
# @raise [Qonfig::SelfDataNotFound]
|
38
|
+
# @raise [Qonfig::IncompatibleYAMLStructureError]
|
39
|
+
#
|
40
|
+
# @api private
|
41
|
+
# @since 0.2.0
|
42
|
+
def load_self_placed_yaml_data
|
43
|
+
caller_file = caller_location.split(':').first
|
44
|
+
|
45
|
+
raise(
|
46
|
+
Qonfig::SelfDataNotFoundError,
|
47
|
+
"Caller file does not exist! (location: #{caller_location})"
|
48
|
+
) unless File.exist?(caller_file)
|
49
|
+
|
50
|
+
data_match = IO.read(caller_file).match(/\n__END__\n(?<end_data>.*)/m)
|
51
|
+
raise Qonfig::SelfDataNotFoundError, '__END__ data not found!' unless data_match
|
52
|
+
|
53
|
+
end_data = data_match[:end_data]
|
54
|
+
raise Qonfig::SelfDataNotFoundError, '__END__ data not found!' unless end_data
|
55
|
+
|
56
|
+
yaml_data = Qonfig::Loaders::YAML.load(end_data)
|
57
|
+
raise(
|
58
|
+
Qonfig::IncompatibleYAMLStructureError,
|
59
|
+
'YAML content should have a hash-like structure'
|
60
|
+
) unless yaml_data.is_a?(Hash)
|
61
|
+
|
62
|
+
yaml_data
|
63
|
+
end
|
64
|
+
|
65
|
+
# @param self_placed_yaml_data [Hash]
|
66
|
+
# @return [Class<Qonfig::DataSet>]
|
67
|
+
#
|
68
|
+
# @api private
|
69
|
+
# @since 0.2.0
|
70
|
+
def build_data_set_klass(self_placed_yaml_data)
|
71
|
+
Qonfig::DataSet::ClassBuilder.build_from_hash(self_placed_yaml_data)
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# @api private
|
4
|
+
# @since 0.2.0
|
5
|
+
class Qonfig::Commands::LoadFromYAML < Qonfig::Commands::Base
|
6
|
+
# @return [String]
|
7
|
+
#
|
8
|
+
# @api private
|
9
|
+
# @since 0.2.0
|
10
|
+
attr_reader :file_path
|
11
|
+
|
12
|
+
# @return [Boolean]
|
13
|
+
#
|
14
|
+
# @api private
|
15
|
+
# @since 0.2.0
|
16
|
+
attr_reader :strict
|
17
|
+
|
18
|
+
# @param file_path [String]
|
19
|
+
# @option strict [Boolean]
|
20
|
+
#
|
21
|
+
# @api private
|
22
|
+
# @since 0.2.0
|
23
|
+
def initialize(file_path, strict: true)
|
24
|
+
@file_path = file_path
|
25
|
+
@strict = strict
|
26
|
+
end
|
27
|
+
|
28
|
+
# @param settings [Qonfig::Settings]
|
29
|
+
# @return [void]
|
30
|
+
#
|
31
|
+
# @raise [Qonfig::IncompatibleYAMLStructureError]
|
32
|
+
#
|
33
|
+
# @api private
|
34
|
+
# @since 0.2.0
|
35
|
+
def call(settings)
|
36
|
+
yaml_data = Qonfig::Loaders::YAML.load_file(file_path, fail_on_unexist: strict)
|
37
|
+
|
38
|
+
raise(
|
39
|
+
Qonfig::IncompatibleYAMLStructureError,
|
40
|
+
'YAML content should have a hash-like structure'
|
41
|
+
) unless yaml_data.is_a?(Hash)
|
42
|
+
|
43
|
+
yaml_based_settings = build_data_set_class(yaml_data).new.settings
|
44
|
+
|
45
|
+
settings.__append_settings__(yaml_based_settings)
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
# @param yaml_data [Hash]
|
51
|
+
# @return [Class<Qonfig::DataSet>]
|
52
|
+
#
|
53
|
+
# @api private
|
54
|
+
# @since 0.2.0
|
55
|
+
def build_data_set_class(yaml_data)
|
56
|
+
Qonfig::DataSet::ClassBuilder.build_from_hash(yaml_data)
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# @api public
|
4
|
+
# @since 0.2.0
|
5
|
+
module Qonfig::Configurable
|
6
|
+
class << self
|
7
|
+
# @param base_klass [Class]
|
8
|
+
# @return [void]
|
9
|
+
#
|
10
|
+
# @api private
|
11
|
+
# @since 0.2.0
|
12
|
+
def included(base_klass)
|
13
|
+
base_klass.instance_variable_set(:@__qonfig_access_lock__, Mutex.new)
|
14
|
+
base_klass.instance_variable_set(:@__qonfig_definition_lock__, Mutex.new)
|
15
|
+
base_klass.instance_variable_set(:@__qonfig_config_klass__, Class.new(Qonfig::DataSet))
|
16
|
+
base_klass.instance_variable_set(:@__qonfig_config__, nil)
|
17
|
+
|
18
|
+
base_klass.extend(ClassMethods)
|
19
|
+
base_klass.include(InstanceMethods)
|
20
|
+
base_klass.singleton_class.prepend(ClassInheritance)
|
21
|
+
|
22
|
+
super
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# @api private
|
27
|
+
# @since 0.2.0
|
28
|
+
module ClassInheritance
|
29
|
+
# @param child_klass [Class]
|
30
|
+
# @return [void]
|
31
|
+
#
|
32
|
+
# @api private
|
33
|
+
# @since 0.2.0
|
34
|
+
def inherited(child_klass)
|
35
|
+
inherited_config_klass = Class.new(@__qonfig_config_klass__)
|
36
|
+
|
37
|
+
child_klass.instance_variable_set(:@__qonfig_definition_lock__, Mutex.new)
|
38
|
+
child_klass.instance_variable_set(:@__qonfig_access_lock__, Mutex.new)
|
39
|
+
child_klass.instance_variable_set(:@__qonfig_config_klass__, inherited_config_klass)
|
40
|
+
child_klass.instance_variable_set(:@__qonfig_config__, nil)
|
41
|
+
|
42
|
+
super
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# @api private
|
47
|
+
# @since 0.2.0
|
48
|
+
module ClassMethods
|
49
|
+
# @param block [Proc]
|
50
|
+
# @return [void]
|
51
|
+
#
|
52
|
+
# @api public
|
53
|
+
# @since 0.2.0
|
54
|
+
def configuration(&block)
|
55
|
+
@__qonfig_definition_lock__.synchronize do
|
56
|
+
@__qonfig_config_klass__.instance_eval(&block) if block_given?
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# @param options_map [Hash]
|
61
|
+
# @param block [Proc]
|
62
|
+
# @return [void]
|
63
|
+
#
|
64
|
+
# @api public
|
65
|
+
# @since 0.2.0
|
66
|
+
def configure(options_map = {}, &block)
|
67
|
+
@__qonfig_access_lock__.synchronize do
|
68
|
+
config.configure(options_map, &block)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# @return [Qonfig::DataSet]
|
73
|
+
#
|
74
|
+
# @api public
|
75
|
+
# @since 0.2.0
|
76
|
+
def config
|
77
|
+
@__qonfig_definition_lock__.synchronize do
|
78
|
+
@__qonfig_config__ ||= @__qonfig_config_klass__.new
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# @api private
|
84
|
+
# @since 0.2.0
|
85
|
+
module InstanceMethods
|
86
|
+
# @return [Qonfig::DataSet]
|
87
|
+
#
|
88
|
+
# @api public
|
89
|
+
# @since 0.2.0
|
90
|
+
def config
|
91
|
+
self.class.instance_variable_get(:@__qonfig_definition_lock__).synchronize do
|
92
|
+
@__qonfig_config__ ||= self.class.instance_variable_get(:@__qonfig_config_klass__).new
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# @return [Qonfig::DataSet]
|
97
|
+
#
|
98
|
+
# @api public
|
99
|
+
# @since 0.6.0
|
100
|
+
def shared_config
|
101
|
+
self.class.config
|
102
|
+
end
|
103
|
+
|
104
|
+
# @param options_map [Hash]
|
105
|
+
# @param block [Proc]
|
106
|
+
# @return [void]
|
107
|
+
#
|
108
|
+
# @api public
|
109
|
+
# @since 0.2.0
|
110
|
+
def configure(options_map = {}, &block)
|
111
|
+
self.class.instance_variable_get(:@__qonfig_access_lock__).synchronize do
|
112
|
+
config.configure(options_map, &block)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|