dry-validation 0.11.2 → 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/.travis.yml +1 -1
- data/CHANGELOG.md +10 -0
- data/Gemfile +7 -5
- data/README.md +4 -2
- data/Rakefile +5 -3
- data/benchmarks/benchmark_form_invalid.rb +2 -2
- data/benchmarks/benchmark_form_valid.rb +2 -2
- data/dry-validation.gemspec +2 -2
- data/examples/{form.rb → params.rb} +1 -1
- data/lib/dry/validation.rb +3 -3
- data/lib/dry/validation/{input_processor_compiler → compat}/form.rb +20 -2
- data/lib/dry/validation/extensions/monads.rb +6 -5
- data/lib/dry/validation/input_processor_compiler.rb +2 -1
- data/lib/dry/validation/input_processor_compiler/params.rb +49 -0
- data/lib/dry/validation/message_compiler.rb +4 -0
- data/lib/dry/validation/messages/abstract.rb +1 -1
- data/lib/dry/validation/predicate_registry.rb +12 -11
- data/lib/dry/validation/schema/class_interface.rb +7 -4
- data/lib/dry/validation/schema/form.rb +3 -15
- data/lib/dry/validation/schema/params.rb +22 -0
- data/lib/dry/validation/version.rb +1 -1
- data/spec/extensions/monads/result_spec.rb +15 -13
- data/spec/integration/messages/i18n_spec.rb +0 -2
- data/spec/integration/{form → params}/predicates/array_spec.rb +5 -5
- data/spec/integration/{form → params}/predicates/empty_spec.rb +8 -8
- data/spec/integration/{form → params}/predicates/eql_spec.rb +8 -8
- data/spec/integration/{form → params}/predicates/even_spec.rb +8 -8
- data/spec/integration/{form → params}/predicates/excluded_from_spec.rb +8 -8
- data/spec/integration/{form → params}/predicates/excludes_spec.rb +8 -8
- data/spec/integration/{form → params}/predicates/false_spec.rb +8 -8
- data/spec/integration/{form → params}/predicates/filled_spec.rb +10 -10
- data/spec/integration/{form → params}/predicates/format_spec.rb +8 -8
- data/spec/integration/{form → params}/predicates/gt_spec.rb +8 -8
- data/spec/integration/{form → params}/predicates/gteq_spec.rb +8 -8
- data/spec/integration/{form → params}/predicates/included_in_spec.rb +8 -8
- data/spec/integration/{form → params}/predicates/includes_spec.rb +8 -8
- data/spec/integration/{form → params}/predicates/key_spec.rb +5 -5
- data/spec/integration/{form → params}/predicates/lt_spec.rb +8 -8
- data/spec/integration/{form → params}/predicates/lteq_spec.rb +8 -8
- data/spec/integration/{form → params}/predicates/max_size_spec.rb +8 -8
- data/spec/integration/{form → params}/predicates/min_size_spec.rb +8 -8
- data/spec/integration/{form → params}/predicates/none_spec.rb +8 -8
- data/spec/integration/{form → params}/predicates/not_eql_spec.rb +8 -8
- data/spec/integration/{form → params}/predicates/odd_spec.rb +8 -8
- data/spec/integration/{form → params}/predicates/size/fixed_spec.rb +8 -8
- data/spec/integration/{form → params}/predicates/size/range_spec.rb +8 -8
- data/spec/integration/{form → params}/predicates/true_spec.rb +8 -8
- data/spec/integration/{form → params}/predicates/type_spec.rb +8 -8
- data/spec/integration/schema/array_schema_spec.rb +3 -3
- data/spec/integration/schema/extending_dsl_spec.rb +2 -2
- data/spec/integration/schema/form_spec.rb +3 -1
- data/spec/integration/schema/hash_schema_spec.rb +3 -3
- data/spec/integration/schema/json/explicit_types_spec.rb +1 -1
- data/spec/integration/schema/macros/each_spec.rb +1 -1
- data/spec/integration/schema/{form → params}/defining_base_schema_spec.rb +1 -1
- data/spec/integration/schema/{form → params}/explicit_types_spec.rb +35 -22
- data/spec/integration/schema/params_spec.rb +234 -0
- data/spec/integration/schema/reusing_schema_spec.rb +1 -1
- data/spec/integration/schema/using_types_spec.rb +1 -1
- data/spec/integration/schema_spec.rb +5 -5
- data/spec/spec_helper.rb +5 -2
- data/spec/unit/input_processor_compiler/{form_spec.rb → params_spec.rb} +13 -13
- metadata +77 -67
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6ae84a30ea54b0e41206d6bca9ca4b7c9a579b865d3c4ca205f4a762e2756c74
|
4
|
+
data.tar.gz: af35e1f8f6a8888b24439078ab5baf069dc46f6ecf60ff539cc818989c2fd997
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ee96f24719da71e1d21526d8786576a5e01637e79950b00f7bc24fcba334fa741cdd55631f3eba8aff682146321a6a87bba13704face8d9b2e4034964070a60d
|
7
|
+
data.tar.gz: '092f8d1d7ce01fe4676bc992af82fddf9e25f919a1147b5992e8a87048b12f3067adc5d96105d03e884ec33138dc8c64ce4a1bbcce555c15c19590d5fdff6ec0'
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,7 +1,17 @@
|
|
1
|
+
# v0.12.0 2018-05-31
|
2
|
+
|
3
|
+
### Changed
|
4
|
+
|
5
|
+
* Code updated to work with `dry-types` 0.13.1 and `dry-struct` 0.5.0, these are now minimal supported versions (flash-gordon)
|
6
|
+
* [BREAKING] `Form` was renamed to `Params` to be consistent with the latest changes from `dry-types`. You can `require 'dry/validation/compat/form'` to use the previous names but it will be removed in the next version (flash-gordon)
|
7
|
+
|
8
|
+
[Compare v0.11.1...v0.12.0](https://github.com/dry-rb/dry-validation/compare/v0.11.0...v0.12.0)
|
9
|
+
|
1
10
|
# v0.11.1 2017-09-15
|
2
11
|
|
3
12
|
### Changed
|
4
13
|
|
14
|
+
* `Result#to_either` was renamed to `#to_monad`, the previous name is kept for backward compatibility (flash-gordon)
|
5
15
|
* [internal] fix warnings from dry-types v0.12.0
|
6
16
|
|
7
17
|
[Compare v0.11.0...v0.11.1](https://github.com/dry-rb/dry-validation/compare/v0.11.0...v0.11.1)
|
data/Gemfile
CHANGED
@@ -2,23 +2,25 @@ source 'https://rubygems.org'
|
|
2
2
|
|
3
3
|
gemspec
|
4
4
|
|
5
|
+
gem 'dry-logic', git: 'https://github.com/dry-rb/dry-logic', branch: 'master'
|
6
|
+
|
5
7
|
group :test do
|
6
8
|
gem 'i18n', require: false
|
7
9
|
platform :mri do
|
8
10
|
gem 'codeclimate-test-reporter', require: false
|
9
11
|
gem 'simplecov', require: false
|
10
12
|
end
|
11
|
-
gem 'dry-monads', '
|
13
|
+
gem 'dry-monads', '>= 0.4.0', require: false
|
12
14
|
gem 'dry-struct', require: false
|
13
15
|
end
|
14
16
|
|
15
17
|
group :tools do
|
16
|
-
gem 'byebug', platform: :mri
|
17
|
-
gem 'pry'
|
18
|
+
gem 'pry-byebug', platform: :mri
|
19
|
+
gem 'pry', platform: :jruby
|
18
20
|
|
19
21
|
unless ENV['TRAVIS']
|
20
|
-
gem 'mutant',
|
21
|
-
gem 'mutant-rspec',
|
22
|
+
gem 'mutant', git: 'https://github.com/mbj/mutant'
|
23
|
+
gem 'mutant-rspec', git: 'https://github.com/mbj/mutant'
|
22
24
|
end
|
23
25
|
end
|
24
26
|
|
data/README.md
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
[gem]: https://rubygems.org/gems/dry-validation
|
2
2
|
[travis]: https://travis-ci.org/dry-rb/dry-validation
|
3
|
-
[gemnasium]: https://gemnasium.com/dry-rb/dry-validation
|
4
3
|
[codeclimate]: https://codeclimate.com/github/dry-rb/dry-validation
|
5
4
|
[coveralls]: https://coveralls.io/r/dry-rb/dry-validation
|
6
5
|
[inchpages]: http://inch-ci.org/github/dry-rb/dry-validation
|
@@ -9,11 +8,14 @@
|
|
9
8
|
|
10
9
|
[][gem]
|
11
10
|
[][travis]
|
12
|
-
[][gemnasium]
|
13
11
|
[][codeclimate]
|
14
12
|
[][codeclimate]
|
15
13
|
[][inchpages]
|
16
14
|
|
15
|
+
## Status
|
16
|
+
|
17
|
+
We're working on a new foundation for dry-validation, called dry-schema. You can see progress in [this PR](https://github.com/dry-rb/dry-schema/pull/3). This will result in a partial rewrite for 1.0.0 version. Currently known bugs/issues will be addressed in 1.0.0, **not in 0.x** due to lack of time. More info about 1.0.0 plans can be found [in this thread](https://discourse.dry-rb.org/t/plans-for-dry-validation-dry-schema-a-new-gem/215/3).
|
18
|
+
|
17
19
|
## Links
|
18
20
|
|
19
21
|
* [Documentation](http://dry-rb.org/gems/dry-validation)
|
data/Rakefile
CHANGED
@@ -8,13 +8,15 @@ require 'rspec/core/rake_task'
|
|
8
8
|
|
9
9
|
desc 'Run all specs in spec directory'
|
10
10
|
task :run_specs do
|
11
|
-
|
12
|
-
RSpec::Core::Runner.run(['spec/integration', 'spec/unit'])
|
11
|
+
core_result = RSpec::Core::Runner.run(['spec/integration', 'spec/unit'])
|
13
12
|
RSpec.clear_examples
|
13
|
+
|
14
14
|
Dir[SPEC_ROOT.join('shared/**/*.rb')].each(&method(:load))
|
15
15
|
|
16
16
|
Dry::Validation.load_extensions(:monads, :struct)
|
17
|
-
RSpec::Core::Runner.run(['spec'])
|
17
|
+
ext_result = RSpec::Core::Runner.run(['spec'])
|
18
|
+
|
19
|
+
exit [core_result, ext_result].max
|
18
20
|
end
|
19
21
|
|
20
22
|
task default: :run_specs
|
@@ -28,14 +28,14 @@ schema = Dry::Validation.Schema do
|
|
28
28
|
required(:age).filled(:int?, gt?: 18)
|
29
29
|
end
|
30
30
|
|
31
|
-
form = Dry::Validation.
|
31
|
+
form = Dry::Validation.Params do
|
32
32
|
configure do
|
33
33
|
config.messages = :i18n
|
34
34
|
config.type_specs = true
|
35
35
|
end
|
36
36
|
|
37
37
|
required(:email, :string).filled
|
38
|
-
required(:age, :
|
38
|
+
required(:age, :integer).filled(:int?, gt?: 18)
|
39
39
|
end
|
40
40
|
|
41
41
|
params = { 'email' => '', 'age' => '18' }
|
@@ -28,14 +28,14 @@ schema = Dry::Validation.Schema do
|
|
28
28
|
required(:age).filled(:int?, gt?: 18)
|
29
29
|
end
|
30
30
|
|
31
|
-
form = Dry::Validation.
|
31
|
+
form = Dry::Validation.Params do
|
32
32
|
configure do
|
33
33
|
config.messages = :i18n
|
34
34
|
config.type_specs = true
|
35
35
|
end
|
36
36
|
|
37
37
|
required(:email, :string).filled
|
38
|
-
required(:age, :
|
38
|
+
required(:age, :integer).filled(:int?, gt?: 18)
|
39
39
|
end
|
40
40
|
|
41
41
|
params = { 'email' => 'foo@bar.baz', 'age' => '19' }
|
data/dry-validation.gemspec
CHANGED
@@ -17,8 +17,8 @@ Gem::Specification.new do |spec|
|
|
17
17
|
spec.add_runtime_dependency 'concurrent-ruby', '~> 1.0'
|
18
18
|
spec.add_runtime_dependency 'dry-configurable', '~> 0.1', '>= 0.1.3'
|
19
19
|
spec.add_runtime_dependency 'dry-equalizer', '~> 0.2'
|
20
|
-
spec.add_runtime_dependency 'dry-
|
21
|
-
spec.add_runtime_dependency 'dry-
|
20
|
+
spec.add_runtime_dependency 'dry-logic', '~> 0.4', '>= 0.4.0'
|
21
|
+
spec.add_runtime_dependency 'dry-types', '~> 0.13.1'
|
22
22
|
spec.add_runtime_dependency 'dry-core', '~> 0.2', '>= 0.2.1'
|
23
23
|
|
24
24
|
spec.add_development_dependency 'bundler'
|
data/lib/dry/validation.rb
CHANGED
@@ -24,8 +24,8 @@ module Dry
|
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
27
|
-
def self.
|
28
|
-
klass = base ? Schema::
|
27
|
+
def self.Params(base = nil, **options, &block)
|
28
|
+
klass = base ? Schema::Params.configure(Class.new(base)) : Schema::Params
|
29
29
|
Validation.Schema(klass, options, &block)
|
30
30
|
end
|
31
31
|
|
@@ -37,7 +37,7 @@ module Dry
|
|
37
37
|
end
|
38
38
|
|
39
39
|
require 'dry/validation/schema'
|
40
|
-
require 'dry/validation/schema/
|
40
|
+
require 'dry/validation/schema/params'
|
41
41
|
require 'dry/validation/schema/json'
|
42
42
|
require 'dry/validation/extensions'
|
43
43
|
require 'dry/validation/version'
|
@@ -1,3 +1,13 @@
|
|
1
|
+
require 'dry/core/deprecations'
|
2
|
+
|
3
|
+
Dry::Core::Deprecations.warn('Form was renamed to Params and will be removed in the next version', tag: :'dry-validation')
|
4
|
+
|
5
|
+
require 'dry/types/compat/int'
|
6
|
+
require 'dry/types/compat/form_types'
|
7
|
+
require 'dry/validation/input_processor_compiler/params'
|
8
|
+
require 'dry/validation/schema/params'
|
9
|
+
|
10
|
+
|
1
11
|
module Dry
|
2
12
|
module Validation
|
3
13
|
class InputProcessorCompiler::Form < InputProcessorCompiler
|
@@ -8,7 +18,7 @@ module Dry
|
|
8
18
|
true?: 'form.true',
|
9
19
|
false?: 'form.false',
|
10
20
|
str?: 'string',
|
11
|
-
int?: 'form.
|
21
|
+
int?: 'form.integer',
|
12
22
|
float?: 'form.float',
|
13
23
|
decimal?: 'form.decimal',
|
14
24
|
date?: 'form.date',
|
@@ -21,7 +31,7 @@ module Dry
|
|
21
31
|
CONST_MAP = {
|
22
32
|
NilClass => 'form.nil',
|
23
33
|
String => 'string',
|
24
|
-
Integer => 'form.
|
34
|
+
Integer => 'form.integer',
|
25
35
|
Float => 'form.float',
|
26
36
|
BigDecimal => 'form.decimal',
|
27
37
|
Array => 'form.array',
|
@@ -45,5 +55,13 @@ module Dry
|
|
45
55
|
[:form_array, [members, {}]]
|
46
56
|
end
|
47
57
|
end
|
58
|
+
|
59
|
+
class << self
|
60
|
+
alias_method :Form, :Params
|
61
|
+
end
|
62
|
+
|
63
|
+
Schema::Form = Schema::Params
|
64
|
+
|
65
|
+
Schema::DEFAULT_PROCESSOR_MAP[:form] = InputProcessorCompiler::Form.new
|
48
66
|
end
|
49
67
|
end
|
@@ -1,17 +1,18 @@
|
|
1
|
-
require 'dry/monads/
|
1
|
+
require 'dry/monads/result'
|
2
2
|
|
3
3
|
module Dry
|
4
4
|
module Validation
|
5
5
|
class Result
|
6
|
-
include Dry::Monads::
|
6
|
+
include Dry::Monads::Result::Mixin
|
7
7
|
|
8
|
-
def
|
8
|
+
def to_monad(options = EMPTY_HASH)
|
9
9
|
if success?
|
10
|
-
|
10
|
+
Success(output)
|
11
11
|
else
|
12
|
-
|
12
|
+
Failure(messages(options))
|
13
13
|
end
|
14
14
|
end
|
15
|
+
alias_method :to_either, :to_monad
|
15
16
|
end
|
16
17
|
end
|
17
18
|
end
|
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'dry/types'
|
2
2
|
require 'dry/types/compiler'
|
3
|
+
require 'dry/types/compat'
|
3
4
|
|
4
5
|
module Dry
|
5
6
|
module Validation
|
@@ -131,4 +132,4 @@ end
|
|
131
132
|
|
132
133
|
require 'dry/validation/input_processor_compiler/sanitizer'
|
133
134
|
require 'dry/validation/input_processor_compiler/json'
|
134
|
-
require 'dry/validation/input_processor_compiler/
|
135
|
+
require 'dry/validation/input_processor_compiler/params'
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Dry
|
2
|
+
module Validation
|
3
|
+
class InputProcessorCompiler::Params < InputProcessorCompiler
|
4
|
+
PREDICATE_MAP = {
|
5
|
+
default: 'string',
|
6
|
+
none?: 'params.nil',
|
7
|
+
bool?: 'params.bool',
|
8
|
+
true?: 'params.true',
|
9
|
+
false?: 'params.false',
|
10
|
+
str?: 'string',
|
11
|
+
int?: 'params.integer',
|
12
|
+
float?: 'params.float',
|
13
|
+
decimal?: 'params.decimal',
|
14
|
+
date?: 'params.date',
|
15
|
+
date_time?: 'params.date_time',
|
16
|
+
time?: 'params.time',
|
17
|
+
hash?: 'params.hash',
|
18
|
+
array?: 'params.array'
|
19
|
+
}.freeze
|
20
|
+
|
21
|
+
CONST_MAP = {
|
22
|
+
NilClass => 'params.nil',
|
23
|
+
String => 'string',
|
24
|
+
Integer => 'params.integer',
|
25
|
+
Float => 'params.float',
|
26
|
+
BigDecimal => 'params.decimal',
|
27
|
+
Array => 'params.array',
|
28
|
+
Hash => 'params.hash',
|
29
|
+
Date => 'params.date',
|
30
|
+
DateTime => 'params.date_time',
|
31
|
+
Time => 'params.time',
|
32
|
+
TrueClass => 'params.true',
|
33
|
+
FalseClass => 'params.false'
|
34
|
+
}.freeze
|
35
|
+
|
36
|
+
def identifier
|
37
|
+
:params
|
38
|
+
end
|
39
|
+
|
40
|
+
def hash_node(schema)
|
41
|
+
[:params_hash, [schema, {}]]
|
42
|
+
end
|
43
|
+
|
44
|
+
def array_node(members)
|
45
|
+
[:params_array, [members, {}]]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -160,6 +160,8 @@ module Dry
|
|
160
160
|
end
|
161
161
|
|
162
162
|
def message_text(rule, template, tokens, opts)
|
163
|
+
original_verbosity = $VERBOSE
|
164
|
+
$VERBOSE = nil
|
163
165
|
text = template % tokens
|
164
166
|
|
165
167
|
if full?
|
@@ -168,6 +170,8 @@ module Dry
|
|
168
170
|
else
|
169
171
|
text
|
170
172
|
end
|
173
|
+
ensure
|
174
|
+
$VERBOSE = original_verbosity
|
171
175
|
end
|
172
176
|
|
173
177
|
def message_tokens(args)
|
@@ -78,7 +78,7 @@ module Dry
|
|
78
78
|
|
79
79
|
tokens[:rule] = predicate unless tokens.key?(:rule)
|
80
80
|
|
81
|
-
opts = options.
|
81
|
+
opts = options.select { |k, _| !config.lookup_options.include?(k) }
|
82
82
|
|
83
83
|
path = lookup_paths(tokens).detect do |key|
|
84
84
|
key?(key, opts) && get(key, opts).is_a?(String)
|
@@ -3,6 +3,16 @@ require 'dry/logic/rule/predicate'
|
|
3
3
|
module Dry
|
4
4
|
module Validation
|
5
5
|
class PredicateRegistry
|
6
|
+
module PredicateDetector
|
7
|
+
def method_added(name)
|
8
|
+
super
|
9
|
+
|
10
|
+
if name.to_s.end_with?('?')
|
11
|
+
registry.update(name => instance_method(name))
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
6
16
|
attr_reader :predicates
|
7
17
|
attr_reader :external
|
8
18
|
|
@@ -30,17 +40,8 @@ module Dry
|
|
30
40
|
end
|
31
41
|
end
|
32
42
|
|
33
|
-
def self.[](
|
34
|
-
Unbound.new(predicates)
|
35
|
-
klass.class_eval do
|
36
|
-
def self.method_added(name)
|
37
|
-
super
|
38
|
-
if name.to_s.end_with?('?')
|
39
|
-
registry.update(name => instance_method(name))
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|
43
|
-
end
|
43
|
+
def self.[](predicates)
|
44
|
+
Unbound.new(predicates)
|
44
45
|
end
|
45
46
|
|
46
47
|
def initialize(external, predicates = {})
|
@@ -7,6 +7,7 @@ module Dry
|
|
7
7
|
class Schema
|
8
8
|
extend Dry::Configurable
|
9
9
|
extend TypeSpecs
|
10
|
+
extend PredicateRegistry::PredicateDetector
|
10
11
|
|
11
12
|
NOOP_INPUT_PROCESSOR = -> input { input }
|
12
13
|
|
@@ -23,11 +24,13 @@ module Dry
|
|
23
24
|
setting :input_rule, nil
|
24
25
|
setting :dsl_extensions, nil
|
25
26
|
|
26
|
-
|
27
|
+
DEFAULT_PROCESSOR_MAP = {
|
27
28
|
sanitizer: InputProcessorCompiler::Sanitizer.new,
|
28
29
|
json: InputProcessorCompiler::JSON.new,
|
29
|
-
|
30
|
-
}
|
30
|
+
params: InputProcessorCompiler::Params.new,
|
31
|
+
}
|
32
|
+
|
33
|
+
setting :input_processor_map, DEFAULT_PROCESSOR_MAP
|
31
34
|
|
32
35
|
setting :type_specs, false
|
33
36
|
|
@@ -179,7 +182,7 @@ module Dry
|
|
179
182
|
end
|
180
183
|
|
181
184
|
def self.set_registry!
|
182
|
-
config.registry = PredicateRegistry[
|
185
|
+
config.registry = PredicateRegistry[config.predicates]
|
183
186
|
end
|
184
187
|
end
|
185
188
|
end
|
@@ -1,21 +1,9 @@
|
|
1
1
|
require 'dry/validation/schema'
|
2
|
+
require 'dry/validation/schema/params'
|
3
|
+
require 'dry/types/compat/form_types'
|
2
4
|
|
3
5
|
module Dry
|
4
6
|
module Validation
|
5
|
-
|
6
|
-
def self.configure(klass = nil, &block)
|
7
|
-
if klass
|
8
|
-
klass.configure do |config|
|
9
|
-
config.input_processor = :form
|
10
|
-
config.hash_type = :symbolized
|
11
|
-
end
|
12
|
-
klass
|
13
|
-
else
|
14
|
-
super(&block)
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
configure(self)
|
19
|
-
end
|
7
|
+
Schema::Form = Schema::Params
|
20
8
|
end
|
21
9
|
end
|