dry-validation 0.1.0 → 1.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +969 -1
  3. data/LICENSE +1 -1
  4. data/README.md +19 -286
  5. data/config/errors.yml +4 -35
  6. data/dry-validation.gemspec +38 -22
  7. data/lib/dry/validation/config.rb +24 -0
  8. data/lib/dry/validation/constants.rb +43 -0
  9. data/lib/dry/validation/contract/class_interface.rb +230 -0
  10. data/lib/dry/validation/contract.rb +173 -0
  11. data/lib/dry/validation/evaluator.rb +233 -0
  12. data/lib/dry/validation/extensions/hints.rb +67 -0
  13. data/lib/dry/validation/extensions/monads.rb +34 -0
  14. data/lib/dry/validation/extensions/predicates_as_macros.rb +75 -0
  15. data/lib/dry/validation/failures.rb +70 -0
  16. data/lib/dry/validation/function.rb +43 -0
  17. data/lib/dry/validation/macro.rb +38 -0
  18. data/lib/dry/validation/macros.rb +104 -0
  19. data/lib/dry/validation/message.rb +100 -0
  20. data/lib/dry/validation/message_set.rb +97 -0
  21. data/lib/dry/validation/messages/resolver.rb +129 -0
  22. data/lib/dry/validation/result.rb +206 -38
  23. data/lib/dry/validation/rule.rb +116 -106
  24. data/lib/dry/validation/schema_ext.rb +19 -0
  25. data/lib/dry/validation/values.rb +108 -0
  26. data/lib/dry/validation/version.rb +3 -1
  27. data/lib/dry/validation.rb +55 -7
  28. data/lib/dry-validation.rb +3 -1
  29. metadata +80 -106
  30. data/.gitignore +0 -8
  31. data/.rspec +0 -3
  32. data/.rubocop.yml +0 -16
  33. data/.rubocop_todo.yml +0 -7
  34. data/.travis.yml +0 -29
  35. data/Gemfile +0 -11
  36. data/Rakefile +0 -12
  37. data/examples/basic.rb +0 -21
  38. data/examples/nested.rb +0 -30
  39. data/examples/rule_ast.rb +0 -33
  40. data/lib/dry/validation/error.rb +0 -43
  41. data/lib/dry/validation/error_compiler.rb +0 -116
  42. data/lib/dry/validation/messages.rb +0 -71
  43. data/lib/dry/validation/predicate.rb +0 -39
  44. data/lib/dry/validation/predicate_set.rb +0 -22
  45. data/lib/dry/validation/predicates.rb +0 -88
  46. data/lib/dry/validation/rule_compiler.rb +0 -57
  47. data/lib/dry/validation/schema/definition.rb +0 -15
  48. data/lib/dry/validation/schema/key.rb +0 -39
  49. data/lib/dry/validation/schema/rule.rb +0 -28
  50. data/lib/dry/validation/schema/value.rb +0 -31
  51. data/lib/dry/validation/schema.rb +0 -74
  52. data/rakelib/rubocop.rake +0 -18
  53. data/spec/fixtures/errors.yml +0 -4
  54. data/spec/integration/custom_error_messages_spec.rb +0 -35
  55. data/spec/integration/custom_predicates_spec.rb +0 -57
  56. data/spec/integration/validation_spec.rb +0 -118
  57. data/spec/shared/predicates.rb +0 -31
  58. data/spec/spec_helper.rb +0 -18
  59. data/spec/unit/error_compiler_spec.rb +0 -165
  60. data/spec/unit/predicate_spec.rb +0 -37
  61. data/spec/unit/predicates/empty_spec.rb +0 -38
  62. data/spec/unit/predicates/eql_spec.rb +0 -21
  63. data/spec/unit/predicates/exclusion_spec.rb +0 -35
  64. data/spec/unit/predicates/filled_spec.rb +0 -38
  65. data/spec/unit/predicates/format_spec.rb +0 -21
  66. data/spec/unit/predicates/gt_spec.rb +0 -40
  67. data/spec/unit/predicates/gteq_spec.rb +0 -40
  68. data/spec/unit/predicates/inclusion_spec.rb +0 -35
  69. data/spec/unit/predicates/int_spec.rb +0 -34
  70. data/spec/unit/predicates/key_spec.rb +0 -29
  71. data/spec/unit/predicates/lt_spec.rb +0 -40
  72. data/spec/unit/predicates/lteq_spec.rb +0 -40
  73. data/spec/unit/predicates/max_size_spec.rb +0 -49
  74. data/spec/unit/predicates/min_size_spec.rb +0 -49
  75. data/spec/unit/predicates/nil_spec.rb +0 -28
  76. data/spec/unit/predicates/size_spec.rb +0 -49
  77. data/spec/unit/predicates/str_spec.rb +0 -32
  78. data/spec/unit/rule/each_spec.rb +0 -20
  79. data/spec/unit/rule/key_spec.rb +0 -27
  80. data/spec/unit/rule/set_spec.rb +0 -32
  81. data/spec/unit/rule/value_spec.rb +0 -42
  82. data/spec/unit/rule_compiler_spec.rb +0 -86
