validate-rb 0.1.0.pre → 1.0.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,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Validate
4
+ module Compare
5
+ module TransformUsing
6
+ def using(&transform_block)
7
+ @transform_block = transform_block
8
+ self
9
+ end
10
+
11
+ def <=>(other)
12
+ return super if @transform_block.nil?
13
+
14
+ super(@transform_block.call(other))
15
+ end
16
+ end
17
+
18
+ class WithAttributes
19
+ include Comparable
20
+ prepend TransformUsing
21
+
22
+ def initialize(attributes)
23
+ @attributes = attributes
24
+ end
25
+
26
+ def <=>(other)
27
+ @attributes.each do |attribute, value|
28
+ result = value <=> other.send(attribute)
29
+ return result unless result.zero?
30
+ end
31
+
32
+ 0
33
+ end
34
+
35
+ def method_missing(symbol, *args)
36
+ return super unless args.empty? && respond_to_missing?(symbol)
37
+
38
+ @attributes[symbol]
39
+ end
40
+
41
+ def respond_to_missing?(attribute, _ = false)
42
+ @attributes.include?(attribute)
43
+ end
44
+
45
+ def to_s
46
+ '<attributes ' + @attributes.map { |attribute, value| "#{attribute}: #{value}"}
47
+ .join(', ') + '>'
48
+ end
49
+ end
50
+
51
+ class ToValue
52
+ include Comparable
53
+ prepend TransformUsing
54
+
55
+ def initialize(value_block)
56
+ @value_block = value_block
57
+ end
58
+
59
+ def <=>(other)
60
+ @value_block.call <=> other
61
+ end
62
+
63
+ def to_s
64
+ '<dynamic value>'
65
+ end
66
+ end
67
+
68
+ module_function
69
+
70
+ def attributes(**attributes)
71
+ WithAttributes.new(attributes)
72
+ end
73
+
74
+ def to(value = nil, &value_block)
75
+ value_block ||= value.is_a?(Proc) ? value : proc { value }
76
+ ToValue.new(value_block)
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,231 @@
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, &block|
120
+ if args.last.is_a?(Hash)
121
+ known_options, kwargs =
122
+ args.pop
123
+ .partition { |k, _| supported_options.include?(k) }
124
+ .map { |h| Hash[h] }
125
+
126
+ if !expects_kwargs && !kwargs.empty?
127
+ args << kwargs
128
+ kwargs = {}
129
+ end
130
+
131
+ if expects_kwargs
132
+ merged_options = {}.merge!(super(*args, **kwargs, &block), known_options)
133
+ else
134
+ args << kwargs unless kwargs.empty?
135
+ merged_options = {}.merge!(super(*args, &block), known_options)
136
+ end
137
+ else
138
+ merged_options = super(*args, &block)
139
+ end
140
+
141
+ options = supported_options.each_with_object({}) do |(n, opt), opts|
142
+ opts[n] = opt.get_or_default(merged_options)
143
+ end
144
+
145
+ unless merged_options.empty?
146
+ raise Error::ArgumentError,
147
+ "unexpected options #{merged_options.inspect}"
148
+ end
149
+
150
+ @options = options.freeze
151
+ end
152
+ remove_instance_variable(:@supported_options)
153
+ end
154
+
155
+ def evaluate(&validation_block)
156
+ define_constraint_method(:valid?, validation_block) do |*args|
157
+ catch(:result) do
158
+ super(*args[0...validation_block.arity])
159
+ :pass
160
+ end == :pass
161
+ end
162
+ self
163
+ end
164
+
165
+ def describe(&describe_block)
166
+ define_method(:to_s, &describe_block)
167
+ self
168
+ end
169
+
170
+ def key(&key_block)
171
+ define_method(:name, &key_block)
172
+ self
173
+ end
174
+
175
+ private
176
+
177
+ def define_constraint_method(name, body, &override)
178
+ @constraint_user_methods.__send__(:define_method, name, &body)
179
+ define_method(name, &override)
180
+ self
181
+ end
182
+ end
183
+
184
+ attr_reader :options
185
+ protected :options
186
+
187
+ def initialize(**options)
188
+ @options = options
189
+ end
190
+
191
+ def name
192
+ raise ::NotImplementedError
193
+ end
194
+
195
+ def valid?(value, ctx = Constraints::ValidationContext.none)
196
+ raise ::NotImplementedError
197
+ end
198
+
199
+ def to_s
200
+ name.to_s.gsub('_', ' ')
201
+ end
202
+
203
+ def inspect
204
+ "#<#{self.class.name} #{@options.map { |name, value| "#{name}: #{value.inspect}" }.join(', ')}>"
205
+ end
206
+
207
+ def ==(other)
208
+ other.is_a?(Constraint) && other.name == name && other.options == options
209
+ end
210
+
211
+ def respond_to_missing?(method, _ = false)
212
+ @options.include?(method)
213
+ end
214
+
215
+ def method_missing(method, *args)
216
+ return super unless args.empty? || respond_to_missing?(method)
217
+
218
+ @options[method]
219
+ end
220
+
221
+ private
222
+
223
+ def fail
224
+ throw(:result, :fail)
225
+ end
226
+
227
+ def pass
228
+ throw(:result, :pass)
229
+ end
230
+ end
231
+ 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