dry-validation 0.6.0 → 0.7.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/.codeclimate.yml +1 -0
- data/.travis.yml +3 -2
- data/CHANGELOG.md +42 -0
- data/Gemfile +8 -1
- data/README.md +13 -89
- data/config/errors.yml +35 -29
- data/dry-validation.gemspec +2 -2
- data/examples/basic.rb +3 -7
- data/examples/each.rb +3 -8
- data/examples/form.rb +3 -6
- data/examples/nested.rb +7 -15
- data/lib/dry/validation.rb +33 -5
- data/lib/dry/validation/error.rb +10 -26
- data/lib/dry/validation/error_compiler.rb +69 -99
- data/lib/dry/validation/error_compiler/input.rb +148 -0
- data/lib/dry/validation/hint_compiler.rb +83 -33
- data/lib/dry/validation/input_processor_compiler.rb +98 -0
- data/lib/dry/validation/input_processor_compiler/form.rb +46 -0
- data/lib/dry/validation/input_processor_compiler/sanitizer.rb +46 -0
- data/lib/dry/validation/messages/abstract.rb +30 -10
- data/lib/dry/validation/messages/i18n.rb +2 -1
- data/lib/dry/validation/messages/namespaced.rb +1 -0
- data/lib/dry/validation/messages/yaml.rb +8 -5
- data/lib/dry/validation/result.rb +33 -25
- data/lib/dry/validation/schema.rb +168 -61
- data/lib/dry/validation/schema/attr.rb +5 -27
- data/lib/dry/validation/schema/check.rb +24 -0
- data/lib/dry/validation/schema/dsl.rb +97 -0
- data/lib/dry/validation/schema/form.rb +2 -26
- data/lib/dry/validation/schema/key.rb +32 -28
- data/lib/dry/validation/schema/rule.rb +88 -32
- data/lib/dry/validation/schema/value.rb +77 -27
- data/lib/dry/validation/schema_compiler.rb +38 -0
- data/lib/dry/validation/version.rb +1 -1
- data/spec/fixtures/locales/pl.yml +1 -1
- data/spec/integration/attr_spec.rb +122 -0
- data/spec/integration/custom_error_messages_spec.rb +9 -11
- data/spec/integration/custom_predicates_spec.rb +68 -18
- data/spec/integration/error_compiler_spec.rb +259 -65
- data/spec/integration/hints_spec.rb +28 -9
- data/spec/integration/injecting_rules_spec.rb +11 -12
- data/spec/integration/localized_error_messages_spec.rb +16 -16
- data/spec/integration/messages/i18n_spec.rb +9 -5
- data/spec/integration/optional_keys_spec.rb +9 -11
- data/spec/integration/schema/array_schema_spec.rb +23 -0
- data/spec/integration/schema/check_rules_spec.rb +39 -31
- data/spec/integration/schema/check_with_nth_el_spec.rb +25 -0
- data/spec/integration/schema/each_with_set_spec.rb +23 -24
- data/spec/integration/schema/form_spec.rb +122 -0
- data/spec/integration/schema/inheriting_schema_spec.rb +31 -0
- data/spec/integration/schema/input_processor_spec.rb +46 -0
- data/spec/integration/schema/macros/confirmation_spec.rb +33 -0
- data/spec/integration/schema/macros/maybe_spec.rb +32 -0
- data/spec/integration/schema/macros/required_spec.rb +59 -0
- data/spec/integration/schema/macros/when_spec.rb +65 -0
- data/spec/integration/schema/nested_values_spec.rb +41 -0
- data/spec/integration/schema/not_spec.rb +14 -14
- data/spec/integration/schema/option_with_default_spec.rb +30 -0
- data/spec/integration/schema/reusing_schema_spec.rb +33 -0
- data/spec/integration/schema/using_types_spec.rb +29 -0
- data/spec/integration/schema/xor_spec.rb +17 -14
- data/spec/integration/schema_spec.rb +75 -245
- data/spec/shared/rule_compiler.rb +8 -0
- data/spec/spec_helper.rb +13 -0
- data/spec/unit/hint_compiler_spec.rb +10 -10
- data/spec/unit/{input_type_compiler_spec.rb → input_processor_compiler/form_spec.rb} +88 -73
- data/spec/unit/schema/key_spec.rb +33 -0
- data/spec/unit/schema/rule_spec.rb +7 -6
- data/spec/unit/schema/value_spec.rb +187 -54
- metadata +53 -31
- data/.rubocop.yml +0 -16
- data/.rubocop_todo.yml +0 -7
- data/lib/dry/validation/input_type_compiler.rb +0 -83
- data/lib/dry/validation/schema/definition.rb +0 -74
- data/lib/dry/validation/schema/result.rb +0 -68
- data/rakelib/rubocop.rake +0 -18
- data/spec/integration/rule_groups_spec.rb +0 -94
- data/spec/integration/schema/attrs_spec.rb +0 -38
- data/spec/integration/schema/default_key_behavior_spec.rb +0 -23
- data/spec/integration/schema/grouped_rules_spec.rb +0 -57
- data/spec/integration/schema/nested_spec.rb +0 -31
- data/spec/integration/schema_form_spec.rb +0 -97
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'dry/types'
|
2
|
+
require 'dry/types/compiler'
|
3
|
+
|
4
|
+
module Dry
|
5
|
+
module Validation
|
6
|
+
class InputProcessorCompiler
|
7
|
+
attr_reader :type_compiler
|
8
|
+
|
9
|
+
DEFAULT_TYPE_NODE = [[:type, 'string']].freeze
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@type_compiler = Dry::Types::Compiler.new(Dry::Types)
|
13
|
+
end
|
14
|
+
|
15
|
+
def call(ast)
|
16
|
+
type_compiler.(hash_node(schema_ast(ast)))
|
17
|
+
end
|
18
|
+
|
19
|
+
def schema_ast(ast)
|
20
|
+
ast.map { |node| visit(node) }
|
21
|
+
end
|
22
|
+
|
23
|
+
def visit(node, *args)
|
24
|
+
send(:"visit_#{node[0]}", node[1], *args)
|
25
|
+
end
|
26
|
+
|
27
|
+
def visit_schema(node, *args)
|
28
|
+
hash_node(node.input_processor_ast(identifier))
|
29
|
+
end
|
30
|
+
|
31
|
+
def visit_or(node, *args)
|
32
|
+
left, right = node
|
33
|
+
[:sum, [visit(left, *args), visit(right, *args)]]
|
34
|
+
end
|
35
|
+
|
36
|
+
def visit_and(node, first = true)
|
37
|
+
if first
|
38
|
+
name, type = node.map { |n| visit(n, false) }.uniq
|
39
|
+
[:key, [name, type]]
|
40
|
+
else
|
41
|
+
result = node.map { |n| visit(n, first) }.uniq
|
42
|
+
|
43
|
+
if result.size == 1
|
44
|
+
result.first
|
45
|
+
else
|
46
|
+
(result - self.class::DEFAULT_TYPE_NODE).first
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def visit_implication(node)
|
52
|
+
key, types = node
|
53
|
+
[:key, [visit(key), visit(types, false)]]
|
54
|
+
end
|
55
|
+
|
56
|
+
def visit_key(node, *args)
|
57
|
+
_, other = node
|
58
|
+
visit(other, *args)
|
59
|
+
end
|
60
|
+
|
61
|
+
def visit_val(node, *args)
|
62
|
+
visit(node, *args)
|
63
|
+
end
|
64
|
+
|
65
|
+
def visit_set(node, *)
|
66
|
+
hash_node(node.map { |n| visit(n) })
|
67
|
+
end
|
68
|
+
|
69
|
+
def visit_each(node, *args)
|
70
|
+
array_node(visit(node, *args))
|
71
|
+
end
|
72
|
+
|
73
|
+
def visit_predicate(node, *args)
|
74
|
+
id, args = node
|
75
|
+
|
76
|
+
if id == :key?
|
77
|
+
args[0]
|
78
|
+
else
|
79
|
+
type(id, args)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def type(predicate, args)
|
84
|
+
default = self.class::PREDICATE_MAP[:default]
|
85
|
+
|
86
|
+
if predicate == :type?
|
87
|
+
const = args[0]
|
88
|
+
[:type, self.class::CONST_MAP[const] || Types.identifier(const)]
|
89
|
+
else
|
90
|
+
[:type, self.class::PREDICATE_MAP[predicate] || default]
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
require 'dry/validation/input_processor_compiler/sanitizer'
|
98
|
+
require 'dry/validation/input_processor_compiler/form'
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Dry
|
2
|
+
module Validation
|
3
|
+
class InputProcessorCompiler::Form < InputProcessorCompiler
|
4
|
+
PREDICATE_MAP = {
|
5
|
+
default: 'string',
|
6
|
+
none?: 'form.nil',
|
7
|
+
bool?: 'form.bool',
|
8
|
+
str?: 'string',
|
9
|
+
int?: 'form.int',
|
10
|
+
float?: 'form.float',
|
11
|
+
decimal?: 'form.decimal',
|
12
|
+
date?: 'form.date',
|
13
|
+
date_time?: 'form.date_time',
|
14
|
+
time?: 'form.time'
|
15
|
+
}.freeze
|
16
|
+
|
17
|
+
CONST_MAP = {
|
18
|
+
NilClass => 'form.nil',
|
19
|
+
String => 'string',
|
20
|
+
Fixnum => 'form.int',
|
21
|
+
Integer => 'form.int',
|
22
|
+
Float => 'form.float',
|
23
|
+
BigDecimal => 'form.decimal',
|
24
|
+
Array => 'form.array',
|
25
|
+
Hash => 'form.hash',
|
26
|
+
Date => 'form.date',
|
27
|
+
DateTime => 'form.date_time',
|
28
|
+
Time => 'form.time',
|
29
|
+
TrueClass => 'form.true',
|
30
|
+
FalseClass => 'form.false'
|
31
|
+
}.freeze
|
32
|
+
|
33
|
+
def identifier
|
34
|
+
:form
|
35
|
+
end
|
36
|
+
|
37
|
+
def hash_node(schema)
|
38
|
+
[:type, ['form.hash', [:symbolized, schema]]]
|
39
|
+
end
|
40
|
+
|
41
|
+
def array_node(members)
|
42
|
+
[:type, ['form.array', members]]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Dry
|
2
|
+
module Validation
|
3
|
+
class InputProcessorCompiler::Sanitizer < InputProcessorCompiler
|
4
|
+
PREDICATE_MAP = {
|
5
|
+
default: 'string',
|
6
|
+
none?: 'nil',
|
7
|
+
bool?: 'bool',
|
8
|
+
str?: 'string',
|
9
|
+
int?: 'int',
|
10
|
+
float?: 'float',
|
11
|
+
decimal?: 'decimal',
|
12
|
+
date?: 'date',
|
13
|
+
date_time?: 'date_time',
|
14
|
+
time?: 'time'
|
15
|
+
}.freeze
|
16
|
+
|
17
|
+
CONST_MAP = {
|
18
|
+
NilClass => 'nil',
|
19
|
+
String => 'string',
|
20
|
+
Fixnum => 'int',
|
21
|
+
Integer => 'int',
|
22
|
+
Float => 'float',
|
23
|
+
BigDecimal => 'decimal',
|
24
|
+
Array => 'array',
|
25
|
+
Hash => 'hash',
|
26
|
+
Date => 'date',
|
27
|
+
DateTime => 'date_time',
|
28
|
+
Time => 'time',
|
29
|
+
TrueClass => 'true',
|
30
|
+
FalseClass => 'false'
|
31
|
+
}.freeze
|
32
|
+
|
33
|
+
def identifier
|
34
|
+
:sanitizer
|
35
|
+
end
|
36
|
+
|
37
|
+
def hash_node(schema)
|
38
|
+
[:type, ['hash', [:schema, schema]]]
|
39
|
+
end
|
40
|
+
|
41
|
+
def array_node(members)
|
42
|
+
[:type, ['array', members]]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -5,9 +5,10 @@ module Dry
|
|
5
5
|
module Validation
|
6
6
|
module Messages
|
7
7
|
class Abstract
|
8
|
-
DEFAULT_PATH = Pathname(__dir__).join('../../../../config/errors.yml').realpath.freeze
|
9
|
-
|
10
8
|
extend Dry::Configurable
|
9
|
+
include Dry::Equalizer(:config)
|
10
|
+
|
11
|
+
DEFAULT_PATH = Pathname(__dir__).join('../../../../config/errors.yml').realpath.freeze
|
11
12
|
|
12
13
|
setting :paths, [DEFAULT_PATH]
|
13
14
|
setting :root, 'errors'.freeze
|
@@ -34,12 +35,30 @@ module Dry
|
|
34
35
|
String => 'string'
|
35
36
|
)
|
36
37
|
|
38
|
+
def self.cache
|
39
|
+
@cache ||= ThreadSafe::Cache.new { |h, k| h[k] = ThreadSafe::Cache.new }
|
40
|
+
end
|
41
|
+
|
42
|
+
attr_reader :config
|
43
|
+
|
44
|
+
def initialize
|
45
|
+
@config = self.class.config
|
46
|
+
end
|
47
|
+
|
48
|
+
def rule(name, options = {})
|
49
|
+
path = "%{locale}.rules.#{name}"
|
50
|
+
get(path, options) if key?(path, options)
|
51
|
+
end
|
52
|
+
|
37
53
|
def call(*args)
|
38
|
-
cache.fetch_or_store(args.hash)
|
54
|
+
cache.fetch_or_store(args.hash) do
|
55
|
+
path, opts = lookup(*args)
|
56
|
+
get(path, opts) if path
|
57
|
+
end
|
39
58
|
end
|
40
59
|
alias_method :[], :call
|
41
60
|
|
42
|
-
def lookup(predicate, options)
|
61
|
+
def lookup(predicate, options = {})
|
43
62
|
tokens = options.merge(
|
44
63
|
root: root,
|
45
64
|
predicate: predicate,
|
@@ -47,8 +66,13 @@ module Dry
|
|
47
66
|
val_type: config.val_types[options[:val_type]]
|
48
67
|
)
|
49
68
|
|
69
|
+
tokens[:rule] = predicate unless tokens.key?(:rule)
|
70
|
+
|
50
71
|
opts = options.reject { |k, _| config.lookup_options.include?(k) }
|
51
|
-
|
72
|
+
|
73
|
+
path = lookup_paths(tokens).detect do |key|
|
74
|
+
key?(key, opts) && get(key, opts).is_a?(String)
|
75
|
+
end
|
52
76
|
|
53
77
|
[path, opts]
|
54
78
|
end
|
@@ -65,12 +89,8 @@ module Dry
|
|
65
89
|
config.root
|
66
90
|
end
|
67
91
|
|
68
|
-
def config
|
69
|
-
self.class.config
|
70
|
-
end
|
71
|
-
|
72
92
|
def cache
|
73
|
-
|
93
|
+
self.class.cache[self]
|
74
94
|
end
|
75
95
|
end
|
76
96
|
end
|
@@ -6,10 +6,12 @@ require 'dry/validation/messages/abstract'
|
|
6
6
|
module Dry
|
7
7
|
module Validation
|
8
8
|
class Messages::YAML < Messages::Abstract
|
9
|
+
include Dry::Equalizer(:data)
|
10
|
+
|
9
11
|
attr_reader :data
|
10
12
|
|
11
13
|
configure do |config|
|
12
|
-
config.root = '
|
14
|
+
config.root = '%{locale}.errors'.freeze
|
13
15
|
end
|
14
16
|
|
15
17
|
def self.load(paths = config.paths)
|
@@ -27,15 +29,16 @@ module Dry
|
|
27
29
|
end
|
28
30
|
|
29
31
|
def initialize(data)
|
32
|
+
super()
|
30
33
|
@data = data
|
31
34
|
end
|
32
35
|
|
33
|
-
def get(key,
|
34
|
-
data[key]
|
36
|
+
def get(key, options = {})
|
37
|
+
data[key % { locale: options.fetch(:locale, :en) }]
|
35
38
|
end
|
36
39
|
|
37
|
-
def key?(key,
|
38
|
-
data.key?(key)
|
40
|
+
def key?(key, options = {})
|
41
|
+
data.key?(key % { locale: options.fetch(:locale, :en) })
|
39
42
|
end
|
40
43
|
|
41
44
|
def merge(overrides)
|
@@ -1,53 +1,61 @@
|
|
1
1
|
module Dry
|
2
2
|
module Validation
|
3
3
|
class Result
|
4
|
+
include Dry::Equalizer(:output, :messages)
|
4
5
|
include Enumerable
|
5
6
|
|
6
|
-
attr_reader :
|
7
|
+
attr_reader :output
|
8
|
+
attr_reader :errors
|
9
|
+
attr_reader :error_compiler
|
10
|
+
attr_reader :hint_compiler
|
7
11
|
|
8
|
-
|
9
|
-
|
12
|
+
alias_method :to_hash, :output
|
13
|
+
|
14
|
+
EMPTY_MESSAGES = {}.freeze
|
15
|
+
|
16
|
+
def initialize(output, errors, error_compiler, hint_compiler)
|
17
|
+
@output = output
|
18
|
+
@errors = errors
|
19
|
+
@error_compiler = error_compiler
|
20
|
+
@hint_compiler = hint_compiler
|
10
21
|
end
|
11
22
|
|
12
23
|
def each(&block)
|
13
|
-
|
24
|
+
output.each(&block)
|
14
25
|
end
|
15
26
|
|
16
27
|
def [](name)
|
17
|
-
|
28
|
+
output.fetch(name)
|
18
29
|
end
|
19
30
|
|
20
|
-
def
|
21
|
-
|
31
|
+
def success?
|
32
|
+
errors.empty?
|
22
33
|
end
|
23
34
|
|
24
|
-
def
|
25
|
-
|
35
|
+
def failure?
|
36
|
+
!success?
|
26
37
|
end
|
27
38
|
|
28
|
-
def
|
29
|
-
|
30
|
-
|
39
|
+
def messages(options = {})
|
40
|
+
@messages ||=
|
41
|
+
begin
|
42
|
+
return EMPTY_MESSAGES if success?
|
31
43
|
|
32
|
-
|
33
|
-
|
34
|
-
end
|
44
|
+
hints = hint_compiler.with(options).call
|
45
|
+
comp = error_compiler.with(options.merge(hints: hints))
|
35
46
|
|
36
|
-
|
37
|
-
|
38
|
-
yield(values) if values.size == names.size
|
47
|
+
comp.(error_ast)
|
48
|
+
end
|
39
49
|
end
|
40
50
|
|
41
|
-
def
|
42
|
-
|
51
|
+
def to_ast
|
52
|
+
[:set, error_ast]
|
43
53
|
end
|
44
54
|
|
45
|
-
|
46
|
-
rule_results.select(&:success?)
|
47
|
-
end
|
55
|
+
private
|
48
56
|
|
49
|
-
def
|
50
|
-
|
57
|
+
def error_ast
|
58
|
+
errors.map { |error| error.to_ast }
|
51
59
|
end
|
52
60
|
end
|
53
61
|
end
|
@@ -1,45 +1,73 @@
|
|
1
|
-
require 'dry/
|
2
|
-
|
1
|
+
require 'dry/types/constraints'
|
2
|
+
|
3
|
+
require 'dry/validation/schema_compiler'
|
4
|
+
require 'dry/validation/schema/key'
|
5
|
+
require 'dry/validation/schema/attr'
|
6
|
+
require 'dry/validation/schema/value'
|
7
|
+
require 'dry/validation/schema/check'
|
3
8
|
|
4
|
-
require 'dry/validation/schema/definition'
|
5
9
|
require 'dry/validation/error'
|
10
|
+
require 'dry/validation/result'
|
6
11
|
require 'dry/validation/messages'
|
7
12
|
require 'dry/validation/error_compiler'
|
8
13
|
require 'dry/validation/hint_compiler'
|
9
|
-
|
10
|
-
require 'dry/validation/
|
14
|
+
|
15
|
+
require 'dry/validation/input_processor_compiler'
|
11
16
|
|
12
17
|
module Dry
|
13
18
|
module Validation
|
14
19
|
class Schema
|
15
20
|
extend Dry::Configurable
|
16
|
-
extend Definition
|
17
21
|
|
18
|
-
|
22
|
+
NOOP_INPUT_PROCESSOR = -> input { input }
|
23
|
+
|
24
|
+
setting :path
|
25
|
+
setting :predicates, Types::Predicates
|
19
26
|
setting :messages, :yaml
|
20
27
|
setting :messages_file
|
21
28
|
setting :namespace
|
29
|
+
setting :rules, []
|
30
|
+
setting :checks, []
|
22
31
|
|
23
|
-
|
24
|
-
|
32
|
+
setting :input_processor, :noop
|
33
|
+
|
34
|
+
setting :input_processor_map, {
|
35
|
+
sanitizer: InputProcessorCompiler::Sanitizer.new,
|
36
|
+
form: InputProcessorCompiler::Form.new
|
37
|
+
}.freeze
|
38
|
+
|
39
|
+
def self.inherited(klass)
|
40
|
+
super
|
41
|
+
klass.setting :options, {}
|
25
42
|
end
|
26
43
|
|
27
|
-
def self.
|
28
|
-
|
44
|
+
def self.new(rules = config.rules, **options)
|
45
|
+
super(rules, default_options.merge(options))
|
29
46
|
end
|
30
47
|
|
31
|
-
def self.
|
32
|
-
|
48
|
+
def self.option(name, default = nil)
|
49
|
+
attr_reader(*name)
|
50
|
+
options.update(name => default)
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.to_ast
|
54
|
+
[:schema, self]
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.rules
|
58
|
+
config.rules
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.predicates
|
62
|
+
config.predicates
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.options
|
66
|
+
config.options
|
33
67
|
end
|
34
68
|
|
35
69
|
def self.messages
|
36
|
-
default =
|
37
|
-
case config.messages
|
38
|
-
when :yaml then Messages.default
|
39
|
-
when :i18n then Messages::I18n.new
|
40
|
-
else
|
41
|
-
fail "+#{config.messages}+ is not a valid messages identifier"
|
42
|
-
end
|
70
|
+
default = default_messages
|
43
71
|
|
44
72
|
if config.messages_file && config.namespace
|
45
73
|
default.merge(config.messages_file).namespaced(config.namespace)
|
@@ -52,23 +80,61 @@ module Dry
|
|
52
80
|
end
|
53
81
|
end
|
54
82
|
|
55
|
-
def self.
|
56
|
-
|
83
|
+
def self.default_messages
|
84
|
+
case config.messages
|
85
|
+
when :yaml then Messages.default
|
86
|
+
when :i18n then Messages::I18n.new
|
87
|
+
else
|
88
|
+
raise "+#{config.messages}+ is not a valid messages identifier"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.error_compiler
|
93
|
+
@error_compiler ||= ErrorCompiler.new(messages)
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.hint_compiler
|
97
|
+
@hint_compiler ||= HintCompiler.new(messages, rules: rule_ast)
|
98
|
+
end
|
99
|
+
|
100
|
+
def self.input_processor
|
101
|
+
@input_processor ||=
|
102
|
+
begin
|
103
|
+
if input_processor_compiler
|
104
|
+
input_processor_compiler.(rule_ast)
|
105
|
+
else
|
106
|
+
NOOP_INPUT_PROCESSOR
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def self.input_processor_ast(type)
|
112
|
+
config.input_processor_map.fetch(type).schema_ast(rule_ast)
|
57
113
|
end
|
58
114
|
|
59
|
-
def self.
|
60
|
-
@
|
115
|
+
def self.input_processor_compiler
|
116
|
+
@input_processor_comp ||= config.input_processor_map[config.input_processor]
|
61
117
|
end
|
62
118
|
|
63
|
-
def self.
|
64
|
-
@
|
119
|
+
def self.rule_ast
|
120
|
+
@rule_ast ||= config.rules.flat_map(&:rules).map(&:to_ast)
|
65
121
|
end
|
66
122
|
|
67
|
-
def self.
|
68
|
-
|
123
|
+
def self.default_options
|
124
|
+
{ predicates: predicates,
|
125
|
+
error_compiler: error_compiler,
|
126
|
+
hint_compiler: hint_compiler,
|
127
|
+
input_processor: input_processor,
|
128
|
+
checks: config.checks }
|
69
129
|
end
|
70
130
|
|
71
|
-
attr_reader :rules
|
131
|
+
attr_reader :rules
|
132
|
+
|
133
|
+
attr_reader :checks
|
134
|
+
|
135
|
+
attr_reader :predicates
|
136
|
+
|
137
|
+
attr_reader :input_processor
|
72
138
|
|
73
139
|
attr_reader :rule_compiler
|
74
140
|
|
@@ -76,55 +142,96 @@ module Dry
|
|
76
142
|
|
77
143
|
attr_reader :hint_compiler
|
78
144
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
@
|
83
|
-
@
|
84
|
-
@
|
85
|
-
@
|
86
|
-
@
|
145
|
+
attr_reader :options
|
146
|
+
|
147
|
+
def initialize(rules, options)
|
148
|
+
@rule_compiler = SchemaCompiler.new(self)
|
149
|
+
@error_compiler = options.fetch(:error_compiler)
|
150
|
+
@hint_compiler = options.fetch(:hint_compiler)
|
151
|
+
@predicates = options.fetch(:predicates)
|
152
|
+
@input_processor = options.fetch(:input_processor, NOOP_INPUT_PROCESSOR)
|
153
|
+
|
154
|
+
initialize_options(options)
|
155
|
+
initialize_rules(rules)
|
156
|
+
initialize_checks(options.fetch(:checks, []))
|
157
|
+
|
158
|
+
freeze
|
159
|
+
end
|
160
|
+
|
161
|
+
def with(new_options)
|
162
|
+
self.class.new(self.class.rules, options.merge(new_options))
|
87
163
|
end
|
88
164
|
|
89
165
|
def call(input)
|
90
|
-
|
166
|
+
processed_input = input_processor[input]
|
167
|
+
Result.new(processed_input, apply(processed_input), error_compiler, hint_compiler)
|
168
|
+
end
|
91
169
|
|
92
|
-
|
93
|
-
|
170
|
+
def [](name)
|
171
|
+
if predicates.key?(name)
|
172
|
+
predicates[name]
|
173
|
+
elsif respond_to?(name)
|
174
|
+
Logic::Predicate.new(name, &method(name))
|
175
|
+
else
|
176
|
+
raise ArgumentError, "+#{name}+ is not a valid predicate name"
|
94
177
|
end
|
178
|
+
end
|
95
179
|
|
96
|
-
|
97
|
-
resolver = -> name { result[name] || self[name] }
|
98
|
-
compiled_checks = Logic::RuleCompiler.new(resolver).(checks)
|
180
|
+
private
|
99
181
|
|
100
|
-
|
101
|
-
|
102
|
-
|
182
|
+
def apply(input)
|
183
|
+
results = rule_results(input)
|
184
|
+
|
185
|
+
results.merge!(check_results(input, results)) unless checks.empty?
|
186
|
+
|
187
|
+
results
|
188
|
+
.select { |_, result| result.failure? }
|
189
|
+
.map { |name, result| Error.new(error_path(name), result) }
|
190
|
+
end
|
191
|
+
|
192
|
+
def error_path(name)
|
193
|
+
full_path = Array[*self.class.config.path]
|
194
|
+
full_path << name
|
195
|
+
full_path.size > 1 ? full_path : full_path[0]
|
196
|
+
end
|
197
|
+
|
198
|
+
def rule_results(input)
|
199
|
+
rules.each_with_object({}) do |(name, rule), hash|
|
200
|
+
hash[name] = rule.(input)
|
103
201
|
end
|
202
|
+
end
|
104
203
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
204
|
+
def check_results(input, result)
|
205
|
+
checks.each_with_object({}) do |(name, check), hash|
|
206
|
+
check_res = check.is_a?(Guard) ? check.(input, result) : check.(input)
|
207
|
+
hash[name] = check_res if check_res
|
109
208
|
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def initialize_options(options)
|
212
|
+
@options = options
|
110
213
|
|
111
|
-
|
214
|
+
self.class.options.each do |name, default|
|
215
|
+
value = options.fetch(name) do
|
216
|
+
case default
|
217
|
+
when Proc then default.()
|
218
|
+
else default end
|
219
|
+
end
|
112
220
|
|
113
|
-
|
221
|
+
instance_variable_set("@#{name}", value)
|
222
|
+
end
|
114
223
|
end
|
115
224
|
|
116
|
-
def
|
117
|
-
|
118
|
-
|
119
|
-
elsif respond_to?(name)
|
120
|
-
Logic::Predicate.new(name, &method(name))
|
121
|
-
else
|
122
|
-
fail ArgumentError, "+#{name}+ is not a valid predicate name"
|
225
|
+
def initialize_rules(rules)
|
226
|
+
@rules = rules.each_with_object({}) do |rule, result|
|
227
|
+
result[rule.name] = rule_compiler.visit(rule.to_ast)
|
123
228
|
end
|
124
229
|
end
|
125
230
|
|
126
|
-
def
|
127
|
-
|
231
|
+
def initialize_checks(checks)
|
232
|
+
@checks = checks.each_with_object({}) do |check, result|
|
233
|
+
result[check.name] = rule_compiler.visit(check.to_ast)
|
234
|
+
end
|
128
235
|
end
|
129
236
|
end
|
130
237
|
end
|