@@ -1,63 +1,231 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent/map"
4
+ require "dry/core/equalizer"
5
+
6
+ require "dry/validation/constants"
7
+ require "dry/validation/message_set"
8
+ require "dry/validation/values"
9
+
1
10
  module Dry
2
11
  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)
12
+ # Result objects are returned by contracts
13
+ #
14
+ # @api public
15
+ class Result
16
+ include Dry::Equalizer(:schema_result, :context, :errors, inspect: false)
17
+
18
+ # Build a new result
19
+ #
20
+ # @param [Dry::Schema::Result] schema_result
21
+ #
22
+ # @api private
23
+ def self.new(schema_result, context = ::Concurrent::Map.new, options = EMPTY_HASH)
24
+ result = super
25
+ yield(result) if block_given?
26
+ result.freeze
7
27
  end
8
- end
9
28
 
10
- class Result
11
- include Dry::Equalizer(:success?, :input, :rule)
29
+ # Context that's shared between rules
30
+ #
31
+ # @return [Concurrent::Map]
32
+ #
33
+ # @api public
34
+ attr_reader :context
12
35
 
13
- attr_reader :input, :value, :rule
36
+ # Result from contract's schema
37
+ #
38
+ # @return [Dry::Schema::Result]
39
+ #
40
+ # @api private
41
+ attr_reader :schema_result
14
42
 
15
- class Set < Result
16
- def success?
17
- value.all?(&:success?)
18
- end
43
+ # Result options
44
+ #
45
+ # @return [Hash]
46
+ #
47
+ # @api private
48
+ attr_reader :options
19
49
 
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
50
+ # Initialize a new result
51
+ #
52
+ # @api private
53
+ def initialize(schema_result, context, options)
54
+ @schema_result = schema_result
55
+ @context = context
56
+ @options = options
57
+ @errors = initialize_errors
24
58
  end
25
59
 
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
60
+ # Return values wrapper with the input processed by schema
61
+ #
62
+ # @return [Values]
63
+ #
64
+ # @api public
65
+ def values
66
+ @values ||= Values.new(schema_result.to_h)
67
+ end
68
+
69
+ # Get error set
70
+ #
71
+ # @!macro errors-options
72
+ # @param [Hash] new_options
73
+ # @option new_options [Symbol] :locale Set locale for messages
74
+ # @option new_options [Boolean] :hints Enable/disable hints
75
+ # @option new_options [Boolean] :full Get messages that include key names
76
+ #
77
+ # @return [MessageSet]
78
+ #
79
+ # @api public
80
+ def errors(new_options = EMPTY_HASH)
81
+ new_options.empty? ? @errors : @errors.with(schema_errors(new_options), new_options)
82
+ end
83
+
84
+ # Check if result is successful
85
+ #
86
+ # @return [Bool]
87
+ #
88
+ # @api public
89
+ def success?
90
+ @errors.empty?
91
+ end
92
+
93
+ # Check if result is not successful
94
+ #
95
+ # @return [Bool]
96
+ #
97
+ # @api public
98
+ def failure?
99
+ !success?
100
+ end
101
+
102
+ # Check if values include an error for the provided key
103
+ #
104
+ # @api public
105
+ def error?(key)
106
+ errors.any? { |msg| Schema::Path[msg.path].include?(Schema::Path[key]) }
107
+ end
108
+
109
+ # Check if the base schema (without rules) includes an error for the provided key
110
+ #
111
+ # @api private
112
+ def schema_error?(key)
113
+ schema_result.error?(key)
31
114
  end
32
115
 
33
- def initialize(input, value, rule)
34
- @input = input
35
- @value = value
36
- @rule = rule
116
+ # Check if the rules includes an error for the provided key
117
+ #
118
+ # @api private
119
+ def rule_error?(key)
120
+ !schema_error?(key) && error?(key)
37
121
  end
38
122
 
