dry-validation 0.2.0 → 0.3.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/CHANGELOG.md +26 -2
- data/Gemfile +4 -0
- data/README.md +131 -42
- data/config/errors.yml +36 -27
- data/examples/basic.rb +2 -4
- data/examples/each.rb +2 -2
- data/examples/form.rb +1 -2
- data/examples/nested.rb +2 -4
- data/examples/rule_ast.rb +0 -8
- data/lib/dry/validation.rb +0 -5
- data/lib/dry/validation/error.rb +2 -6
- data/lib/dry/validation/error_compiler.rb +19 -5
- data/lib/dry/validation/input_type_compiler.rb +2 -1
- data/lib/dry/validation/messages.rb +7 -58
- data/lib/dry/validation/messages/abstract.rb +75 -0
- data/lib/dry/validation/messages/i18n.rb +24 -0
- data/lib/dry/validation/messages/namespaced.rb +27 -0
- data/lib/dry/validation/messages/yaml.rb +50 -0
- data/lib/dry/validation/result.rb +19 -49
- data/lib/dry/validation/rule.rb +2 -2
- data/lib/dry/validation/rule/group.rb +21 -0
- data/lib/dry/validation/rule/result.rb +73 -0
- data/lib/dry/validation/rule_compiler.rb +5 -0
- data/lib/dry/validation/schema.rb +33 -14
- data/lib/dry/validation/schema/definition.rb +16 -0
- data/lib/dry/validation/schema/result.rb +21 -3
- data/lib/dry/validation/schema/rule.rb +1 -1
- data/lib/dry/validation/schema/value.rb +2 -1
- data/lib/dry/validation/version.rb +1 -1
- data/spec/fixtures/locales/en.yml +5 -0
- data/spec/fixtures/locales/pl.yml +14 -0
- data/spec/integration/custom_error_messages_spec.rb +4 -16
- data/spec/{unit → integration}/error_compiler_spec.rb +81 -39
- data/spec/integration/localized_error_messages_spec.rb +52 -0
- data/spec/integration/messages/i18n_spec.rb +71 -0
- data/spec/integration/rule_groups_spec.rb +35 -0
- data/spec/integration/schema_form_spec.rb +9 -9
- data/spec/integration/schema_spec.rb +2 -2
- data/spec/shared/predicates.rb +2 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/unit/rule/group_spec.rb +12 -0
- data/spec/unit/schema_spec.rb +35 -0
- metadata +24 -6
- data/spec/fixtures/errors.yml +0 -4
data/examples/form.rb
CHANGED
@@ -9,7 +9,6 @@ end
|
|
9
9
|
|
10
10
|
schema = UserFormSchema.new
|
11
11
|
|
12
|
-
errors = schema.
|
12
|
+
errors = schema.call('email' => '', 'age' => '18').messages
|
13
13
|
|
14
14
|
puts errors.inspect
|
15
|
-
# [[:email, ["email must be filled"]], [:age, ["age must be greater than 18 (18 was given)"]]]
|
data/examples/nested.rb
CHANGED
@@ -21,12 +21,10 @@ end
|
|
21
21
|
|
22
22
|
schema = Schema.new
|
23
23
|
|
24
|
-
errors = schema.
|
24
|
+
errors = schema.call({}).messages
|
25
25
|
|
26
26
|
puts errors.inspect
|
27
|
-
#<Dry::Validation::Error::Set:0x007fc4f89c4360 @errors=[#<Dry::Validation::Error:0x007fc4f89c4108 @result=#<Dry::Validation::Result::Value success?=false input=nil rule=#<Dry::Validation::Rule::Key name=:address predicate=#<Dry::Validation::Predicate id=:key?>>>>]>
|
28
27
|
|
29
|
-
errors = schema.
|
28
|
+
errors = schema.call(address: { city: 'NYC' }).messages
|
30
29
|
|
31
30
|
puts errors.inspect
|
32
|
-
#<Dry::Validation::Error::Set:0x007fd151189b18 @errors=[#<Dry::Validation::Error:0x007fd151188e20 @result=#<Dry::Validation::Result::Set success?=false input={:city=>"NYC"} rule=#<Dry::Validation::Rule::Set name=:address predicate=[#<Dry::Validation::Rule::Conjunction left=#<Dry::Validation::Rule::Key name=:city predicate=#<Dry::Validation::Predicate id=:key?>> right=#<Dry::Validation::Rule::Value name=:city predicate=#<Dry::Validation::Predicate id=:min_size?>>>, #<Dry::Validation::Rule::Conjunction left=#<Dry::Validation::Rule::Key name=:street predicate=#<Dry::Validation::Predicate id=:key?>> right=#<Dry::Validation::Rule::Value name=:street predicate=#<Dry::Validation::Predicate id=:filled?>>>, #<Dry::Validation::Rule::Conjunction left=#<Dry::Validation::Rule::Key name=:country predicate=#<Dry::Validation::Predicate id=:key?>> right=#<Dry::Validation::Rule::Set name=:country predicate=[#<Dry::Validation::Rule::Conjunction left=#<Dry::Validation::Rule::Key name=:name predicate=#<Dry::Validation::Predicate id=:key?>> right=#<Dry::Validation::Rule::Value name=:name predicate=#<Dry::Validation::Predicate id=:filled?>>>, #<Dry::Validation::Rule::Conjunction left=#<Dry::Validation::Rule::Key name=:code predicate=#<Dry::Validation::Predicate id=:key?>> right=#<Dry::Validation::Rule::Value name=:code predicate=#<Dry::Validation::Predicate id=:filled?>>>]>>]>>>]>
|
data/examples/rule_ast.rb
CHANGED
@@ -21,13 +21,5 @@ compiler = Dry::Validation::RuleCompiler.new(Dry::Validation::Predicates)
|
|
21
21
|
rules = compiler.call(ast)
|
22
22
|
|
23
23
|
puts rules.inspect
|
24
|
-
# [
|
25
|
-
# #<Dry::Validation::Rule::Conjunction
|
26
|
-
# left=#<Dry::Validation::Rule::Key name=:age predicate=#<Dry::Validation::Predicate id=:key?>>
|
27
|
-
# right=#<Dry::Validation::Rule::Conjunction
|
28
|
-
# left=#<Dry::Validation::Rule::Value name=:age predicate=#<Dry::Validation::Predicate id=:filled?>>
|
29
|
-
# right=#<Dry::Validation::Rule::Value name=:age predicate=#<Dry::Validation::Predicate id=:gt?>>>>
|
30
|
-
# ]
|
31
24
|
|
32
25
|
puts rules.map(&:to_ary).inspect
|
33
|
-
# [[:and, [:key, [:age, [:predicate, [:key?, [:age]]]]], [[:and, [:val, [:age, [:predicate, [:filled?, []]]]], [[:val, [:age, [:predicate, [:gt?, [18]]]]]]]]]]
|
data/lib/dry/validation.rb
CHANGED
data/lib/dry/validation/error.rb
CHANGED
@@ -6,8 +6,8 @@ module Dry
|
|
6
6
|
|
7
7
|
attr_reader :errors
|
8
8
|
|
9
|
-
def initialize
|
10
|
-
@errors =
|
9
|
+
def initialize(errors)
|
10
|
+
@errors = errors
|
11
11
|
end
|
12
12
|
|
13
13
|
def each(&block)
|
@@ -18,10 +18,6 @@ module Dry
|
|
18
18
|
errors.empty?
|
19
19
|
end
|
20
20
|
|
21
|
-
def <<(error)
|
22
|
-
errors << error
|
23
|
-
end
|
24
|
-
|
25
21
|
def to_ary
|
26
22
|
errors.map { |error| error.to_ary }
|
27
23
|
end
|
@@ -1,14 +1,19 @@
|
|
1
1
|
module Dry
|
2
2
|
module Validation
|
3
3
|
class ErrorCompiler
|
4
|
-
attr_reader :messages
|
4
|
+
attr_reader :messages, :options
|
5
5
|
|
6
|
-
def initialize(messages)
|
6
|
+
def initialize(messages, options = {})
|
7
7
|
@messages = messages
|
8
|
+
@options = options
|
8
9
|
end
|
9
10
|
|
10
11
|
def call(ast)
|
11
|
-
ast.map { |node| visit(node) }
|
12
|
+
ast.map { |node| visit(node) }.reduce(:merge)
|
13
|
+
end
|
14
|
+
|
15
|
+
def with(options)
|
16
|
+
self.class.new(messages, options)
|
12
17
|
end
|
13
18
|
|
14
19
|
def visit(node, *args)
|
@@ -21,7 +26,7 @@ module Dry
|
|
21
26
|
|
22
27
|
def visit_input(input, *args)
|
23
28
|
name, value, rules = input
|
24
|
-
|
29
|
+
{ name => rules.map { |rule| visit(rule, name, value) } }
|
25
30
|
end
|
26
31
|
|
27
32
|
def visit_key(rule, name, value)
|
@@ -35,7 +40,16 @@ module Dry
|
|
35
40
|
end
|
36
41
|
|
37
42
|
def visit_predicate(predicate, value, name)
|
38
|
-
|
43
|
+
predicate_name, args = predicate
|
44
|
+
|
45
|
+
lookup_options = options.merge(
|
46
|
+
rule: name, val_type: value.class, arg_type: args[0].class
|
47
|
+
)
|
48
|
+
|
49
|
+
template = messages[predicate_name, lookup_options]
|
50
|
+
tokens = visit(predicate, value).merge(name: name)
|
51
|
+
|
52
|
+
[template % tokens, value]
|
39
53
|
end
|
40
54
|
|
41
55
|
def visit_key?(*args, value)
|
@@ -1,65 +1,14 @@
|
|
1
|
-
require 'yaml'
|
2
|
-
require 'pathname'
|
3
|
-
|
4
1
|
module Dry
|
5
2
|
module Validation
|
6
|
-
|
7
|
-
DEFAULT_PATH = Pathname(__dir__).join('../../../config/errors.yml').freeze
|
8
|
-
|
9
|
-
attr_reader :data
|
10
|
-
|
3
|
+
module Messages
|
11
4
|
def self.default
|
12
|
-
load
|
13
|
-
end
|
14
|
-
|
15
|
-
def self.load(path)
|
16
|
-
new(load_yaml(path))
|
17
|
-
end
|
18
|
-
|
19
|
-
def self.load_yaml(path)
|
20
|
-
Validation.symbolize_keys(YAML.load_file(path))
|
21
|
-
end
|
22
|
-
|
23
|
-
class Namespaced
|
24
|
-
attr_reader :namespace, :fallback
|
25
|
-
|
26
|
-
def initialize(namespace, fallback)
|
27
|
-
@namespace = namespace
|
28
|
-
@fallback = fallback
|
29
|
-
end
|
30
|
-
|
31
|
-
def lookup(*args)
|
32
|
-
namespace.lookup(*args) { fallback.lookup(*args) }
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
def initialize(data)
|
37
|
-
@data = data
|
38
|
-
end
|
39
|
-
|
40
|
-
def merge(overrides)
|
41
|
-
if overrides.is_a?(Hash)
|
42
|
-
self.class.new(data.merge(overrides))
|
43
|
-
else
|
44
|
-
self.class.new(data.merge(Messages.load_yaml(overrides)))
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
def namespaced(namespace)
|
49
|
-
Namespaced.new(Messages.new(data[namespace]), self)
|
50
|
-
end
|
51
|
-
|
52
|
-
def lookup(identifier, key, arg, &block)
|
53
|
-
message = data.fetch(:attributes, {}).fetch(key, {}).fetch(identifier) do
|
54
|
-
data.fetch(identifier, &block)
|
55
|
-
end
|
56
|
-
|
57
|
-
if message.is_a?(Hash)
|
58
|
-
message.fetch(arg.class.name.downcase.to_sym, message.fetch(:default))
|
59
|
-
else
|
60
|
-
message
|
61
|
-
end
|
5
|
+
Messages::YAML.load
|
62
6
|
end
|
63
7
|
end
|
64
8
|
end
|
65
9
|
end
|
10
|
+
|
11
|
+
require 'dry/validation/messages/abstract'
|
12
|
+
require 'dry/validation/messages/namespaced'
|
13
|
+
require 'dry/validation/messages/yaml'
|
14
|
+
require 'dry/validation/messages/i18n' if defined?(I18n)
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'thread_safe/cache'
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Validation
|
5
|
+
module Messages
|
6
|
+
class Abstract
|
7
|
+
extend Dry::Configurable
|
8
|
+
|
9
|
+
setting :path, Pathname(__dir__).join('../../../../config/errors.yml').realpath.freeze
|
10
|
+
setting :root, 'errors'.freeze
|
11
|
+
setting :lookup_options, [:root, :predicate, :rule, :val_type, :arg_type].freeze
|
12
|
+
|
13
|
+
setting :lookup_paths, %w(
|
14
|
+
%{root}.rules.%{rule}.%{predicate}.arg.%{arg_type}
|
15
|
+
%{root}.rules.%{rule}.%{predicate}
|
16
|
+
%{root}.%{predicate}.value.%{val_type}.arg.%{arg_type}
|
17
|
+
%{root}.%{predicate}.value.%{val_type}
|
18
|
+
%{root}.%{predicate}.arg.%{arg_type}
|
19
|
+
%{root}.%{predicate}
|
20
|
+
).freeze
|
21
|
+
|
22
|
+
setting :arg_type_default, 'default'.freeze
|
23
|
+
setting :val_type_default, 'default'.freeze
|
24
|
+
|
25
|
+
setting :arg_types, Hash.new { |*| config.arg_type_default }.update(
|
26
|
+
Range => 'range'
|
27
|
+
)
|
28
|
+
|
29
|
+
setting :val_types, Hash.new { |*| config.val_type_default }.update(
|
30
|
+
Range => 'range',
|
31
|
+
String => 'string'
|
32
|
+
)
|
33
|
+
|
34
|
+
def call(*args)
|
35
|
+
cache.fetch_or_store(args.hash) { get(*lookup(*args)) }
|
36
|
+
end
|
37
|
+
alias_method :[], :call
|
38
|
+
|
39
|
+
def lookup(predicate, options)
|
40
|
+
tokens = options.merge(
|
41
|
+
root: root,
|
42
|
+
predicate: predicate,
|
43
|
+
arg_type: config.arg_types[options[:arg_type]],
|
44
|
+
val_type: config.val_types[options[:val_type]]
|
45
|
+
)
|
46
|
+
|
47
|
+
opts = options.reject { |k, _| config.lookup_options.include?(k) }
|
48
|
+
path = lookup_paths(tokens).detect { |key| key?(key, opts) && get(key).is_a?(String) }
|
49
|
+
|
50
|
+
[path, opts]
|
51
|
+
end
|
52
|
+
|
53
|
+
def lookup_paths(tokens)
|
54
|
+
config.lookup_paths.map { |path| path % tokens }
|
55
|
+
end
|
56
|
+
|
57
|
+
def namespaced(namespace)
|
58
|
+
Messages::Namespaced.new(namespace, self)
|
59
|
+
end
|
60
|
+
|
61
|
+
def root
|
62
|
+
config.root
|
63
|
+
end
|
64
|
+
|
65
|
+
def config
|
66
|
+
self.class.config
|
67
|
+
end
|
68
|
+
|
69
|
+
def cache
|
70
|
+
@cache ||= ThreadSafe::Cache.new
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'i18n'
|
2
|
+
require 'dry/validation/messages/abstract'
|
3
|
+
|
4
|
+
module Dry
|
5
|
+
module Validation
|
6
|
+
class Messages::I18n < Messages::Abstract
|
7
|
+
attr_reader :t
|
8
|
+
|
9
|
+
::I18n.load_path << config.path
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@t = I18n.method(:t)
|
13
|
+
end
|
14
|
+
|
15
|
+
def get(key, options = {})
|
16
|
+
t.(key, options)
|
17
|
+
end
|
18
|
+
|
19
|
+
def key?(key, options)
|
20
|
+
I18n.exists?(key, options.fetch(:locale, I18n.default_locale))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Dry
|
2
|
+
module Validation
|
3
|
+
module Messages
|
4
|
+
class Namespaced < Messages::Abstract
|
5
|
+
attr_reader :namespace, :messages, :root
|
6
|
+
|
7
|
+
def initialize(namespace, messages)
|
8
|
+
@namespace = namespace
|
9
|
+
@messages = messages
|
10
|
+
@root = messages.root
|
11
|
+
end
|
12
|
+
|
13
|
+
def key?(key, *args)
|
14
|
+
messages.key?(key, *args)
|
15
|
+
end
|
16
|
+
|
17
|
+
def get(key, options = {})
|
18
|
+
messages.get(key, options)
|
19
|
+
end
|
20
|
+
|
21
|
+
def lookup_paths(tokens)
|
22
|
+
super(tokens.merge(root: "#{root}.rules.#{namespace}")) + super
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'pathname'
|
3
|
+
|
4
|
+
require 'dry/validation/messages/abstract'
|
5
|
+
|
6
|
+
module Dry
|
7
|
+
module Validation
|
8
|
+
class Messages::YAML < Messages::Abstract
|
9
|
+
attr_reader :data
|
10
|
+
|
11
|
+
configure do |config|
|
12
|
+
config.root = 'en.errors'.freeze
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.load(path = config.path)
|
16
|
+
new(load_file(path))
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.load_file(path)
|
20
|
+
flat_hash(YAML.load_file(path))
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.flat_hash(h, f = [], g = {})
|
24
|
+
return g.update(f.join('.'.freeze) => h) unless h.is_a? Hash
|
25
|
+
h.each { |k, r| flat_hash(r, f + [k], g) }
|
26
|
+
g
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize(data)
|
30
|
+
@data = data
|
31
|
+
end
|
32
|
+
|
33
|
+
def get(key, _options = {})
|
34
|
+
data[key]
|
35
|
+
end
|
36
|
+
|
37
|
+
def key?(key, *args)
|
38
|
+
data.key?(key)
|
39
|
+
end
|
40
|
+
|
41
|
+
def merge(overrides)
|
42
|
+
if overrides.is_a?(Hash)
|
43
|
+
self.class.new(data.merge(self.class.flat_hash(overrides)))
|
44
|
+
else
|
45
|
+
self.class.new(data.merge(Messages::YAML.load_file(overrides)))
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -1,71 +1,41 @@
|
|
1
1
|
module Dry
|
2
2
|
module Validation
|
3
|
-
def self.Result(input, value, rule)
|
4
|
-
case value
|
5
|
-
when Array then Result::Set.new(input, value, rule)
|
6
|
-
else Result::Value.new(input, value, rule)
|
7
|
-
end
|
8
|
-
end
|
9
|
-
|
10
3
|
class Result
|
11
|
-
include
|
12
|
-
|
13
|
-
attr_reader :input, :value, :rule
|
4
|
+
include Enumerable
|
14
5
|
|
15
|
-
|
16
|
-
def success?
|
17
|
-
value.all?(&:success?)
|
18
|
-
end
|
6
|
+
attr_reader :rule_results
|
19
7
|
|
20
|
-
|
21
|
-
|
22
|
-
[:input, [rule.name, input, value.values_at(*indices).map(&:to_ary)]]
|
23
|
-
end
|
8
|
+
def initialize(rule_results)
|
9
|
+
@rule_results = rule_results
|
24
10
|
end
|
25
11
|
|
26
|
-
|
27
|
-
|
28
|
-
[:input, [rule.name, input, [rule.to_ary]]]
|
29
|
-
end
|
30
|
-
alias_method :to_a, :to_ary
|
12
|
+
def each(&block)
|
13
|
+
rule_results.each(&block)
|
31
14
|
end
|
32
15
|
|
33
|
-
def
|
34
|
-
|
35
|
-
@value = value
|
36
|
-
@rule = rule
|
16
|
+
def to_ary
|
17
|
+
failures.map(&:to_ary)
|
37
18
|
end
|
38
19
|
|
39
|
-
def
|
40
|
-
|
41
|
-
other.(input)
|
42
|
-
else
|
43
|
-
Validation.Result(input, true, rule)
|
44
|
-
end
|
20
|
+
def <<(rule_result)
|
21
|
+
rule_results << rule_result
|
45
22
|
end
|
46
23
|
|
47
|
-
def
|
48
|
-
|
49
|
-
|
50
|
-
else
|
51
|
-
self
|
52
|
-
end
|
24
|
+
def with_values(names, &block)
|
25
|
+
values = names.map { |name| by_name(name) }.compact.map(&:input)
|
26
|
+
yield(values) if values.size == names.size
|
53
27
|
end
|
54
28
|
|
55
|
-
def
|
56
|
-
|
57
|
-
self
|
58
|
-
else
|
59
|
-
other.(input)
|
60
|
-
end
|
29
|
+
def by_name(name)
|
30
|
+
successes.detect { |rule_result| rule_result.name == name }
|
61
31
|
end
|
62
32
|
|
63
|
-
def
|
64
|
-
|
33
|
+
def successes
|
34
|
+
rule_results.select(&:success?)
|
65
35
|
end
|
66
36
|
|
67
|
-
def
|
68
|
-
|
37
|
+
def failures
|
38
|
+
rule_results.select(&:failure?)
|
69
39
|
end
|
70
40
|
end
|
71
41
|
end
|