poro_validator 0.0.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 +7 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +11 -0
- data/LICENSE.txt +21 -0
- data/README.md +382 -0
- data/Rakefile +139 -0
- data/lib/poro_validator.rb +54 -0
- data/lib/poro_validator/configuration.rb +63 -0
- data/lib/poro_validator/error_store.rb +52 -0
- data/lib/poro_validator/errors.rb +56 -0
- data/lib/poro_validator/exceptions.rb +14 -0
- data/lib/poro_validator/validator.rb +82 -0
- data/lib/poro_validator/validator/base_class.rb +55 -0
- data/lib/poro_validator/validator/conditions.rb +71 -0
- data/lib/poro_validator/validator/context.rb +19 -0
- data/lib/poro_validator/validator/factory.rb +32 -0
- data/lib/poro_validator/validator/validation.rb +50 -0
- data/lib/poro_validator/validator/validations.rb +55 -0
- data/lib/poro_validator/validators/base_class.rb +53 -0
- data/lib/poro_validator/validators/exclusion_validator.rb +26 -0
- data/lib/poro_validator/validators/float_validator.rb +17 -0
- data/lib/poro_validator/validators/format_validator.rb +16 -0
- data/lib/poro_validator/validators/inclusion_validator.rb +26 -0
- data/lib/poro_validator/validators/integer_validator.rb +17 -0
- data/lib/poro_validator/validators/length_validator.rb +41 -0
- data/lib/poro_validator/validators/numeric_validator.rb +48 -0
- data/lib/poro_validator/validators/presence_validator.rb +23 -0
- data/lib/poro_validator/validators/range_array_validator.rb +19 -0
- data/lib/poro_validator/validators/with_validator.rb +21 -0
- data/lib/poro_validator/version.rb +3 -0
- data/poro_validator.gemspec +97 -0
- data/spec/features/composable_validations_spec.rb +26 -0
- data/spec/features/inheritable_spec.rb +29 -0
- data/spec/features/nested_validations_spec.rb +136 -0
- data/spec/lib/poro_validator/configuration_spec.rb +37 -0
- data/spec/lib/poro_validator/error_store_spec.rb +125 -0
- data/spec/lib/poro_validator/errors_spec.rb +79 -0
- data/spec/lib/poro_validator/validator/base_class_spec.rb +62 -0
- data/spec/lib/poro_validator/validator/conditions_spec.rb +112 -0
- data/spec/lib/poro_validator/validator/factory_spec.rb +23 -0
- data/spec/lib/poro_validator/validator/validation_spec.rb +69 -0
- data/spec/lib/poro_validator/validator/validations_spec.rb +34 -0
- data/spec/lib/poro_validator/validator_spec.rb +55 -0
- data/spec/lib/poro_validator/validators/base_class_spec.rb +11 -0
- data/spec/lib/poro_validator/validators/exclusion_validator_spec.rb +81 -0
- data/spec/lib/poro_validator/validators/float_validator_spec.rb +43 -0
- data/spec/lib/poro_validator/validators/format_validator_spec.rb +48 -0
- data/spec/lib/poro_validator/validators/inclusion_validator_spec.rb +81 -0
- data/spec/lib/poro_validator/validators/integer_validator_spec.rb +43 -0
- data/spec/lib/poro_validator/validators/length_validator_spec.rb +64 -0
- data/spec/lib/poro_validator/validators/numeric_validator_spec.rb +68 -0
- data/spec/lib/poro_validator/validators/presence_validator_spec.rb +52 -0
- data/spec/lib/poro_validator/validators/with_validator_spec.rb +90 -0
- data/spec/poro_validator_spec.rb +25 -0
- data/spec/spec_helper.rb +34 -0
- data/spec/support/spec_helpers/concerns.rb +46 -0
- data/spec/support/spec_helpers/validator_test_macros.rb +99 -0
- metadata +199 -0
@@ -0,0 +1,54 @@
|
|
1
|
+
# PoroValidator::Validator is a lightweight Plain Old Ruby Object
|
2
|
+
# validator.
|
3
|
+
#
|
4
|
+
# @since 0.0.1
|
5
|
+
module PoroValidator
|
6
|
+
# Allow access to the anonymouse validator module
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# class FoodValidator
|
10
|
+
# include PoroValidator.validator
|
11
|
+
#
|
12
|
+
# end
|
13
|
+
|
14
|
+
def self.validator
|
15
|
+
mod = Module.new
|
16
|
+
mod.define_singleton_method(:included) do |base|
|
17
|
+
base.send(:include, ::PoroValidator::Validator)
|
18
|
+
end
|
19
|
+
mod
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.configuration
|
23
|
+
@configuration || Configuration.new
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Base Classes
|
28
|
+
require "poro_validator/version"
|
29
|
+
require "poro_validator/error_store"
|
30
|
+
require "poro_validator/errors"
|
31
|
+
require "poro_validator/exceptions"
|
32
|
+
require "poro_validator/configuration"
|
33
|
+
require "poro_validator/validator/base_class"
|
34
|
+
require "poro_validator/validator/validation"
|
35
|
+
require "poro_validator/validator/validations"
|
36
|
+
require "poro_validator/validator/conditions"
|
37
|
+
require "poro_validator/validator/factory"
|
38
|
+
require "poro_validator/validator/context"
|
39
|
+
|
40
|
+
# Validators
|
41
|
+
require "poro_validator/validators/base_class"
|
42
|
+
require "poro_validator/validators/presence_validator"
|
43
|
+
require "poro_validator/validators/format_validator"
|
44
|
+
require "poro_validator/validators/with_validator"
|
45
|
+
require "poro_validator/validators/range_array_validator"
|
46
|
+
require "poro_validator/validators/inclusion_validator"
|
47
|
+
require "poro_validator/validators/exclusion_validator"
|
48
|
+
require "poro_validator/validators/integer_validator"
|
49
|
+
require "poro_validator/validators/float_validator"
|
50
|
+
require "poro_validator/validators/length_validator"
|
51
|
+
require "poro_validator/validators/numeric_validator"
|
52
|
+
|
53
|
+
# Modules
|
54
|
+
require "poro_validator/validator"
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module PoroValidator
|
2
|
+
class Configuration
|
3
|
+
class Message
|
4
|
+
# @private_const
|
5
|
+
DEFAULT_MESSAGES = {
|
6
|
+
default: lambda { "is not valid" },
|
7
|
+
presence: lambda { "is not present" },
|
8
|
+
integer: lambda { "is not an integer" },
|
9
|
+
float: lambda { "is not a float" },
|
10
|
+
|
11
|
+
inclusion: lambda do |range|
|
12
|
+
"is not within the range of #{range.inspect}"
|
13
|
+
end,
|
14
|
+
|
15
|
+
exclusion: lambda do |range|
|
16
|
+
"is not outside the range of #{range.inspect}"
|
17
|
+
end,
|
18
|
+
|
19
|
+
format: lambda do |pattern|
|
20
|
+
"does not match the pattern: #{pattern.inspect}"
|
21
|
+
end,
|
22
|
+
|
23
|
+
length: lambda do |length|
|
24
|
+
"does not match the length options: #{length.inspect}"
|
25
|
+
end,
|
26
|
+
|
27
|
+
numeric: lambda do |numeric|
|
28
|
+
"does not match the numeric options: #{numeric.inspect}"
|
29
|
+
end
|
30
|
+
}
|
31
|
+
|
32
|
+
def initialize
|
33
|
+
@messages = {}
|
34
|
+
end
|
35
|
+
|
36
|
+
def get(validator, *args)
|
37
|
+
args.compact.length > 0 ? message(validator).call(*args) : message(validator).call
|
38
|
+
end
|
39
|
+
|
40
|
+
def set(validator, message)
|
41
|
+
unless message.is_a?(::Proc)
|
42
|
+
raise PoroValidator::ConfigError.new(
|
43
|
+
"A proc/lambda is required to configure validator messages"
|
44
|
+
)
|
45
|
+
end
|
46
|
+
@messages[validator] = message
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def message(validator)
|
52
|
+
@messages[validator] || DEFAULT_MESSAGES[validator] ||
|
53
|
+
DEFAULT_MESSAGES[:default]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
attr_reader :message
|
58
|
+
|
59
|
+
def initialize
|
60
|
+
@message = Message.new
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module PoroValidator
|
2
|
+
class ErrorStore
|
3
|
+
attr_reader :data
|
4
|
+
|
5
|
+
def initialize(data = {})
|
6
|
+
@data = {}
|
7
|
+
self.load(data)
|
8
|
+
end
|
9
|
+
|
10
|
+
def get(key)
|
11
|
+
validate_key!(key)
|
12
|
+
@data[key.to_s]
|
13
|
+
end
|
14
|
+
|
15
|
+
def set(key, value = nil, &block)
|
16
|
+
validate_key!(key)
|
17
|
+
if key.is_a?(::Array)
|
18
|
+
key = key.flatten.reverse.inject do |a,n|
|
19
|
+
{ n => a }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
@data[key.to_s] = block_given? ? yield : value
|
23
|
+
end
|
24
|
+
|
25
|
+
def set?(key)
|
26
|
+
validate_key!(key)
|
27
|
+
@data.keys.include?(key.to_s)
|
28
|
+
end
|
29
|
+
|
30
|
+
def load(data)
|
31
|
+
data.each do |key, value|
|
32
|
+
validate_key!(key)
|
33
|
+
@data[key.to_s] = value
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def reset
|
38
|
+
@data = {}
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def validate_key!(key)
|
44
|
+
unless key.is_a?(::String) || key.is_a?(::Symbol) || key.is_a?(::Array) \
|
45
|
+
|| key.is_a?(::Hash)
|
46
|
+
raise ::PoroValidator::InvalidType.new(
|
47
|
+
"only String, Symbol, Array or Hash are allowed! invalid key: #{key.inspect}"
|
48
|
+
)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module PoroValidator
|
2
|
+
class Errors
|
3
|
+
|
4
|
+
attr_reader :store
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@store = ErrorStore.new
|
8
|
+
end
|
9
|
+
|
10
|
+
def add(attr, validator, *msg_opts)
|
11
|
+
if store.set?(attr)
|
12
|
+
store.get(attr) << message_lookup(validator, *msg_opts)
|
13
|
+
else
|
14
|
+
store.set(attr, [message_lookup(validator, *msg_opts)])
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def count
|
19
|
+
store.data.inject(0) do |m, kv|
|
20
|
+
_, errors = *kv
|
21
|
+
m + errors.length
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def empty?
|
26
|
+
count == 0
|
27
|
+
end
|
28
|
+
|
29
|
+
def full_messages
|
30
|
+
store.data.inject([]) do |m, kv|
|
31
|
+
attr, errors = *kv
|
32
|
+
errors.each { |e| m << "#{attr} #{e}" }
|
33
|
+
m
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def on(attr)
|
38
|
+
return unless store.set?(attr)
|
39
|
+
store.get(attr)
|
40
|
+
end
|
41
|
+
|
42
|
+
def clear_errors
|
43
|
+
store.reset
|
44
|
+
end
|
45
|
+
|
46
|
+
alias_method :[], :on
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def message_lookup(validator, *msg_opts)
|
51
|
+
validator.is_a?(Symbol) ? ::PoroValidator.configuration.message.get(
|
52
|
+
validator, *msg_opts
|
53
|
+
) : validator
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module PoroValidator
|
2
|
+
class Exceptions < StandardError
|
3
|
+
def initialize(message = nil, object = nil)
|
4
|
+
super(message)
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
class ConfigError < Exceptions; end
|
9
|
+
class ValidatorNotFound < Exceptions; end
|
10
|
+
class InvalidCondition < Exceptions; end
|
11
|
+
class InvalidType < Exceptions; end
|
12
|
+
class OverloadriddenRequired < Exceptions; end
|
13
|
+
class InvalidValidator < Exceptions; end
|
14
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module PoroValidator
|
2
|
+
module Validator
|
3
|
+
def self.included(base)
|
4
|
+
base.extend(ClassMethods)
|
5
|
+
end
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
def validates(attr_name, **options, &block)
|
9
|
+
if block_given?
|
10
|
+
nested_validations = build_nested_validations(attr_name, &block)
|
11
|
+
nested_validations.each do |nested_validation|
|
12
|
+
validations << nested_validation
|
13
|
+
end
|
14
|
+
else
|
15
|
+
validations.build(attr_name, **options)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def validations
|
20
|
+
@validations ||= build_validations
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def build_validations(validations = [])
|
26
|
+
Validations.build_validations(validations)
|
27
|
+
end
|
28
|
+
|
29
|
+
def validates_with(vals)
|
30
|
+
@validations = vals
|
31
|
+
end
|
32
|
+
|
33
|
+
def build_nested_validations(attr_name, &block)
|
34
|
+
nested_validator = Class.new do
|
35
|
+
include PoroValidator.validator
|
36
|
+
end
|
37
|
+
nested_validator.instance_eval(&block)
|
38
|
+
nested_validations = nested_validator.validations.validations
|
39
|
+
nested_validations.each do |nv|
|
40
|
+
nv[:validator].attribute = [attr_name, nv[:validator].attribute]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Ensures that when a Validator class is subclassed, the
|
45
|
+
# validation rules will be carried into the subclass as well.
|
46
|
+
#
|
47
|
+
# @example
|
48
|
+
# class PersonValidator
|
49
|
+
# include Veto.validator
|
50
|
+
# validates :name, presence: true
|
51
|
+
# end
|
52
|
+
#
|
53
|
+
# class CustomerValidator < PersonValidator
|
54
|
+
# validates :customer_id, presence: true
|
55
|
+
# end
|
56
|
+
#
|
57
|
+
# customer = Customer.new
|
58
|
+
# validator = CustomerValidator.new
|
59
|
+
# validator.validate!(customer) # => ["name is not present", "customer_id is not present"]
|
60
|
+
def inherited(descendant)
|
61
|
+
descendant.send(:validates_with, (build_validations(validations.validations.dup)))
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def errors
|
66
|
+
@errors ||= ::PoroValidator::Errors.new
|
67
|
+
end
|
68
|
+
|
69
|
+
def valid?(entity)
|
70
|
+
validate_entity(entity)
|
71
|
+
errors.empty?
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def validate_entity(entity)
|
77
|
+
errors.clear_errors
|
78
|
+
context = Context.new(entity, self, errors)
|
79
|
+
self.class.validations.run_validations(context)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module PoroValidator
|
2
|
+
module Validator
|
3
|
+
class BaseClass
|
4
|
+
# Public - Initializes the base class that creates a default value for
|
5
|
+
# the @validations variable with an empty array
|
6
|
+
#
|
7
|
+
# @validations - Stores all the validations intended for a Ruby object
|
8
|
+
# (entity)
|
9
|
+
def initialize
|
10
|
+
@validations = []
|
11
|
+
end
|
12
|
+
|
13
|
+
# Public - Calls the internal #__run_validations__ method to perform the
|
14
|
+
# validations stored in the @validations instance array against a
|
15
|
+
# validator context if the conditions are truthy.
|
16
|
+
def run_validations(context)
|
17
|
+
__run_validations__(context)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Adds/appends validations to the validations array
|
21
|
+
#
|
22
|
+
# @params[validation] - Validator Object to be stored to the array of
|
23
|
+
# validations.
|
24
|
+
def <<(validation)
|
25
|
+
@validations << validation
|
26
|
+
end
|
27
|
+
|
28
|
+
# @return array of hashes of validations and conditions
|
29
|
+
def validations
|
30
|
+
@validations
|
31
|
+
end
|
32
|
+
|
33
|
+
# @return array of validators
|
34
|
+
def validators
|
35
|
+
@validators
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
# @private
|
41
|
+
def __run_validations__(context)
|
42
|
+
validations.each do |validation|
|
43
|
+
validator = validation[:validator]
|
44
|
+
conditions = validation[:conditions] || {}
|
45
|
+
|
46
|
+
unless conditions.empty?
|
47
|
+
next unless Conditions.matched?(conditions, context)
|
48
|
+
end
|
49
|
+
|
50
|
+
validator.__validate__(context)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module PoroValidator
|
2
|
+
module Validator
|
3
|
+
class Conditions
|
4
|
+
def self.matched?(conditions, context)
|
5
|
+
results = []
|
6
|
+
inst = new(context)
|
7
|
+
|
8
|
+
conditions.each do |condition|
|
9
|
+
condition.each do |type, value|
|
10
|
+
unless [:if, :unless].include?(type)
|
11
|
+
raise ::PoroValidator::InvalidCondition.new(
|
12
|
+
"Unimplemented conditional key: #{type}"
|
13
|
+
)
|
14
|
+
end
|
15
|
+
|
16
|
+
if value.is_a?(::Array)
|
17
|
+
value.each do |v|
|
18
|
+
results << inst.match_condition(type, v)
|
19
|
+
end
|
20
|
+
next
|
21
|
+
end
|
22
|
+
|
23
|
+
results << inst.match_condition(type, value)
|
24
|
+
next
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
return false if results.include?(false)
|
29
|
+
true
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize(context)
|
33
|
+
@context = context
|
34
|
+
end
|
35
|
+
|
36
|
+
def match_condition(type, condition)
|
37
|
+
result =
|
38
|
+
case condition
|
39
|
+
when String
|
40
|
+
condition_type[String].call(context, condition)
|
41
|
+
when Symbol
|
42
|
+
condition_type[Symbol].call(context, condition)
|
43
|
+
when Proc
|
44
|
+
condition.call(context)
|
45
|
+
else
|
46
|
+
raise ::PoroValidator::InvalidCondition.new(
|
47
|
+
"Unimplemented condition: #{condition.inspect}"
|
48
|
+
)
|
49
|
+
end
|
50
|
+
|
51
|
+
if type == :unless
|
52
|
+
return !result
|
53
|
+
end
|
54
|
+
result
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def context
|
60
|
+
@context
|
61
|
+
end
|
62
|
+
|
63
|
+
def condition_type
|
64
|
+
{
|
65
|
+
String => lambda { |context, string| context.entity.instance_eval(string) },
|
66
|
+
Symbol => lambda { |context, method| context.send(method, context.entity) },
|
67
|
+
}
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|