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.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +1 -0
  3. data/.travis.yml +3 -2
  4. data/CHANGELOG.md +42 -0
  5. data/Gemfile +8 -1
  6. data/README.md +13 -89
  7. data/config/errors.yml +35 -29
  8. data/dry-validation.gemspec +2 -2
  9. data/examples/basic.rb +3 -7
  10. data/examples/each.rb +3 -8
  11. data/examples/form.rb +3 -6
  12. data/examples/nested.rb +7 -15
  13. data/lib/dry/validation.rb +33 -5
  14. data/lib/dry/validation/error.rb +10 -26
  15. data/lib/dry/validation/error_compiler.rb +69 -99
  16. data/lib/dry/validation/error_compiler/input.rb +148 -0
  17. data/lib/dry/validation/hint_compiler.rb +83 -33
  18. data/lib/dry/validation/input_processor_compiler.rb +98 -0
  19. data/lib/dry/validation/input_processor_compiler/form.rb +46 -0
  20. data/lib/dry/validation/input_processor_compiler/sanitizer.rb +46 -0
  21. data/lib/dry/validation/messages/abstract.rb +30 -10
  22. data/lib/dry/validation/messages/i18n.rb +2 -1
  23. data/lib/dry/validation/messages/namespaced.rb +1 -0
  24. data/lib/dry/validation/messages/yaml.rb +8 -5
  25. data/lib/dry/validation/result.rb +33 -25
  26. data/lib/dry/validation/schema.rb +168 -61
  27. data/lib/dry/validation/schema/attr.rb +5 -27
  28. data/lib/dry/validation/schema/check.rb +24 -0
  29. data/lib/dry/validation/schema/dsl.rb +97 -0
  30. data/lib/dry/validation/schema/form.rb +2 -26
  31. data/lib/dry/validation/schema/key.rb +32 -28
  32. data/lib/dry/validation/schema/rule.rb +88 -32
  33. data/lib/dry/validation/schema/value.rb +77 -27
  34. data/lib/dry/validation/schema_compiler.rb +38 -0
  35. data/lib/dry/validation/version.rb +1 -1
  36. data/spec/fixtures/locales/pl.yml +1 -1
  37. data/spec/integration/attr_spec.rb +122 -0
  38. data/spec/integration/custom_error_messages_spec.rb +9 -11
  39. data/spec/integration/custom_predicates_spec.rb +68 -18
  40. data/spec/integration/error_compiler_spec.rb +259 -65
  41. data/spec/integration/hints_spec.rb +28 -9
  42. data/spec/integration/injecting_rules_spec.rb +11 -12
  43. data/spec/integration/localized_error_messages_spec.rb +16 -16
  44. data/spec/integration/messages/i18n_spec.rb +9 -5
  45. data/spec/integration/optional_keys_spec.rb +9 -11
  46. data/spec/integration/schema/array_schema_spec.rb +23 -0
  47. data/spec/integration/schema/check_rules_spec.rb +39 -31
  48. data/spec/integration/schema/check_with_nth_el_spec.rb +25 -0
  49. data/spec/integration/schema/each_with_set_spec.rb +23 -24
  50. data/spec/integration/schema/form_spec.rb +122 -0
  51. data/spec/integration/schema/inheriting_schema_spec.rb +31 -0
  52. data/spec/integration/schema/input_processor_spec.rb +46 -0
  53. data/spec/integration/schema/macros/confirmation_spec.rb +33 -0
  54. data/spec/integration/schema/macros/maybe_spec.rb +32 -0
  55. data/spec/integration/schema/macros/required_spec.rb +59 -0
  56. data/spec/integration/schema/macros/when_spec.rb +65 -0
  57. data/spec/integration/schema/nested_values_spec.rb +41 -0
  58. data/spec/integration/schema/not_spec.rb +14 -14
  59. data/spec/integration/schema/option_with_default_spec.rb +30 -0
  60. data/spec/integration/schema/reusing_schema_spec.rb +33 -0
  61. data/spec/integration/schema/using_types_spec.rb +29 -0
  62. data/spec/integration/schema/xor_spec.rb +17 -14
  63. data/spec/integration/schema_spec.rb +75 -245
  64. data/spec/shared/rule_compiler.rb +8 -0
  65. data/spec/spec_helper.rb +13 -0
  66. data/spec/unit/hint_compiler_spec.rb +10 -10
  67. data/spec/unit/{input_type_compiler_spec.rb → input_processor_compiler/form_spec.rb} +88 -73
  68. data/spec/unit/schema/key_spec.rb +33 -0
  69. data/spec/unit/schema/rule_spec.rb +7 -6
  70. data/spec/unit/schema/value_spec.rb +187 -54
  71. metadata +53 -31
  72. data/.rubocop.yml +0 -16
  73. data/.rubocop_todo.yml +0 -7
  74. data/lib/dry/validation/input_type_compiler.rb +0 -83
  75. data/lib/dry/validation/schema/definition.rb +0 -74
  76. data/lib/dry/validation/schema/result.rb +0 -68
  77. data/rakelib/rubocop.rake +0 -18
  78. data/spec/integration/rule_groups_spec.rb +0 -94
  79. data/spec/integration/schema/attrs_spec.rb +0 -38
  80. data/spec/integration/schema/default_key_behavior_spec.rb +0 -23
  81. data/spec/integration/schema/grouped_rules_spec.rb +0 -57
  82. data/spec/integration/schema/nested_spec.rb +0 -31
  83. data/spec/integration/schema_form_spec.rb +0 -97
