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.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/CODE_OF_CONDUCT.md +13 -0
  3. data/Gemfile +11 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +382 -0
  6. data/Rakefile +139 -0
  7. data/lib/poro_validator.rb +54 -0
  8. data/lib/poro_validator/configuration.rb +63 -0
  9. data/lib/poro_validator/error_store.rb +52 -0
  10. data/lib/poro_validator/errors.rb +56 -0
  11. data/lib/poro_validator/exceptions.rb +14 -0
  12. data/lib/poro_validator/validator.rb +82 -0
  13. data/lib/poro_validator/validator/base_class.rb +55 -0
  14. data/lib/poro_validator/validator/conditions.rb +71 -0
  15. data/lib/poro_validator/validator/context.rb +19 -0
  16. data/lib/poro_validator/validator/factory.rb +32 -0
  17. data/lib/poro_validator/validator/validation.rb +50 -0
  18. data/lib/poro_validator/validator/validations.rb +55 -0
  19. data/lib/poro_validator/validators/base_class.rb +53 -0
  20. data/lib/poro_validator/validators/exclusion_validator.rb +26 -0
  21. data/lib/poro_validator/validators/float_validator.rb +17 -0
  22. data/lib/poro_validator/validators/format_validator.rb +16 -0
  23. data/lib/poro_validator/validators/inclusion_validator.rb +26 -0
  24. data/lib/poro_validator/validators/integer_validator.rb +17 -0
  25. data/lib/poro_validator/validators/length_validator.rb +41 -0
  26. data/lib/poro_validator/validators/numeric_validator.rb +48 -0
  27. data/lib/poro_validator/validators/presence_validator.rb +23 -0
  28. data/lib/poro_validator/validators/range_array_validator.rb +19 -0
  29. data/lib/poro_validator/validators/with_validator.rb +21 -0
  30. data/lib/poro_validator/version.rb +3 -0
  31. data/poro_validator.gemspec +97 -0
  32. data/spec/features/composable_validations_spec.rb +26 -0
  33. data/spec/features/inheritable_spec.rb +29 -0
  34. data/spec/features/nested_validations_spec.rb +136 -0
  35. data/spec/lib/poro_validator/configuration_spec.rb +37 -0
  36. data/spec/lib/poro_validator/error_store_spec.rb +125 -0
  37. data/spec/lib/poro_validator/errors_spec.rb +79 -0
  38. data/spec/lib/poro_validator/validator/base_class_spec.rb +62 -0
  39. data/spec/lib/poro_validator/validator/conditions_spec.rb +112 -0
  40. data/spec/lib/poro_validator/validator/factory_spec.rb +23 -0
  41. data/spec/lib/poro_validator/validator/validation_spec.rb +69 -0
  42. data/spec/lib/poro_validator/validator/validations_spec.rb +34 -0
  43. data/spec/lib/poro_validator/validator_spec.rb +55 -0
  44. data/spec/lib/poro_validator/validators/base_class_spec.rb +11 -0
  45. data/spec/lib/poro_validator/validators/exclusion_validator_spec.rb +81 -0
  46. data/spec/lib/poro_validator/validators/float_validator_spec.rb +43 -0
  47. data/spec/lib/poro_validator/validators/format_validator_spec.rb +48 -0
  48. data/spec/lib/poro_validator/validators/inclusion_validator_spec.rb +81 -0
  49. data/spec/lib/poro_validator/validators/integer_validator_spec.rb +43 -0
  50. data/spec/lib/poro_validator/validators/length_validator_spec.rb +64 -0
  51. data/spec/lib/poro_validator/validators/numeric_validator_spec.rb +68 -0
  52. data/spec/lib/poro_validator/validators/presence_validator_spec.rb +52 -0
  53. data/spec/lib/poro_validator/validators/with_validator_spec.rb +90 -0
  54. data/spec/poro_validator_spec.rb +25 -0
  55. data/spec/spec_helper.rb +34 -0
  56. data/spec/support/spec_helpers/concerns.rb +46 -0
  57. data/spec/support/spec_helpers/validator_test_macros.rb +99 -0
  58. 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