validate-rb 0.1.0.pre → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3d7f65479ecf94e4e15474fd5f86ca4bd8b46a4260b0a1e77b3d666ca0e52e8d
4
- data.tar.gz: dec398c1f408cd2bfdee3281e386c313f19d67d9cfc5ade0c3c22bf16c3b764c
3
+ metadata.gz: 950214c76f33653b36c70bf0ac8a5bd9e639823b394317f4d433c16796ca1e10
4
+ data.tar.gz: 4a41af1288c617845258471dafb59efde0d63d6d89afc8f020cfd7fce7b49fc2
5
5
  SHA512:
6
- metadata.gz: 32b614ffea7a9909f64a3ced25f761347b19358771f9b6579d0eaa55e7bda35c6cf30650026faa3e4149b7205cb77f5fdcfb574516ac6ad78eac59bf076e9b15
7
- data.tar.gz: 897066f58c0f7f7197b1821902a5039279a34ac1cb3c196448a3d4a5caf2bc1e3fc45fa8fdf0919ad5ba45a09363a087afac3715d7201189b1127f31f3d6a8f9
6
+ metadata.gz: 2a28334e266467a7a3de84b4cfd06c5d64a8c016dd4150de9ec5d0d9dc910342f7be0829556def6686c73d3a8c7fd281ac2648ac8cdaadd34de02cd74ca69f99
7
+ data.tar.gz: 75e60c01c7a1d51f424fc0194aae5771ead30dc739fddf75f673b480c5e3459d22c1e74491b7806098553aabce50feadfc5f22fec14507a14ba7ce18c13821f6
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020 Gusto
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Validate.rb
1
+ # Validate.rb ![CI](https://github.com/Gusto/validate-rb/workflows/CI/badge.svg)
2
2
 
3
3
  Yummy constraint validations for Ruby
4
4
 
@@ -7,7 +7,7 @@ Yummy constraint validations for Ruby
7
7
  Add this line to your application's Gemfile:
8
8
 
9
9
  ```ruby
10
- gem 'validaterb'
10
+ gem 'validate-rb'
11
11
  ```
12
12
 
13
13
  And then execute:
@@ -16,7 +16,7 @@ And then execute:
16
16
 
17
17
  Or install it yourself as:
18
18
 
19
- $ gem install validaterb
19
+ $ gem install validate-rb
20
20
 
21
21
  ## Usage
22
22
 
@@ -120,5 +120,5 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
120
120
 
121
121
  ## Contributing
122
122
 
123
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/gusto-validation.
123
+ Bug reports and pull requests are welcome on GitHub at https://github.com/gusto/validate-rb.
124
124
 
data/lib/validate.rb CHANGED
@@ -1,6 +1,81 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "validaterb/version"
3
+ require 'forwardable'
4
+ require 'monitor'
5
+ require 'ipaddr'
4
6
 
7
+ require_relative 'validate/version'
8
+ require_relative 'validate/errors'
9
+ require_relative 'validate/constraint'
10
+ require_relative 'validate/assertions'
11
+ require_relative 'validate/arguments'
12
+ require_relative 'validate/ast'
13
+ require_relative 'validate/scope'
14
+ require_relative 'validate/helpers'
15
+ require_relative 'validate/constraints/validation_context'
16
+ require_relative 'validate/constraints'
17
+ require_relative 'validate/validators/dsl'
18
+ require_relative 'validate/validators'
19
+ require_relative 'validate/compare'
20
+
21
+ # Validate.rb can be used independently by calling {Validate.validate}
22
+ # or included in classes and modules.
23
+ #
24
+ # @example Validating an object using externally defined metadata
25
+ # Address = Struct.new(:street, :city, :state, :zip)
26
+ # Validate::Validators.define(Address) do
27
+ # attr(:street) { not_blank }
28
+ # attr(:city) { not_blank }
29
+ # attr(:state) { not_blank & length(2) }
30
+ # attr(:zip) { not_blank & match(/[0-9]{5}(\-[0-9]{4})?/) }
31
+ # end
32
+ # puts Validate.validate(Address.new)
33
+ #
34
+ # @example Validating an object using metdata defined in class
35
+ # class Address < Struct.new(:street, :city, :state, :zip)
36
+ # include Validate
37
+ # validate do
38
+ # attr(:street) { not_blank }
39
+ # attr(:city) { not_blank }
40
+ # attr(:state) { not_blank & length(2) }
41
+ # attr(:zip) { not_blank & match(/[0-9]{5}(\-[0-9]{4})?/) }
42
+ # end
43
+ # end
44
+ # puts Validate.validate(Address.new)
5
45
  module Validate