@@ -1,39 +1,23 @@
1
1
  module Dry
2
2
  module Validation
3
3
  class Error
4
- class Set
5
- include Enumerable
4
+ include Dry::Equalizer(:name, :result)
6
5
 
7
- attr_reader :errors
6
+ attr_reader :name, :result
8
7
 
9
- def initialize(errors)
10
- @errors = errors
11
- end
12
-
13
- def each(&block)
14
- errors.each(&block)
15
- end
16
-
17
- def empty?
18
- errors.empty?
19
- end
20
-
21
- def to_ary
22
- errors.map { |error| error.to_ary }
23
- end
24
- alias_method :to_a, :to_ary
8
+ def initialize(name, result)
9
+ @name = name
10
+ @result = result
25
11
  end
26
12
 
27
- attr_reader :result
28
-
29
- def initialize(result)
30
- @result = result
13
+ def schema?
14
+ result.response.is_a?(Validation::Result)
31
15
  end
32
16
 
33
- def to_ary
34
- [:error, result.to_ary]
17
+ def to_ast
18
+ node = [:error, [name, result.to_ast]]
19
+ schema? ? [:schema, node] : node
35
20
  end
36
- alias_method :to_a, :to_ary
37
21
  end
38
22
  end
39
23
  end
@@ -1,18 +1,25 @@
1
1
  module Dry
2
2
  module Validation
3
3
  class ErrorCompiler
4
- attr_reader :messages, :options
4
+ attr_reader :messages, :hints, :options
5
5
 
6
6
  DEFAULT_RESULT = {}.freeze
7
+ EMPTY_HINTS = [].freeze
7
8
  KEY_SEPARATOR = '.'.freeze
8
9
 
9
10
  def initialize(messages, options = {})
10
11
  @messages = messages
11
- @options = options
12
+ @options = Hash[options]
13
+ @hints = @options.fetch(:hints, {})
14
+ @full = options.fetch(:full, false)
12
15
  end
13
16
 
14
- def call(ast)
15
- merge(ast.map { |node| visit(node) }) || DEFAULT_RESULT
17
+ def full?
18
+ @full
19
+ end
20
+
21
+ def call(ast, *args)
22
+ merge(ast.map { |node| visit(node, *args) }) || DEFAULT_RESULT
16
23
  end
17
24
 
18
25
  def with(new_options)
@@ -23,135 +30,98 @@ module Dry
23
30
  __send__(:"visit_#{node[0]}", node[1], *args)
24
31
  end
25
32
 
26
- def visit_error(error)
27
- visit(error)
28
- end
29
-
30
- def visit_input(input, *)
31
- name = normalize_name(input[0])
32
- _, value, rules = input
33
- errors = [rules.map { |rule| visit(rule, name, value) }, value]
34
-
35
- if input[0].is_a?(Hash)
36
- root, sub = input[0].to_a.flatten
37
- { root => { sub => errors } }
38
- else
39
- { input[0] => errors }
40
- end
41
- end
42
-
43
- def visit_group(_, name, _)
44
- messages[name, rule: name]
45
- end
46
-
47
- def visit_check(node, *)
48
- name = normalize_name(node[0])
49
- messages[name, rule: name]
50
- end
51
-
52
- def visit_key(rule, name, value)
53
- _, predicate = rule
54
- visit(predicate, value, name)
55
- end
56
-
57
- def visit_attr(rule, name, value)
58
- _, predicate = rule
59
- visit(predicate, value, name)
60
- end
61
-
62
- def visit_val(rule, name, value)
63
- name, predicate = rule
64
- visit(predicate, value, name)
65
- end
66
-
67
- def visit_predicate(predicate, value, name)
68
- predicate_name, args = predicate
69
-
70
- lookup_options = options.merge(
71
- rule: name, val_type: value.class, arg_type: args[0].class
72
- )
73
-
74
- template = messages[predicate_name, lookup_options]
75
- tokens = visit(predicate, value).merge(name: name)
76
-
77
- template % tokens
33
+ def visit_schema(node, *args)
34
+ visit_error(node[1], true)
78
35
  end
