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.
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