46
+ # Validate an object and get constraint violations list back
47
+ #
48
+ # @param object [Object] object to validate
49
+ # @param as [Symbol, Class] (object.class) validator to use, defaults to
50
+ # object's class
51
+ #
52
+ # @return [Array<Constraint::Violation>] list of constraint violations
53
+ def self.validate(object, as: object.class)
54
+ violations = []
55
+ Scope.current
56
+ .validator(as)
57
+ .validate(Constraints::ValidationContext.root(object, violations))
58
+ violations.freeze
59
+ end
60
+
61
+ # Check if a given validator exists
62
+ #
63
+ # @param name [Symbol, Class] validator to check
64
+ #
65
+ # @return [Boolean] `true` if validator is present, `else` otherwise
66
+ def self.validator?(name)
67
+ Scope.current.validator?(name)
68
+ end
69
+
70
+ # Hook to allow for inclusion in class or module
71
+ def self.included(base)
72
+ base.extend(ClassMethods)
73
+ end
74
+
75
+ # @private
76
+ module ClassMethods
77
+ def validator(&body)
78
+ @validator ||= Validators.create(&body)
79
+ end
80
+ end
6
81
  end
@@ -0,0 +1,94 @@
1
+ module Validate
2
+ module Arguments
3
+ module ClassMethods
4
+ def method_added(method_name)
5
+ super
6
+ guard_method(instance_method(method_name), @methods_guard)
7
+ end
8
+
9
+ def singleton_method_added(method_name)
10
+ super
11
+ guard_method(singleton_method(method_name), @singleton_methods_guard)
12
+ end
13
+
14
+ def arg(name, &body)
15
+ if @args.include?(name)
16
+ raise Error::ArgumentError, "duplicate argument :#{name}"
17
+ end
18
+
19
+ @args[name] = Assertions.create(&body)
20
+ self
21
+ end
22
+
23
+ private
24
+
25
+ def guard_method(method, guard_module)
26
+ return if @args.empty?
27
+ guard = ArgumentsGuard.new(method, @args)
28
+ guard_module.__send__(:define_method, method.name) do |*args, **kwargs, &block|
29
+ if kwargs.empty?
30
+ guard.enforce!(*args, &block)
31
+ super(*args, &block)
32
+ else
33
+ guard.enforce!(*args, **kwargs, &block)
34
+ super(*args, **kwargs, &block)
35
+ end
36
+ end
37
+ ensure
38
+ @args = {}
39
+ end
40
+ end
41
+
42
+ def self.included(base)
43
+ base.extend(ClassMethods)
44
+ base.instance_exec do
45
+ @args = {}
46
+ prepend(@methods_guard = Module.new)
47
+ singleton_class.prepend(@singleton_methods_guard = Module.new)
48
+ end
49
+ end
50
+
51
+ class ArgumentsGuard
52
+ DEFAULT_VALUE = BasicObject.new
53
+
54
+ def initialize(method, rules)
55
+ signature = []
56
+ assertions = []
57
+
58
+ method.parameters.each do |(kind, name)|
59
+ case kind
60
+ when :req
61
+ signature << name.to_s
62
+ when :opt
63
+ signature << "#{name} = DEFAULT_VALUE"
64
+ when :rest
65
+ signature << "*#{name}"
66
+ when :keyreq
67
+ signature << "#{name}:"
68
+ when :key
69
+ signature << "#{name}: DEFAULT_VALUE"
70
+ when :keyrest
71
+ signature << "**#{name}"
72
+ when :block
73
+ signature << "&#{name}"
74
+ else
75
+ raise Error::ArgumentError,
76
+ "unsupported parameter type #{kind}"
77
+ end
78
+ next unless rules.include?(name)
79
+
80
+ assertions <<
81
+ "@rules[:#{name}].assert(#{name}, message: '#{name}') unless #{name}.eql?(DEFAULT_VALUE)"
82
+ end
83
+
84
+ singleton_class.class_eval(<<~RUBY, __FILE__, __LINE__)
85
+ def enforce!(#{signature.join(', ')})
86
+ #{assertions.join("\n ")}
87
+ end
88
+ RUBY
89
+
90
+ @rules = rules
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,27 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Validate
5
+ module Assertions
6
+ module_function
7
+
8
+ def create(*args, &block)
9
+ Assertion.new(AST::DefinitionContext.create(*args, &block))
10
+ end
11
+
12
+ class Assertion
13
+ def initialize(validation_context)
14
+ @constraints = validation_context
15
+ end
16
+
17
+ def assert(value, error_class: Error::ArgumentError, message: 'invalid value')
18
+ ctx = Constraints::ValidationContext.root(value)
19
+ @constraints.evaluate(ctx)
20
+ return value unless ctx.has_violations?
21
+
22
+ raise error_class, message,
23
+ cause: ctx.to_err
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,381 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Validate
5
+ module AST
6
+ def self.build(*args, &block)
7
+ Generator.new
8
+ .generate(*args, &block)
9
+ .freeze
10
+ end
11
+
12
+ class DefinitionContext
13
+ module Builder
14
+ module_function
15
+
16
+ def all_constraints(*constraints)
17
+ Rules::Unanimous.new(constraints.map { |node| send(*node) })
18
+ end
19
+
20
+ def at_least_one_constraint(*constraints)
21
+ Rules::Affirmative.new(constraints.map { |node| send(*node) })
22
+ end
23
+
24
+ def no_constraints(*constraints)
25
+ Rules::Negative.new(constraints.map { |node| send(*node) })
26
+ end
27
+
28
+ def constraint(name, args, block, trace)
29
+ if defined?(Constraints) && Constraints.respond_to?(name)
30
+ begin
31
+ return Constraints.send(name, *(args.map { |node| send(*node) }), &block)
32
+ rescue => e
33
+ ::Kernel.raise Error::ValidationRuleError, e.message, trace
34
+ end
35
+ end
36
+
37
+ Rules::Pending.new(name, args.map { |node| send(*node) }, block, trace)
38
+ end
39
+
40
+ def value(value)
41
+ value
42
+ end
43
+ end
44
+
45
+ def self.create(*args, &block)
46
+ ast = AST.build(*args, &block)
47
+ context = new
48
+ ast.each { |node| context.add_constraint(Builder.send(*node)) }
49
+ context
50
+ end
51
+
52
+ def initialize
53
+ @constraints = {}
54
+ end
55
+
56
+ def add_constraint(constraint)
57
+ if @constraints.include?(constraint.name)
58
+ raise Error::ValidationRuleError,
59
+ "duplicate constraint #{constraint.name}"
60
+ end
61
+
62
+ @constraints[constraint.name] = constraint
63
+ self
64
+ end
65
+
66
+ def evaluate(ctx)
67
+ @constraints.each_value
68
+ .reject { |c| catch(:pending) { c.valid?(ctx.value, ctx) } }
69
+ .each { |c| ctx.add_violation(c) }
70
+ ctx
71
+ end
72
+ end
73
+
74
+ CORE_CONSTRAINTS = %i[
75
+ not_nil not_blank not_empty
76
+ is_a one_of validate
77
+ min max equal match
78
+ valid each_value unique
79
+ length
80
+ ].freeze
81
+
82
+ class Generator < ::BasicObject
83
+ def initialize
84
+ @stack = []
85
+ end
86
+
87
+ def generate(*args, &block)
88
+ instance_exec(*args, &block)
89
+
90
+ if @stack.one? && @stack.first[0] == :all_constraints
91
+ return @stack.first[1..-1]
92
+ end
93
+
94
+ @stack
95
+ end
96
+
97
+ def &(other)
98
+ unless other == self
99
+ ::Kernel.raise(
100
+ Error::ValidationRuleError,
101
+ 'bad rule, only constraints and &, |, and ! operators allowed'
102
+ )
103
+ end
104
+
105
+ right = @stack.pop
106
+ left = @stack.pop
107
+ if right[0] == :all_constraints
108
+ right.insert(1, left)
109
+ @stack << right
110
+ else
111
+ @stack << [:all_constraints, left, right]
112
+ end
113
+ self
114
+ end
115
+
116
+ def |(other)
117
+ unless other == self
118
+ ::Kernel.raise(
119
+ Error::ValidationRuleError,
120
+ 'bad rule, only constraints and &, |, and ! operators allowed'
121
+ )
122
+ end
123
+
124
+ right = @stack.pop
125
+ left = @stack.pop
126
+ if right[0] == :at_least_one_constraint
127
+ right.insert(1, left)
128
+ @stack << right
129
+ else
130
+ @stack << [:at_least_one_constraint, left, right]
131
+ end
132
+ self
133
+ end
134
+
135
+ def !
136
+ prev = @stack.pop
137
+ if prev[0] == :no_constraints
138
+ @stack << prev[1]
139
+ elsif prev[0] == :all_constraints
140
+ prev[0] = :no_constraints
141
+ @stack << prev
142
+ else
143
+ @stack << [:no_constraints, prev]
144
+ end
145
+ self
146
+ end
147
+
148
+ private
149
+
150
+ def method_missing(method, *args, &block)
151
+ return super unless respond_to_missing?(method)
152
+
153
+ @stack << [
154
+ :constraint,
155
+ method,
156
+ args.map { |arg| [:value, arg] },
157
+ block,
158
+ ::Kernel.caller
159
+ .reject { |line| line.include?(__FILE__) }
160
+ ]
161
+ self
162
+ end
163
+
164
+ def respond_to_missing?(method, _ = false)
165
+ (defined?(Constraints) && Constraints.respond_to?(method)) || CORE_CONSTRAINTS.include?(method)
166
+ end
167
+ end
168
+
169
+ module Combinator
170
+ extend Forwardable
171
+ def_delegators :@constraints, :[]
172
+
173
+ def respond_to_missing?(_, _ = false)
174
+ false
175
+ end
176
+
177
+ private
178
+
179
+ def constraint_message(index)
180
+ @constraints[index].message % Hash.new do |_, key|
181
+ if key.to_s.start_with?('constraint')
182
+ "%{#{key.to_s.gsub('constraint', "constraint[#{index}]")}}"
183
+ else
184
+ "%{#{key}}"
185
+ end
186
+ end
187
+ end
188
+ end
189
+
190
+ module Rules
191
+ class Pending < Constraint
192
+ include MonitorMixin
193
+
194
+ def initialize(name, args, block, caller)
195
+ @name = name
196
+ @args = args
197
+ @block = block
198
+ @caller = caller
199
+ @constraint = nil
200
+
201
+ extend SingleForwardable
202
+ mon_initialize
203
+ end
204
+
205
+ def name
206
+ load_constraint { return @name }.name
207
+ end
208
+
209
+ def valid?(value, ctx = Constraints::ValidationContext.none)
210
+ load_constraint { throw(:pending, true) }.valid?(value, ctx)
211
+ end
212
+
213
+ def to_s
214
+ load_constraint { return "[pending #{@name}]" }.to_s
215
+ end
216
+
217
+ def inspect
218
+ load_constraint { return "[pending #{@name}]" }.inspect
219
+ end
220
+
221
+ def ==(other)
222
+ load_constraint { return false } == other
223
+ end
224
+
225
+ def method_missing(method, *args)
226
+ load_constraint { return NameError }.__send__(method, *args)
227
+ end
228
+
229
+ def respond_to_missing?(method, pvt = false)
230
+ load_constraint { return false }.__send__(:respond_to_missing?, method, pvt)
231
+ end
232
+
233
+ private
234
+
235
+ def load_constraint
236
+ yield unless defined?(Constraints) && Constraints.respond_to?(@name)
237
+
238
+ synchronize do
239
+ return @constraint if @constraint
240
+
241
+ begin
242
+ @constraint = Constraints.send(@name, *@args, &@block)
243
+ rescue => e
244
+ ::Kernel.raise Error::ValidationRuleError, e.message, @caller
245
+ end
246
+
247
+ def_delegators(:@constraint, :name, :valid?, :to_s,
248
+ :inspect, :==, :message)
249
+
250
+ @name = @args = @block = @caller = nil
251
+ @constraint
252
+ end
253
+ end
254
+ end
255
+
256
+ class Unanimous < Constraint
257
+ include Combinator
258
+ include Arguments
259
+
260
+ arg(:constraints) do
261
+ not_nil
262
+ length(min: 2)
263
+ each_value { is_a(Constraint) }
264
+ unique(:name)
265
+ end
266
+ def initialize(constraints)
267
+ @constraints = constraints.freeze
268
+ end
269
+
270
+ def valid?(value, _ = Constraints::ValidationContext.none)
271
+ ctx = Constraints::ValidationContext.root(value)
272
+ @constraints.all? do |c|
273
+ c.valid?(value, ctx) && !ctx.has_violations?
274
+ end
275
+ end
276
+
277
+ def name
278
+ 'both_' + @constraints.map(&:name).sort.join('_and_')
279
+ end
280
+
281
+ def inspect
282
+ return @constraints.first.inspect if @constraints.one?
283
+
284
+ "(#{@constraints.map(&:inspect).join(' & ')})"
285
+ end
286
+
287
+ def message
288
+ 'both ' + @constraints
289
+ .size
290
+ .times
291
+ .map { |i| "[#{constraint_message(i)}]" }
292
+ .join(', and ')
293
+ end
294
+ end
295
+
296
+ class Affirmative < Constraint
297
+ include Combinator
298
+ include Arguments
299
+
300
+ arg(:constraints) do
301
+ not_nil
302
+ length(min: 2)
303
+ each_value { is_a(Constraint) }
304
+ unique(:name)
305
+ end
306
+ def initialize(constraints)
307
+ @constraints = constraints.freeze
308
+ end
309
+
310
+ def valid?(value, _ = Constraints::ValidationContext.none)
311
+ ctx = Constraints::ValidationContext.root(value)
312
+ @constraints.any? do |c|
313
+ ctx.clear_violations
314
+ c.valid?(value, ctx) && !ctx.has_violations?
315
+ end
316
+ end
317
+
318
+ def name
319
+ 'either_' + @constraints.map(&:name).sort.join('_or_')
320
+ end
321
+
322
+ def inspect
323
+ return @constraints.first.inspect if @constraints.one?
324
+
325
+ "(#{@constraints.map(&:inspect).join(' | ')})"
326
+ end
327
+
328
+ def message
329
+ 'either ' + @constraints
330
+ .size
331
+ .times
332
+ .map { |i| "[#{constraint_message(i)}]" }
333
+ .join(', or ')
334
+ end
335
+ end
336
+
337
+ class Negative < Constraint
338
+ include Combinator
339
+ include Arguments
340
+
341
+ arg(:constraints) do
342
+ not_nil
343
+ not_empty
344
+ each_value { is_a(Constraint) }
345
+ unique(:name)
346
+ end
347
+ def initialize(constraints)
348
+ @constraints = constraints.freeze
349
+ end
350
+
351
+ def valid?(value, _ = Constraints::ValidationContext.none)
352
+ ctx = Constraints::ValidationContext.root(value)
353
+ @constraints.none? do |c|
354
+ ctx.clear_violations
355
+ c.valid?(value, ctx) && !ctx.has_violations?
356
+ end
357
+ end
358
+
359
+ def name
360
+ 'neither_' + @constraints.map(&:name).sort.join('_nor_')
361
+ end
362
+
363
+ def message
364
+ return "not [#{constraint_message(0)}]" if @constraints.one?
365
+
366
+ 'neither ' + @constraints
367
+ .size
368
+ .times
369
+ .map { |i| "[#{constraint_message(i)}]" }
370
+ .join(', nor ')
371
+ end
372
+
373
+ def inspect
374
+ return "!#{@constraints.first.inspect}" if @constraints.one?
375
+
376
+ "!(#{@constraints.map(&:inspect).join(' & ')})"
377
+ end
378
+ end
379
+ end
380
+ end
381
+ end