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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -2
  3. data/Gemfile +4 -0
  4. data/README.md +131 -42
  5. data/config/errors.yml +36 -27
  6. data/examples/basic.rb +2 -4
  7. data/examples/each.rb +2 -2
  8. data/examples/form.rb +1 -2
  9. data/examples/nested.rb +2 -4
  10. data/examples/rule_ast.rb +0 -8
  11. data/lib/dry/validation.rb +0 -5
  12. data/lib/dry/validation/error.rb +2 -6
  13. data/lib/dry/validation/error_compiler.rb +19 -5
  14. data/lib/dry/validation/input_type_compiler.rb +2 -1
  15. data/lib/dry/validation/messages.rb +7 -58
  16. data/lib/dry/validation/messages/abstract.rb +75 -0
  17. data/lib/dry/validation/messages/i18n.rb +24 -0
  18. data/lib/dry/validation/messages/namespaced.rb +27 -0
  19. data/lib/dry/validation/messages/yaml.rb +50 -0
  20. data/lib/dry/validation/result.rb +19 -49
  21. data/lib/dry/validation/rule.rb +2 -2
  22. data/lib/dry/validation/rule/group.rb +21 -0
  23. data/lib/dry/validation/rule/result.rb +73 -0
  24. data/lib/dry/validation/rule_compiler.rb +5 -0
  25. data/lib/dry/validation/schema.rb +33 -14
  26. data/lib/dry/validation/schema/definition.rb +16 -0
  27. data/lib/dry/validation/schema/result.rb +21 -3
  28. data/lib/dry/validation/schema/rule.rb +1 -1
  29. data/lib/dry/validation/schema/value.rb +2 -1
  30. data/lib/dry/validation/version.rb +1 -1
  31. data/spec/fixtures/locales/en.yml +5 -0
  32. data/spec/fixtures/locales/pl.yml +14 -0
  33. data/spec/integration/custom_error_messages_spec.rb +4 -16
  34. data/spec/{unit → integration}/error_compiler_spec.rb +81 -39
  35. data/spec/integration/localized_error_messages_spec.rb +52 -0
  36. data/spec/integration/messages/i18n_spec.rb +71 -0
  37. data/spec/integration/rule_groups_spec.rb +35 -0
  38. data/spec/integration/schema_form_spec.rb +9 -9
  39. data/spec/integration/schema_spec.rb +2 -2
  40. data/spec/shared/predicates.rb +2 -0
  41. data/spec/spec_helper.rb +1 -0
  42. data/spec/unit/rule/group_spec.rb +12 -0
  43. data/spec/unit/schema_spec.rb +35 -0
  44. metadata +24 -6
  45. data/spec/fixtures/errors.yml +0 -4
@@ -9,7 +9,6 @@ end
9
9
 
10
10
  schema = UserFormSchema.new
11
11
 
12
- errors = schema.messages('email' => '', 'age' => '18')
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)"]]]
@@ -21,12 +21,10 @@ end
21
21
 
22
22
  schema = Schema.new
23
23
 
24
- errors = schema.messages({})
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.messages(address: { city: 'NYC' })
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?>>>]>>]>>>]>
@@ -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]]]]]]]]]]
@@ -6,11 +6,6 @@ require 'dry-container'
6
6
  # a common task in Ruby
7
7
  module Dry
8
8
  module Validation
9
- def self.symbolize_keys(hash)
10
- hash.each_with_object({}) do |(k, v), r|
11
- r[k.to_sym] = v.is_a?(Hash) ? symbolize_keys(v) : v
12
- end
13
- end
14
9
  end
15
10
  end
16
11
 
@@ -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
- [name, rules.map { |rule| visit(rule, name, value) }]
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
- messages.lookup(predicate[0], name, predicate[1][0]) % visit(predicate, value).merge(name: name)
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)
@@ -55,7 +55,8 @@ module Dry
55
55
  end
56
56
 
57
57
  def visit_implication(node)
58
- [:key, node.map(&method(:visit))]
58
+ key, types = node
59
+ [:key, [visit(key), visit(types, false)]]
59
60
  end
60
61
 
61
62
  def visit_key(node, *args)
@@ -1,65 +1,14 @@
1
- require 'yaml'
2
- require 'pathname'
3
-
4
1
  module Dry
5
2
  module Validation
6
- class Messages
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(DEFAULT_PATH)
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 Dry::Equalizer(:success?, :input, :rule)
12
-
13
- attr_reader :input, :value, :rule
4
+ include Enumerable
14
5
 
15
- class Set < Result
16
- def success?
17
- value.all?(&:success?)
18
- end
6
+ attr_reader :rule_results
19
7
 
20
- def to_ary
21
- indices = value.map { |v| v.failure? ? value.index(v) : nil }.compact
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
- class Value < Result
27
- def to_ary
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 initialize(input, value, rule)
34
- @input = input
35
- @value = value
36
- @rule = rule
16
+ def to_ary
17
+ failures.map(&:to_ary)
37
18
  end
38
19
 
39
- def >(other)
40
- if success?
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 and(other)
48
- if success?
49
- other.(input)
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 or(other)
56
- if success?
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 success?
64
- @value
33
+ def successes
34
+ rule_results.select(&:success?)
65
35
  end
66
36
 
67
- def failure?
68
- ! success?
37
+ def failures
38
+ rule_results.select(&:failure?)
69
39
  end
70
40
  end
71
41
  end