dry-validation 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|