79
36
 
80
- def visit_key?(*args, _value)
81
- { name: args[0][0] }
37
+ def visit_set(node, *args)
38
+ call(node, *args)
82
39
  end
83
40
 
84
- def visit_attr?(*args, _value)
85
- { name: args[0][0] }
86
- end
87
-
88
- def visit_exclusion?(*args, _value)
89
- { list: args[0][0].join(', ') }
90
- end
41
+ def visit_error(error, schema = false)
42
+ name, other = error
43
+ message = messages[name]
91
44
 
92
- def visit_inclusion?(*args, _value)
93
- { list: args[0][0].join(', ') }
94
- end
45
+ if message
46
+ { name => [message] }
47
+ else
48
+ result = schema ? visit(other, name) : visit(other)
95
49
 
96
- def visit_gt?(*args, value)
97
- { num: args[0][0], value: value }
50
+ if result.is_a?(Array)
51
+ merge(result)
52
+ else
53
+ merge_hints(result)
54
+ end
55
+ end
98
56
  end
99
57
 
100
- def visit_gteq?(*args, value)
101
- { num: args[0][0], value: value }
58
+ def visit_input(node, path = nil)
59
+ name, result = node
60
+ visit(result, path || name)
102
61
  end
103
62
 
104
- def visit_lt?(*args, value)
105
- { num: args[0][0], value: value }
63
+ def visit_result(node, name = nil)
64
+ value, other = node
65
+ input_visitor(name, value).visit(other)
106
66
  end
107
67
 
108
- def visit_lteq?(*args, value)
109
- { num: args[0][0], value: value }
68
+ def visit_implication(node)
69
+ _, right = node
70
+ visit(right)
110
71
  end
111
72
 
112
- def visit_int?(*args, value)
113
- { num: args[0][0], value: value }
73
+ def visit_key(rule)
74
+ _, predicate = rule
75
+ visit(predicate)
114
76
  end
115
77
 
116
- def visit_max_size?(*args, value)
117
- { num: args[0][0], value: value }
78
+ def visit_attr(rule)
79
+ _, predicate = rule
80
+ visit(predicate)
118
81
  end
119
82
 
120
- def visit_min_size?(*args, value)
121
- { num: args[0][0], value: value }
83
+ def visit_val(node)
84
+ visit(node)
122
85
  end
123
86
 
124
- def visit_eql?(*args, value)
125
- { eql_value: args[0][0], value: value }
126
- end
87
+ private
127
88
 
128
- def visit_size?(*args, value)
129
- num = args[0][0]
89
+ def merge_hints(messages)
90
+ messages.each_with_object({}) do |(name, msgs), res|
91
+ if msgs.is_a?(Hash)
92
+ res[name] = merge_hints(msgs)
93
+ else
94
+ all_msgs = msgs + (hints[name] || EMPTY_HINTS)
95
+ all_msgs.uniq!
130
96
 
131
- if num.is_a?(Range)
132
- { left: num.first, right: num.last, value: value }
133
- else
134
- { num: args[0][0], value: value }
97
+ res[name] = all_msgs
98
+ end
135
99
  end
136
100
  end
137
101
 
138
- private
139
-
140
102
  def normalize_name(name)
141
- Array(name).join(KEY_SEPARATOR).to_sym
103
+ Array(name).join('.').to_sym
142
104
  end
143
105
 
144
106
  def merge(result)
145
- result.reduce do |a, e|
146
- e.merge(a) do |_, l, r|
147
- l.is_a?(Hash) ? l.merge(r) : l + r
107
+ result.reduce { |a, e| deep_merge(a, e) } || DEFAULT_RESULT
108
+ end
109
+
110
+ def deep_merge(left, right)
111
+ left.merge(right) do |_, a, e|
112
+ if a.is_a?(Hash)
113
+ deep_merge(a, e)
114
+ else
115
+ a + e
148
116
  end
149
117
  end
150
118
  end
151
119
 
152
- def method_missing(_meth, *args)
153
- { value: args[1] }
120
+ def input_visitor(name, input)
121
+ Input.new(messages, options.merge(name: name, input: input))
154
122
  end
