dry-validation 0.6.0 → 0.7.0

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