39
- def and(other)
40
- if success?
41
- other.(input)
123
+ # Check if the result contains any base rule errors
124
+ #
125
+ # @api private
126
+ def base_rule_error?
127
+ !errors.filter(:base?).empty?
128
+ end
129
+
130
+ # Check if there's any error for the provided key
131
+ #
132
+ # This does not consider errors from the nested values
133
+ #
134
+ # @api private
135
+ def base_error?(key)
136
+ schema_result.errors.any? { |error|
137
+ key_path = Schema::Path[key]
138
+ err_path = Schema::Path[error.path]
139
+
140
+ next unless key_path.same_root?(err_path)
141
+
142
+ key_path == err_path
143
+ }
144
+ end
145
+
146
+ # Add a new error for the provided key
147
+ #
148
+ # @api private
149
+ def add_error(error)
150
+ @errors.add(error)
151
+ self
152
+ end
153
+
154
+ # Read a value under provided key
155
+ #
156
+ # @param [Symbol] key
157
+ #
158
+ # @return [Object]
159
+ #
160
+ # @api public
161
+ def [](key)
162
+ values[key]
163
+ end
164
+
165
+ # Check if a key was set
166
+ #
167
+ # @param [Symbol] key
168
+ #
169
+ # @return [Bool]
170
+ #
171
+ # @api public
172
+ def key?(key)
173
+ values.key?(key)
174
+ end
175
+
176
+ # Coerce to a hash
177
+ #
178
+ # @api public
179
+ def to_h
180
+ values.to_h
181
+ end
182
+
183
+ # Return a string representation
184
+ #
185
+ # @api public
186
+ def inspect
187
+ if context.empty?
188
+ "#<#{self.class}#{to_h} errors=#{errors.to_h}>"
42
189
  else
43
- self
190
+ "#<#{self.class}#{to_h} errors=#{errors.to_h} context=#{context.each.to_h}>"
44
191
  end
45
192
  end
46
193
 
47
- def or(other)
48
- if success?
49
- self
50
- else
51
- other.(input)
194
+ # Freeze result and its error set
195
+ #
196
+ # @api private
197
+ def freeze
198
+ values.freeze
199
+ errors.freeze
200
+ super
201
+ end
202
+
203
+ if RUBY_VERSION >= "2.7"
204
+ # Pattern matching
205
+ #
206
+ # @api private
207
+ def deconstruct_keys(keys)
208
+ values.deconstruct_keys(keys)
209
+ end
210
+
211
+ # Pattern matching
212
+ #
213
+ # @api private
214
+ def deconstruct
215
+ [values, context.each.to_h]
52
216
  end
53
217
  end
54
218
 
55
- def success?
56
- @value
219
+ private
220
+
221
+ # @api private
222
+ def initialize_errors(options = self.options)
223
+ MessageSet.new(schema_errors(options), options)
57
224
  end
58
225
 
59
- def failure?
60
- ! success?
226
+ # @api private
227
+ def schema_errors(options)
228
+ schema_result.message_set(options).to_a
61
229
  end
62
230
  end
63
231
  end
@@ -1,124 +1,134 @@
1
- require 'dry/validation/result'
1
+ # frozen_string_literal: true
2
2
 
3
- module Dry
4
- module Validation
5
- class Rule
6
- include Dry::Equalizer(:name, :predicate)
7
-
8
- class Key < Rule
9
- def self.new(name, predicate)
10
- super(name, predicate.curry(name))
11
- end
3
+ require "dry/core/equalizer"
12
4
 
13
- def type
14
- :key
15
- end
5
+ require "dry/validation/constants"
6
+ require "dry/validation/function"
16
7
 
17
- def call(input)
18
- Validation.Result(input[name], predicate.(input), self)
19
- end
8
+ module Dry
9
+ module Validation
10
+ # Rules capture configuration and evaluator blocks
11
+ #
12
+ # When a rule is applied, it creates an `Evaluator` using schema result and its
13
+ # block will be evaluated in the context of the evaluator.
14
+ #
15
+ # @see Contract#rule
16
+ #
17
+ # @api public
18
+ class Rule < Function
19
+ include Dry::Equalizer(:keys, :block, inspect: false)
20
+
21
+ # @!attribute [r] keys
22
+ # @return [Array<Symbol, String, Hash>]
23
+ # @api private
24
+ option :keys
25
+
26
+ # @!attribute [r] macros
27
+ # @return [Array<Symbol>]
28
+ # @api private
29
+ option :macros, default: proc { EMPTY_ARRAY.dup }
30
+
31
+ # Evaluate the rule within the provided context
32
+ #
33
+ # @param [Contract] contract
34
+ # @param [Result] result
35
+ #
36
+ # @api private
37
+ def call(contract, result)
38
+ Evaluator.new(
39
+ contract,
40
+ keys: keys,
41
+ macros: macros,
42
+ block_options: block_options,
43
+ result: result,
44
+ values: result.values,
45
+ _context: result.context,
46
+ &block
47
+ )
20
48
  end