155
123
  end
156
124
  end
157
125
  end
126
+
127
+ require 'dry/validation/error_compiler/input'
@@ -0,0 +1,148 @@
1
+ module Dry
2
+ module Validation
3
+ class ErrorCompiler::Input < ErrorCompiler
4
+ attr_reader :name, :input, :rule, :val_type
5
+
6
+ def initialize(messages, options)
7
+ super
8
+ @name = options.fetch(:name)
9
+ @input = options.fetch(:input)
10
+ @rule = Array(name).last
11
+ @val_type = input.class
12
+ end
13
+
14
+ def visit_each(node)
15
+ node.map { |el| visit(el) }
16
+ end
17
+
18
+ def visit_set(node)
19
+ result = node.map do |input|
20
+ visit(input)
21
+ end
22
+ merge(result)
23
+ end
24
+
25
+ def visit_el(node)
26
+ idx, el = node
27
+ name = [*Array(name), idx]
28
+ visit(el, name)
29
+ end
30
+
31
+ def visit_check(node)
32
+ _, other = node
33
+ visit(other)
34
+ end
35
+
36
+ def visit_predicate(node)
37
+ predicate, args = node
38
+
39
+ lookup_options = options.merge(
40
+ rule: rule, val_type: val_type, arg_type: args[0].class
41
+ )
42
+
43
+ tokens = options_for(predicate, args)
44
+ template = messages[predicate, lookup_options.merge(tokens)]
45
+
46
+ unless template
47
+ raise MissingMessageError.new("message for #{predicate} was not found")
48
+ end
49
+
50
+ rule_name =
51
+ if rule.is_a?(Symbol)
52
+ messages.rule(rule, lookup_options) || rule
53
+ else
54
+ rule
55
+ end
56
+
57
+ message =
58
+ if full?
59
+ "#{rule_name} #{template % tokens}"
60
+ else
61
+ template % tokens
62
+ end
63
+
64
+ path = [[message], *[tokens[:name], *Array(name).reverse].uniq]
65
+
66
+ path.reduce { |a, e| { e => a } }
67
+ end
68
+
69
+ def options_for_type?(*args)
70
+ { type: args[0][0] }
71
+ end
72
+
73
+ def options_for_key?(*args)
74
+ { name: args[0][0] }
75
+ end
76
+
77
+ def options_for_attr?(*args)
78
+ { name: args[0][0] }
79
+ end
80
+
81
+ def options_for_exclusion?(*args)
82
+ { list: args[0][0].join(', ') }
83
+ end
84
+
85
+ def options_for_inclusion?(*args)
86
+ { list: args[0][0].join(', ') }
87
+ end
88
+
89
+ def options_for_gt?(*args)
90
+ { num: args[0][0], value: input }
91
+ end
92
+
93
+ def options_for_gteq?(*args)
94
+ { num: args[0][0], value: input }
95
+ end
96
+
97
+ def options_for_lt?(*args)
98
+ { num: args[0][0], value: input }
99
+ end
100
+
101
+ def options_for_lteq?(*args)
102
+ { num: args[0][0], value: input }
103
+ end
104
+
105
+ def options_for_int?(*args)
106
+ { num: args[0][0], value: input }
107
+ end
108
+
109
+ def options_for_max_size?(*args)
110
+ { num: args[0][0], value: input }
111
+ end
112
+
113
+ def options_for_min_size?(*args)
114
+ { num: args[0][0], value: input }
115
+ end
116
+
117
+ def options_for_eql?(*args)
118
+ { eql_value: args[0][0], value: input }
119
+ end
120
+
121
+ def options_for_size?(*args)
122
+ num = args[0][0]
123
+
124
+ if num.is_a?(Range)
125
+ { left: num.first, right: num.last, value: input }
126
+ else
127
+ { num: args[0][0], value: input }
128
+ end
129
+ end
130
+
131
+ def options_for(predicate, args)
132
+ meth = :"options_for_#{predicate}"
133
+
134
+ defaults = { name: rule, rule: rule, value: input }
135
+
136
+ if respond_to?(meth)
137
+ defaults.merge!(__send__(meth, args))
138
+ end
139
+
140
+ defaults
141
+ end
142
+
143
+ def input_visitor(new_name, value)
144
+ self.class.new(messages, options.merge(name: [*name, *new_name].uniq, input: value))
145
+ end
146
+ end
147
+ end
148
+ end
@@ -1,14 +1,37 @@
1
- require 'dry/validation/error_compiler'
1
+ require 'dry/validation/error_compiler/input'
2
2
 
3
3
  module Dry
