validate-rb 0.1.0.alpha.1

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.
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Validate
4
+ module Compare
5
+ class ByAttributes
6
+ include Comparable
7
+
8
+ def initialize(attributes)
9
+ @attributes = attributes
10
+ end
11
+
12
+ def <=>(other)
13
+ @attributes.map { |attribute, value| value <=> other.send(attribute) }
14
+ .find { |result| !result.zero? } || 0
15
+ end
16
+
17
+ def method_missing(symbol, *args)
18
+ return super unless args.empty? && respond_to_missing?(symbol)
19
+
20
+ @attributes[symbol]
21
+ end
22
+
23
+ def respond_to_missing?(attribute, _ = false)
24
+ @attributes.include?(attribute)
25
+ end
26
+ end
27
+
28
+ module_function
29
+
30
+ def attributes(**attributes)
31
+ ByAttributes.new(attributes)
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,217 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Validate
5
+ class Constraint
6
+ class Violation
7
+ attr_reader :value, :path, :constraint
8
+
9
+ def initialize(value, path, constraint)
10
+ @value = value
11
+ @path = path
12
+ @constraint = constraint
13
+ end
14
+
15
+ def message(template = @constraint.message)
16
+ (template % parameters).strip
17
+ end
18
+
19
+ alias to_s message
20
+
21
+ private
22
+
23
+ def parameters
24
+ @parameters ||= Hash.new do |_, key|
25
+ String(instance_eval(key.to_s))
26
+ end
27
+ end
28
+ end
29
+
30
+ class Option
31
+ attr_reader :name
32
+
33
+ def initialize(name, default:, assertion: nil, &assert_block)
34
+ @name = name
35
+ @default = default.is_a?(Proc) ? default : -> { default }
36
+ @assertion = assertion || assert_block && Assertions.create(&assert_block)
37
+ end
38
+
39
+ def replace_default(default)
40
+ Option.new(@name, default: default, assertion: @assertion)
41
+ end
42
+
43
+ def get_or_default(options)
44
+ value = options.delete(@name) { return @default&.call }
45
+ @assertion&.assert(value, message: "invalid option #{@name}")
46
+ value
47
+ end
48
+ end
49
+
50
+ def self.inherited(child)
51
+ child.extend DSL
52
+ end
53
+
54
+ def self.create_class(name, **defaults, &constraint_block)
55
+ Class.new(self) do
56
+ @supported_options = common_options.transform_values do |option|
57
+ defaults.include?(option.name) ? option.replace_default(defaults[option.name]) : option
58
+ end
59
+ include(@constraint_user_methods = Module.new)
60
+ @constraint_user_methods.define_method(:name) { name.to_s }
61
+ class_eval(&constraint_block)
62
+ if instance_variable_defined?(:@supported_options)
63
+ initialize { |**options| options }
64
+ end
65
+ end
66
+ end
67
+
68
+ module DSL
69
+ def constraint_name
70
+ @constraint_name ||= Assertions.create do
71
+ not_nil(message: 'constraint name must not be nil')
72
+ is_a(Symbol, message: 'constraint name must be a Symbol')
73
+ end
74
+ end
75
+ module_function :constraint_name
76
+
77
+ def common_options
78
+ @common_options ||= {
79
+ message: Option.new(:message, default: 'be %{constraint.name}') do
80
+ not_blank
81
+ is_a(String)
82
+ end
83
+ }.freeze
84
+ end
85
+ module_function :common_options
86
+
87
+ def option(
88
+ name,
89
+ default: lambda do
90
+ raise Error::KeyError,
91
+ "option #{name.inspect} is required for #{self.name}"
92
+ end,
93
+ &assert_block
94
+ )
95
+ constraint_name.assert(name)
96
+ if @supported_options.include?(name)
97
+ raise Error::ArgumentError, "duplicate option :#{name}"
98
+ end
99
+
100
+ @supported_options[name] = Option.new(
101
+ name,
102
+ default: default,
103
+ &assert_block
104
+ )
105
+ self
106
+ end
107
+
108
+ def initialize(&initialize_block)
109
+ supported_options = @supported_options
110
+ expects_kwargs = false
111
+ initialize_block.parameters.each do |(kind, name)|
112
+ if %i(keyreq key).include?(kind) && supported_options.include?(name)
113
+ raise Error::ArgumentError,
114
+ "key name #{name}: conflicts with an existing option"
115
+ end
116
+ expects_kwargs = true if kind == :keyrest
117
+ end
118
+
119
+ define_constraint_method(:initialize, initialize_block) do |*args, **kwargs, &block|
120
+ known_options, extra_kwargs =
121
+ kwargs.partition { |k, _| supported_options.include?(k) }
122
+ .map { |h| Hash[h] }
123
+ args << extra_kwargs if expects_kwargs || !extra_kwargs.empty?
124
+
125
+ merged_options = {}.merge!(super(*args, &block), known_options)
126
+
127
+ options = supported_options.each_with_object({}) do |(n, opt), opts|
128
+ opts[n] = opt.get_or_default(merged_options)
129
+ end
130
+
131
+ unless merged_options.empty?
132
+ raise Error::ArgumentError,
133
+ "undefined options #{merged_options.inspect}"
134
+ end
135
+
136
+ @options = options.freeze
137
+ end
138
+ remove_instance_variable(:@supported_options)
139
+ end
140
+
141
+ def evaluate(&validation_block)
142
+ define_constraint_method(:valid?, validation_block) do |*args|
143
+ catch(:result) do
144
+ super(*args[0...validation_block.arity])
145
+ :pass
146
+ end == :pass
147
+ end
148
+ self
149
+ end
150
+
151
+ def describe(&describe_block)
152
+ define_method(:to_s, &describe_block)
153
+ self
154
+ end
155
+
156
+ def key(&key_block)
157
+ define_method(:name, &key_block)
158
+ self
159
+ end
160
+
161
+ private
162
+
163
+ def define_constraint_method(name, body, &override)
164
+ @constraint_user_methods.__send__(:define_method, name, &body)
165
+ define_method(name, &override)
166
+ self
167
+ end
168
+ end
169
+
170
+ attr_reader :options
171
+ protected :options
172
+
173
+ def initialize(**options)
174
+ @options = options
175
+ end
176
+
177
+ def name
178
+ raise ::NotImplementedError
179
+ end
180
+
181
+ def valid?(value, ctx = Constraints::ValidationContext.none)
182
+ raise ::NotImplementedError
183
+ end
184
+
185
+ def to_s
186
+ name.to_s.gsub('_', ' ')
187
+ end
188
+
189
+ def inspect
190
+ "#<#{self.class.name} #{@options.map { |name, value| "#{name}: #{value.inspect}" }.join(', ')}>"
191
+ end
192
+
193
+ def ==(other)
194
+ other.is_a?(Constraint) && other.name == name && other.options == options
195
+ end
196
+
197
+ def respond_to_missing?(method, _ = false)
198
+ @options.include?(method)
199
+ end
200
+
201
+ def method_missing(method, *args)
202
+ return super unless args.empty? || respond_to_missing?(method)
203
+
204
+ @options[method]
205
+ end
206
+
207
+ private
208
+
209
+ def fail
210
+ throw(:result, :fail)
211
+ end
212
+
213
+ def pass
214
+ throw(:result, :pass)
215
+ end
216
+ end
217
+ end
@@ -0,0 +1,337 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Validate
4
+ module Constraints
5
+ include Validate::Arguments
6
+
7
+ @reserved_names = Hash[%i[define validation_context].map { |n| [n, n] }]
8
+
9
+ arg(:name) do
10
+ not_blank(message: 'constraint name must not be blank')
11
+ is_a(Symbol, message: 'constraint name must be a Symbol')
12
+ end
13
+ arg(:body) do
14
+ not_nil(message: 'constraint body is required')
15
+ end
16
+ def self.define(name, **defaults, &body)
17
+ if @reserved_names.include?(name)
18
+ raise Error::ArgumentError,
19
+ "#{name} is already defined"
20
+ end
21
+
22
+ @reserved_names[name] = name
23
+ constraint_class = Constraint.create_class(name, defaults, &body)
24
+ Constraints.const_set(Helpers.camelize(name), constraint_class)
25
+ define_method(name, &constraint_class.method(:new))
26
+ module_function(name)
27
+ end
28
+
29
+ define(:not_nil, message: 'not be nil') do
30
+ evaluate { |value| fail if value.nil? }
31
+ end
32
+
33
+ define(:not_blank, message: 'not be blank') do
34
+ evaluate do |value|
35
+ fail if value.nil? || !value.respond_to?(:empty?) || value.empty?
36
+ end
37
+ end
38
+
39
+ define(:not_empty, message: 'not be empty') do
40
+ evaluate { |value| fail if value&.empty? }
41
+ end
42
+
43
+ define(:is_a, message: 'be a %{constraint.klass}') do
44
+ option(:klass) do
45
+ not_nil(message: 'klass is required')
46
+ end
47
+
48
+ initialize { |klass| { klass: klass } }
49
+ evaluate do |value|
50
+ pass if value.nil?
51
+
52
+ klass = options[:klass]
53
+ fail unless klass === value
54
+ end
55
+ end
56
+
57
+ define(:respond_to, message: 'respond to %{constraint.method_name.inspect}') do
58
+ option(:method_name) do
59
+ not_blank(message: 'method_name is required')
60
+ is_a(Symbol, message: 'method_name must be a Symbol')
61
+ end
62
+
63
+ initialize do |method_name|
64
+ method_name.nil? ? {} : { method_name: method_name }
65
+ end
66
+ evaluate do |instance|
67
+ pass if instance.nil?
68
+ fail unless instance.respond_to?(options[:method_name])
69
+ end
70
+ key { "respond_to_#{options[:method_name]}" }
71
+ end
72
+
73
+ define(:length, message: 'have length of %{constraint.describe_length}') do
74
+ option(:min) { respond_to(:>, message: 'min must respond to :>') }
75
+ option(:max) { respond_to(:<, message: 'max must respond to :<') }
76
+
77
+ initialize do |range = nil|
78
+ case range
79
+ when ::Range
80
+ { min: range.min, max: range.max }
81
+ else
82
+ { min: range, max: range }
83
+ end
84
+ end
85
+ evaluate do |value|
86
+ pass if value.nil?
87
+ fail unless value.respond_to?(:length)
88
+
89
+ length = value.length
90
+ fail if (options[:min]&.> length) || (options[:max]&.< length)
91
+ end
92
+
93
+ def describe_length
94
+ if options[:max] == options[:min]
95
+ options[:max].to_s
96
+ elsif options[:max].nil?
97
+ "at least #{options[:min]}"
98
+ elsif options[:min].nil?
99
+ "at most #{options[:max]}"
100
+ else
101
+ "at least #{options[:min]} and at most #{options[:max]}"
102
+ end
103
+ end
104
+ key { "length_over_#{options[:min]}_under_#{options[:max]}" }
105
+ end
106
+
107
+ define(:one_of, message: 'be %{constraint.describe_presence}') do
108
+ option(:values) do
109
+ respond_to(:include?, message: 'values must respond to :include?')
110
+ end
111
+
112
+ initialize do |*values|
113
+ if values.one? && values.first.respond_to?(:include?)
114
+ { values: values.first }
115
+ else
116
+ { values: values }
117
+ end
118
+ end
119
+ evaluate do |value|
120
+ pass if value.nil?
121
+
122
+ values = options[:values]
123
+ pass if values.respond_to?(:cover?) && values.cover?(value)
124
+ fail unless values.include?(value)
125
+ end
126
+
127
+ def describe_presence
128
+ case options[:values]
129
+ when ::Hash
130
+ "one of #{options[:values].keys}"
131
+ when ::Range
132
+ "covered by #{options[:values]}"
133
+ else
134
+ "one of #{options[:values]}"
135
+ end
136
+ end
137
+ end
138
+
139
+ define(:validate, message: 'pass validation') do
140
+ option(:using) do
141
+ not_nil(message: 'using is required')
142
+ is_a(Proc, message: 'using must be a Proc')
143
+ end
144
+
145
+ initialize { |&validate_block| { using: validate_block } }
146
+ evaluate do |value|
147
+ pass if value.nil?
148
+ fail unless instance_exec(value, &options[:using])
149
+ end
150
+ end
151
+
152
+ define(:attr, message: 'have attribute %{constraint.attribute}') do
153
+ option(:attribute) do
154
+ not_nil(message: 'attribute is required')
155
+ is_a(Symbol, message: 'attribute must be a Symbol')
156
+ end
157
+ option(:constraints) do
158
+ is_a(AST::DefinitionContext, message: 'constraints must be a DefinitionContext')
159
+ end
160
+
161
+ initialize do |attribute, &block|
162
+ { attribute: attribute,
163
+ constraints: block && AST::DefinitionContext.create(&block) }
164
+ end
165
+ evaluate do |value, ctx|
166
+ pass if value.nil?
167
+
168
+ attribute = options[:attribute]
169
+ begin
170
+ options[:constraints].evaluate(ctx.attr(attribute))
171
+ rescue NameError
172
+ fail
173
+ end
174
+ end
175
+ key { "attr_#{options[:attribute]}" }
176
+ end
177
+
178
+ define(:key, message: 'have key %{constraint.key.inspect}') do
179
+ option(:key) do
180
+ not_nil(message: 'key is required')
181
+ end
182
+ option(:constraints) do
183
+ is_a(AST::DefinitionContext, message: 'constraints must be a DefinitionContext')
184
+ end
185
+
186
+ initialize do |key, &block|
187
+ { key: key,
188
+ constraints: block && AST::DefinitionContext.create(&block) }
189
+ end
190
+
191
+ evaluate do |instance, ctx|
192
+ pass if instance.nil?
193
+ fail unless instance.respond_to?(:[])
194
+
195
+ key = options[:key]
196
+ begin
197
+ options[:constraints]&.evaluate(ctx[key])
198
+ rescue KeyError
199
+ fail
200
+ end
201
+ end
202
+ key { "key_#{options[:key]}" }
203
+ end
204
+
205
+ define(:min, message: 'be at least %{constraint.min}') do
206
+ option(:min) do
207
+ not_nil(message: 'min is required')
208
+ respond_to(:>, message: 'min must respond to :>')
209
+ end
210
+
211
+ initialize do |min = nil|
212
+ min.nil? ? {} : { min: min }
213
+ end
214
+ evaluate do |value|
215
+ pass if value.nil?
216
+ fail if options[:min] > value
217
+ end
218
+ end
219
+
220
+ define(:max, message: 'be at most %{constraint.max}') do
221
+ option(:max) do
222
+ not_nil(message: 'max is required')
223
+ respond_to(:<, message: 'max must respond to :<')
224
+ end
225
+
226
+ initialize do |max = nil|
227
+ max.nil? ? {} : { max: max }
228
+ end
229
+ evaluate do |value|
230
+ pass if value.nil?
231
+ fail if options[:max] < value
232
+ end
233
+ end
234
+
235
+ define(:equal, message: 'be equal to %{constraint.equal}') do
236
+ option(:equal) do
237
+ not_nil(message: 'equal is required')
238
+ respond_to(:==, message: 'equal must respond to :==')
239
+ end
240
+
241
+ initialize do |equal = nil|
242
+ equal.nil? ? {} : { equal: equal }
243
+ end
244
+ evaluate do |value|
245
+ pass if value.nil?
246
+ fail unless options[:equal] == value
247
+ end
248
+ end
249
+
250
+ define(:match, message: 'match %{constraint.regexp}') do
251
+ option(:regexp) do
252
+ not_nil(message: 'regexp is required')
253
+ respond_to(:=~, message: 'regexp must respond to :=~')
254
+ end
255
+
256
+ initialize do |regexp = nil|
257
+ regexp.nil? ? {} : { regexp: regexp }
258
+ end
259
+ evaluate do |value|
260
+ pass if value.nil?
261
+ fail unless value.is_a?(String) && options[:regexp] =~ value
262
+ end
263
+ end
264
+
265
+ define(:valid, message: 'be valid %{constraint.validator || value.class}') do
266
+ option(:validator, default: nil)
267
+
268
+ initialize do |validator = nil|
269
+ validator.nil? ? {} : { validator: validator }
270
+ end
271
+ evaluate do |value, ctx|
272
+ pass if value.nil?
273
+
274
+ Scope.current
275
+ .validator(options[:validator] || value.class)
276
+ .validate(ctx)
277
+ end
278
+ key { options[:validator] && "valid_#{options[:validator]}" || 'valid' }
279
+ end
280
+
281
+ define(:each_value, message: 'have values') do
282
+ option(:constraints) do
283
+ not_nil(message: 'constraints are required')
284
+ is_a(AST::DefinitionContext, message: 'constraints must be a DefinitionContext')
285
+ end
286
+
287
+ initialize do |&block|
288
+ return {} if block.nil?
289
+
290
+ { constraints: AST::DefinitionContext.create(&block) }
291
+ end
292
+ evaluate do |collection, ctx|
293
+ pass if collection.nil?
294
+ fail unless collection.respond_to?(:each)
295
+
296
+ constraints = options[:constraints]
297
+ case collection
298
+ when ::Hash
299
+ collection.each do |key, value|
300
+ constraints.evaluate(ctx[key])
301
+ end
302
+ else
303
+ i = 0
304
+ collection.each do |value|
305
+ constraints.evaluate(ctx[i])
306
+ i += 1
307
+ end
308
+ end
309
+ end
310
+ end
311
+
312
+ define(
313
+ :unique,
314
+ message: 'have unique %{constraint.describe_unique_attribute}'
315
+ ) do
316
+ option(:attribute, default: nil) do
317
+ is_a(Symbol, message: 'attribute %{value.inspect} must be a Symbol')
318
+ end
319
+
320
+ initialize do |attribute = nil|
321
+ attribute.nil? ? {} : { attribute: attribute }
322
+ end
323
+ evaluate do |value|
324
+ pass if value.nil?
325
+ fail unless value.respond_to?(:uniq) && value.respond_to?(:size)
326
+ fail unless value.size == value.uniq(&options[:attribute]).size
327
+ end
328
+ key do
329
+ options[:attribute] && "unique_#{options[:attribute]}" || 'unique'
330
+ end
331
+
332
+ def describe_unique_attribute
333
+ options[:attribute] || 'values'
334
+ end
335
+ end
336
+ end
337
+ end