21
49
 
22
- class Value < Rule
23
- def call(input)
24
- Validation.Result(input, predicate.(input), self)
25
- end
26
-
27
- def type
28
- :val
29
- end
50
+ # Define which macros should be executed
51
+ #
52
+ # @see Contract#rule
53
+ # @return [Rule]
54
+ #
55
+ # @api public
56
+ def validate(*macros, &block)
57
+ @macros = parse_macros(*macros)
58
+ @block = block if block
59
+ self
30
60
  end
31
61
 
32
- class Composite
33
- include Dry::Equalizer(:left, :right)
34
-
35
- attr_reader :name, :left, :right
36
-
37
- def initialize(left, right)
38
- @name = left.name
39
- @left = left
40
- @right = right
41
- end
42
-
43
- def to_ary
44
- [type, left.to_ary, [right.to_ary]]
45
- end
46
- alias_method :to_a, :to_ary
62
+ # Define a validation function for each element of an array
63
+ #
64
+ # The function will be applied only if schema checks passed
65
+ # for a given array item.
66
+ #
67
+ # @example
68
+ # rule(:nums).each do |index:|
69
+ # key([:number, index]).failure("must be greater than 0") if value < 0
70
+ # end
71
+ # rule(:nums).each(min: 3)
72
+ # rule(address: :city) do
73
+ # key.failure("oops") if value != 'Munich'
74
+ # end
75
+ #
76
+ # @return [Rule]
77
+ #
78
+ # @api public
79
+ def each(*macros, &block)
80
+ root = keys[0]
81
+ macros = parse_macros(*macros)
82
+ @keys = []
83
+
84
+ @block = proc do
85
+ unless result.base_error?(root) || !values.key?(root)
86
+ values[root].each_with_index do |_, idx|
87
+ path = [*Schema::Path[root].to_a, idx]
88
+
89
+ next if result.schema_error?(path)
90
+
91
+ evaluator = with(macros: macros, keys: [path], index: idx, &block)
92
+
93
+ failures.concat(evaluator.failures)
94
+ end
95
+ end
96
+ end
97
+
98
+ @block_options = map_keywords(block) if block
99
+
100
+ self
47
101
  end
48
102
 