4
4
  module Validation
5
- class HintCompiler < ErrorCompiler
6
- attr_reader :messages, :rules, :options
5
+ class HintCompiler < ErrorCompiler::Input
6
+ include Dry::Equalizer(:messages, :rules, :options)
7
+
8
+ attr_reader :rules, :excluded
9
+
10
+ TYPES = {
11
+ none?: NilClass,
12
+ bool?: TrueClass,
13
+ str?: String,
14
+ int?: Fixnum,
15
+ float?: Float,
16
+ decimal?: BigDecimal,
17
+ date?: Date,
18
+ date_time?: DateTime,
19
+ time?: Time,
20
+ hash?: Hash,
21
+ array?: Array
22
+ }.freeze
23
+
24
+ EXCLUDED = [:none?, :filled?, :key?].freeze
25
+
26
+ def self.cache
27
+ @cache ||= ThreadSafe::Cache.new
28
+ end
7
29
 
8
30
  def initialize(messages, options = {})
9
- @messages = messages
10
- @options = Hash[options]
31
+ super(messages, { name: nil, input: nil }.merge(options))
11
32
  @rules = @options.delete(:rules)
33
+ @excluded = @options.fetch(:excluded, EXCLUDED)
34
+ @val_type = options[:val_type]
12
35
  end
13
36
 
14
37
  def with(new_options)
@@ -16,58 +39,85 @@ module Dry
16
39
  end
17
40
 
18
41
  def call
19
- messages = Hash.new { |h, k| h[k] = [] }
42
+ self.class.cache.fetch_or_store(hash) do
43
+ super(rules)
44
+ end
45
+ end
46
+
47
+ def visit_predicate(node)
48
+ predicate, _ = node
49
+
50
+ val_type = TYPES[predicate]
51
+
52
+ return with(val_type: val_type) if val_type
53
+ return {} if excluded.include?(predicate)
20
54
 
21
- rules.map { |node| visit(node) }.compact.each do |hints|
22
- name, msgs = hints
23
- messages[name].concat(msgs)
55
+ super
56
+ end
57
+
58
+ def visit_set(node)
59
+ result = node.map do |el|
60
+ visit(el)
24
61
  end
62
+ merge(result)
63
+ end
25
64
 
26
- messages
65
+ def visit_each(node)
66
+ visit(node)
27
67
  end
28
68
 
29
69
  def visit_or(node)
30
70
  left, right = node
31
- [visit(left), Array(visit(right)).flatten.compact].compact
71
+ merge([visit(left), visit(right)])
32
72
  end
33
73
 
34
74
  def visit_and(node)
35
75
  left, right = node
36
- [visit(left), Array(visit(right)).flatten.compact].compact
37
- end
38
76
 
39
- def visit_val(node)
40
- name, predicate = node
41
- visit(predicate, name)
42
- end
77
+ result = visit(left)
43
78
 
44
- def visit_predicate(node, name)
45
- predicate_name, args = node
79
+ if result.is_a?(self.class)
80
+ result.visit(right)
81
+ else
82
+ visit(right)
83
+ end
84
+ end
46
85
 
47
- lookup_options = options.merge(rule: name, arg_type: args[0].class)
86
+ def visit_implication(node)
87
+ _, right = node
88
+ visit(right)
89
+ end
48
90
 
49
- template = messages[predicate_name, lookup_options]
50
- predicate_opts = visit(node, args)
91
+ def visit_key(node)
92
+ name, predicate = node
93
+ with(name: Array([*self.name, name])).visit(predicate)
94
+ end
95
+ alias_method :visit_attr, :visit_key
51
96
 
52
- return unless predicate_opts
97
+ def visit_val(node)
98
+ visit(node)
99
+ end
53
100
 
54
- tokens = predicate_opts.merge(name: name)
101
+ def visit_schema(node)
102
+ DEFAULT_RESULT
103
+ end
55
104
 
56
- template % tokens
105
+ def visit_check(node)
106
+ DEFAULT_RESULT
57
107
  end
58
108
 
59
- def visit_key(node)
60
- name, _ = node
61
- name
109
+ def visit_xor(node)
110
+ DEFAULT_RESULT
62
111
  end
63
112
 
64
- def visit_attr(node)
65
- name, _ = node
66
- name
113
+ def visit_not(node)
114
+ DEFAULT_RESULT
67
115
  end
68
116
 
69
- def method_missing(name, *args)
70
- nil
117
+ private
118
+
119
+ def merge(result)
120
+ super(result.reject { |el| el.is_a?(self.class) })
71
121
  end
72
122
  end
73
123
  end