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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3d7f65479ecf94e4e15474fd5f86ca4bd8b46a4260b0a1e77b3d666ca0e52e8d
4
- data.tar.gz: dec398c1f408cd2bfdee3281e386c313f19d67d9cfc5ade0c3c22bf16c3b764c
3
+ metadata.gz: b55536a2282b3141ff4111c11d5f08ad8f04f059e04a654a3cb7c05e60ee7a3d
4
+ data.tar.gz: 9cc607cd83e595acc81097eb3919cdfcbd16d6fc4c79c15d9dd402c5baba18d1
5
5
  SHA512:
6
- metadata.gz: 32b614ffea7a9909f64a3ced25f761347b19358771f9b6579d0eaa55e7bda35c6cf30650026faa3e4149b7205cb77f5fdcfb574516ac6ad78eac59bf076e9b15
7
- data.tar.gz: 897066f58c0f7f7197b1821902a5039279a34ac1cb3c196448a3d4a5caf2bc1e3fc45fa8fdf0919ad5ba45a09363a087afac3715d7201189b1127f31f3d6a8f9
6
+ metadata.gz: ccd37c926676d904de86c3a1130e00c1f13fd36f7f1334e46965717dca0403af4c1986309deb4d81873cea505fcbf4aa0e61c79f0081de669cf4ed74775b5e69
7
+ data.tar.gz: 1361c275de360acaf287edbd7e361b0ebf43572f487bd79224f37f888849efc3082a71c797883f9f6620699122732951f66dbcf2e4bc3b8470385e0d3385b4e4
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
 
@@ -1,6 +1,80 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "validaterb/version"
3
+ require 'forwardable'
4
+ require 'monitor'
4
5
 
6
+ require_relative 'validate/version'
7
+ require_relative 'validate/errors'
8
+ require_relative 'validate/constraint'
9
+ require_relative 'validate/assertions'
10
+ require_relative 'validate/arguments'
11
+ require_relative 'validate/ast'
12
+ require_relative 'validate/scope'
13
+ require_relative 'validate/helpers'
14
+ require_relative 'validate/constraints/validation_context'
15
+ require_relative 'validate/constraints'
16
+ require_relative 'validate/validators/dsl'
17
+ require_relative 'validate/validators'
18
+ require_relative 'validate/compare'
19
+
20
+ # Validate.rb can be used independently by calling {Validate.validate}
21
+ # or included in classes and modules.
22
+ #
23
+ # @example Validating an object using externally defined metadata
24
+ # Address = Struct.new(:street, :city, :state, :zip)
25
+ # Validate::Validators.define(Address) do
26
+ # attr(:street) { not_blank }
27
+ # attr(:city) { not_blank }
28
+ # attr(:state) { not_blank & length(2) }
29
+ # attr(:zip) { not_blank & match(/[0-9]{5}(\-[0-9]{4})?/) }
30
+ # end
31
+ # puts Validate.validate(Address.new)
32
+ #
33
+ # @example Validating an object using metdata defined in class
34
+ # class Address < Struct.new(:street, :city, :state, :zip)
35
+ # include Validate
36
+ # validate do
37
+ # attr(:street) { not_blank }
38
+ # attr(:city) { not_blank }
39
+ # attr(:state) { not_blank & length(2) }
40
+ # attr(:zip) { not_blank & match(/[0-9]{5}(\-[0-9]{4})?/) }
41
+ # end
42
+ # end
43
+ # puts Validate.validate(Address.new)
5
44
  module Validate
45
+ # Validate an object and get constraint violations list back
46
+ #
47
+ # @param object [Object] object to validate
48
+ # @param as [Symbol, Class] (object.class) validator to use, defaults to
49
+ # object's class
50
+ #
51
+ # @return [Array<Constraint::Violation>] list of constraint violations
52
+ def self.validate(object, as: object.class)
53
+ violations = []
54
+ Scope.current
55
+ .validator(as)
56
+ .validate(Constraints::ValidationContext.root(object, violations))
57
+ violations.freeze
58
+ end
59
+
60
+ # Check if a given validator exists
61
+ #
62
+ # @param name [Symbol, Class] validator to check
63
+ #
64
+ # @return [Boolean] `true` if validator is present, `else` otherwise
65
+ def self.validator?(name)
66
+ Scope.current.validator?(name)
67
+ end
68
+
69
+ # Hook to allow for inclusion in class or module
70
+ def self.included(base)
71
+ base.extend(ClassMethods)
72
+ end
73
+
74
+ # @private
75
+ module ClassMethods
76
+ def validator(&body)
77
+ @validator ||= Validators.create(&body)
78
+ end
79
+ end
6
80
  end
@@ -0,0 +1,93 @@
1
+ module Validate
2
+ module Arguments
3
+ module ClassMethods
4
+ def method_added(method_name)
5
+ super
6
+ return if @args.empty?
7
+
8
+ method = instance_method(method_name)
9
+ guard = ArgumentsGuard.new(method, @args.dup)
10
+
11
+ @methods_guard.__send__(:define_method, method_name) do |*args, &block|
12
+ guard.send(method_name, *args, &block)
13
+ super(*args, &block)
14
+ end
15
+ ensure
16
+ @args.clear
17
+ end
18
+
19
+ def singleton_method_added(method_name)
20
+ super
21
+ return if @args.empty?
22
+
23
+ method = singleton_method(method_name)
24
+ guard = ArgumentsGuard.new(method, @args.dup)
25
+
26
+ @methods_guard.__send__(:define_singleton_method, method_name) do |*args, &block|
27
+ guard.send(method_name, *args, &block)
28
+ super(*args, &block)
29
+ end
30
+ ensure
31
+ @args.clear
32
+ end
33
+
34
+ def arg(name, &body)
35
+ if @args.include?(name)
36
+ raise Error::ArgumentError, "duplicate argument :#{name}"
37
+ end
38
+
39
+ @args[name] = Assertions.create(&body)
40
+ self
41
+ end
42
+ end
43
+
44
+ def self.included(base)
45
+ base.extend(ClassMethods)
46
+ base.instance_exec do
47
+ @args = {}
48
+ prepend(@methods_guard = Module.new)
49
+ end
50
+ end
51
+
52
+ class ArgumentsGuard
53
+ DEFAULT_VALUE = BasicObject.new
54
+
55
+ def initialize(method, rules)
56
+ signature = []
57
+ assertions = []
58
+
59
+ method.parameters.each do |(kind, name)|
60
+ case kind
61
+ when :req
62
+ signature << name.to_s
63
+ when :opt
64
+ signature << "#{name} = DEFAULT_VALUE"
65
+ when :rest
66
+ signature << "*#{name}"
67
+ when :keyreq
68
+ signature << "#{name}:"
69
+ when :key
70
+ signature << "#{name}: DEFAULT_VALUE"
71
+ when :keyrest
72
+ signature << "**#{name}"
73
+ when :block
74
+ signature << "&#{name}"
75
+ else
76
+ raise Error::ArgumentError, "unsupported parameter type #{kind}"
77
+ end
78
+ next unless rules.include?(name)
79
+
80
+ assertions << "@rules[:#{name}].assert(#{name}, message: 'invalid argument #{name}')"
81
+ end
82
+
83
+ singleton_class.class_eval(<<~RUBY, __FILE__, __LINE__ + 1)
84
+ def #{method.name}(#{signature.join(', ')})
85
+ #{assertions.join("\n ")}
86
+ end
87
+ RUBY
88
+
89
+ @rules = rules
90
+ end
91
+ end
92
+ end
93
+ 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