49
- class Conjunction < Composite
50
- def call(input)
51
- left.(input).and(right)
52
- end
53
-
54
- def type
55
- :and
56
- end
103
+ # Return a nice string representation
104
+ #
105
+ # @return [String]
106
+ #
107
+ # @api public
108
+ def inspect
109
+ %(#<#{self.class} keys=#{keys.inspect}>)
57
110
  end
58
111
 
59
- class Disjunction < Composite
60
- def call(input)
61
- left.(input).or(right)
62
- end
63
-
64
- def type
65
- :or
112
+ # Parse function arguments into macros structure
113
+ #
114
+ # @return [Array]
115
+ #
116
+ # @api private
117
+ def parse_macros(*args)
118
+ args.each_with_object([]) do |spec, macros|
119
+ case spec
120
+ when Hash
121
+ add_macro_from_hash(macros, spec)
122
+ else
123
+ macros << Array(spec)
124
+ end
66
125
  end
67
126
  end
68
127
 
69
- class Each < Rule
70
- def call(input)
71
- Validation.Result(input, input.map { |element| predicate.(element) }, self)
128
+ def add_macro_from_hash(macros, spec)
129
+ spec.each do |k, v|
130
+ macros << [k, v.is_a?(Array) ? v : [v]]
72
131
  end
73
-
74
- def type
75
- :each
76
- end
77
- end
78
-
79
- class Set < Rule
80
- def call(input)
81
- Validation.Result(input, predicate.map { |rule| rule.(input) }, self)
82
- end
83
-
84
- def type
85
- :set
86
- end
87
-
88
- def at(*args)
89
- self.class.new(name, predicate.values_at(*args))
90
- end
91
-
92
- def to_ary
93
- [type, [name, predicate.map(&:to_ary)]]
94
- end
95
- alias_method :to_a, :to_ary
96
- end
97
-
98
- attr_reader :name, :predicate
99
-
100
- def initialize(name, predicate)
101
- @name = name
102
- @predicate = predicate
103
- end
104
-
105
- def to_ary
106
- [type, [name, predicate.to_ary]]
107
- end
108
- alias_method :to_a, :to_ary
109
-
110
- def and(other)
111
- Conjunction.new(self, other)
112
- end
113
- alias_method :&, :and
114
-
115
- def or(other)
116
- Disjunction.new(self, other)
117
- end
118
- alias_method :|, :or
119
-
120
- def curry(*args)
121
- self.class.new(name, predicate.curry(*args))
122
132
  end
123
133
  end
124
134
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/schema/path"
4
+
5
+ module Dry
6
+ module Schema
7
+ class Path
8
+ # @api private
9
+ def multi_value?
10
+ last.is_a?(Array)
11
+ end
12
+
13
+ # @api private
14
+ def expand
15
+ to_a[0..-2].product(last).map { |spec| self.class[spec] }
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/core/equalizer"
4
+ require "dry/schema/path"
5
+ require "dry/validation/constants"
6
+
7
+ module Dry
8
+ module Validation
9
+ # A convenient wrapper for data processed by schemas
10
+ #
11
+ # Values are available within the rule blocks. They act as hash-like
12
+ # objects and expose a convenient API for accessing data.
13
+ #
14
+ # @api public
15
+ class Values
16
+ include Enumerable
17
+ include Dry::Equalizer(:data)
18
+
19
+ # Schema's result output
20
+ #
21
+ # @return [Hash]
22
+ #
23
+ # @api private
24
+ attr_reader :data
25
+
26
+ # @api private
27
+ def initialize(data)
28
+ @data = data
29
+ end
30
+
31
+ # Read from the provided key
32
+ #
33
+ # @example
34
+ # rule(:age) do
35
+ # key.failure('must be > 18') if values[:age] <= 18
36
+ # end
37
+ #
38
+ # @param args [Symbol, String, Hash, Array<Symbol>] If given as a single
39
+ # Symbol, String, Array or Hash, build a key array using
40
+ # {Dry::Schema::Path} digging for data. If given as positional
41
+ # arguments, use these with Hash#dig on the data directly.
42
+ #
43
+ # @return [Object]
44
+ #
45
+ # @api public
46
+ def [](*args)
47
+ return data.dig(*args) if args.size > 1
48
+
49
+ case (key = args[0])
50
+ when Symbol, String, Array, Hash
51
+ keys = Schema::Path[key].to_a
52
+
53
+ return data.dig(*keys) unless keys.last.is_a?(Array)
54
+
55
+ last = keys.pop
56
+ vals = self.class.new(data.dig(*keys))
57
+ vals.fetch_values(*last) { nil }
58
+ else
59
+ raise ArgumentError, "+key+ must be a valid path specification"
60
+ end
61
+ end
62
+
63
+ # @api public
64
+ # rubocop: disable Metrics/PerceivedComplexity
65
+ def key?(key, hash = data)
66
+ return hash.key?(key) if key.is_a?(Symbol)
67
+
68
+ # rubocop: disable Lint/DuplicateBranch
69
+ Schema::Path[key].reduce(hash) do |a, e|
70
+ if e.is_a?(Array)
71
+ result = e.all? { |k| key?(k, a) }
72
+ return result
73
+ elsif e.is_a?(Symbol) && a.is_a?(Array)
74
+ return false
75
+ elsif a.nil?
76
+ return false
77
+ elsif a.is_a?(String)
78
+ return false
79
+ else
80
+ return false unless a.is_a?(Array) ? (e >= 0 && e < a.size) : a.key?(e)
81
+ end
82
+ a[e]
83
+ end
84
+ # rubocop: enable Lint/DuplicateBranch
85
+
86
+ true
87
+ end
88
+ # rubocop: enable Metrics/PerceivedComplexity
89
+
90
+ # @api private
91
+ def respond_to_missing?(meth, include_private = false)
92
+ super || data.respond_to?(meth, include_private)
93
+ end
94
+
95
+ private
96
+
97
+ # @api private
98
+ def method_missing(meth, *args, &block)
99
+ if data.respond_to?(meth)
100
+ data.public_send(meth, *args, &block)
101
+ else
102
+ super
103
+ end
104
+ end
105
+ ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
106
+ end
107
+ end
108
+ end
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Dry
2
4
  module Validation
3
- VERSION = '0.1.0'.freeze
5
+ VERSION = "1.8.0"
4
6
  end
5
7
  end