dry-validation 0.1.0 → 1.8.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 (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