vanilla_validator 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +8 -0
  3. data/Gemfile.lock +96 -0
  4. data/README.md +158 -0
  5. data/Rakefile +4 -0
  6. data/lib/locale/en.yml +87 -0
  7. data/lib/vanilla_validator/helpers.rb +87 -0
  8. data/lib/vanilla_validator/railtie.rb +39 -0
  9. data/lib/vanilla_validator/result.rb +15 -0
  10. data/lib/vanilla_validator/rule.rb +10 -0
  11. data/lib/vanilla_validator/rule_parser.rb +44 -0
  12. data/lib/vanilla_validator/rules/after.rb +13 -0
  13. data/lib/vanilla_validator/rules/after_or_equal.rb +13 -0
  14. data/lib/vanilla_validator/rules/base_rule.rb +17 -0
  15. data/lib/vanilla_validator/rules/before.rb +13 -0
  16. data/lib/vanilla_validator/rules/before_or_equal.rb +13 -0
  17. data/lib/vanilla_validator/rules/block_rule.rb +19 -0
  18. data/lib/vanilla_validator/rules/boolean.rb +13 -0
  19. data/lib/vanilla_validator/rules/date.rb +19 -0
  20. data/lib/vanilla_validator/rules/email.rb +13 -0
  21. data/lib/vanilla_validator/rules/eq.rb +13 -0
  22. data/lib/vanilla_validator/rules/falsy.rb +14 -0
  23. data/lib/vanilla_validator/rules/gte.rb +13 -0
  24. data/lib/vanilla_validator/rules/in.rb +13 -0
  25. data/lib/vanilla_validator/rules/like.rb +14 -0
  26. data/lib/vanilla_validator/rules/max.rb +13 -0
  27. data/lib/vanilla_validator/rules/min.rb +13 -0
  28. data/lib/vanilla_validator/rules/numeric.rb +14 -0
  29. data/lib/vanilla_validator/rules/other_rule.rb +40 -0
  30. data/lib/vanilla_validator/rules/required.rb +21 -0
  31. data/lib/vanilla_validator/rules/required_if.rb +15 -0
  32. data/lib/vanilla_validator/rules/url.rb +13 -0
  33. data/lib/vanilla_validator/value_extractor.rb +93 -0
  34. data/lib/vanilla_validator/version.rb +5 -0
  35. data/lib/vanilla_validator.rb +103 -0
  36. metadata +137 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: eb035f079a5c22180c9824294d979c0aeefd71fc71ff87233bf4520d64d81f14
4
+ data.tar.gz: 21e7fe1cd16419549f74a7ff9b9828adea78ff134fed11e6f5e95fefc9097653
5
+ SHA512:
6
+ metadata.gz: 4b796d246078765d99c74abd6b5891f9cd20c827717ea317f984a15737c2498365538850e72f10d56844ebc34466cb8f16b0d0a518d25b303ad5226fcdb03d85
7
+ data.tar.gz: 8a89d30b83e550f88b6b4eba8cfdcb91f91d32bfee03c5c7a12a719e1920e739c1db82d113ca8ea212c6a7a204f7a5cf42b022411dbbbe5ce2be615e188b914d
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in vanilla_validator.gemspec
6
+ gemspec
7
+
8
+ gem "actionpack"
data/Gemfile.lock ADDED
@@ -0,0 +1,96 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ vanilla_validator (0.1.4)
5
+ activesupport
6
+ i18n
7
+ zeitwerk (~> 2.5)
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ actionpack (7.1.1)
13
+ actionview (= 7.1.1)
14
+ activesupport (= 7.1.1)
15
+ nokogiri (>= 1.8.5)
16
+ rack (>= 2.2.4)
17
+ rack-session (>= 1.0.1)
18
+ rack-test (>= 0.6.3)
19
+ rails-dom-testing (~> 2.2)
20
+ rails-html-sanitizer (~> 1.6)
21
+ actionview (7.1.1)
22
+ activesupport (= 7.1.1)
23
+ builder (~> 3.1)
24
+ erubi (~> 1.11)
25
+ rails-dom-testing (~> 2.2)
26
+ rails-html-sanitizer (~> 1.6)
27
+ activesupport (7.1.1)
28
+ base64
29
+ bigdecimal
30
+ concurrent-ruby (~> 1.0, >= 1.0.2)
31
+ connection_pool (>= 2.2.5)
32
+ drb
33
+ i18n (>= 1.6, < 2)
34
+ minitest (>= 5.1)
35
+ mutex_m
36
+ tzinfo (~> 2.0)
37
+ base64 (0.1.1)
38
+ bigdecimal (3.1.4)
39
+ builder (3.2.4)
40
+ concurrent-ruby (1.1.10)
41
+ connection_pool (2.4.1)
42
+ crass (1.0.6)
43
+ diff-lcs (1.5.0)
44
+ drb (2.1.1)
45
+ ruby2_keywords
46
+ erubi (1.12.0)
47
+ i18n (1.12.0)
48
+ concurrent-ruby (~> 1.0)
49
+ loofah (2.21.4)
50
+ crass (~> 1.0.2)
51
+ nokogiri (>= 1.12.0)
52
+ minitest (5.20.0)
53
+ mutex_m (0.1.2)
54
+ nokogiri (1.15.4-x86_64-linux)
55
+ racc (~> 1.4)
56
+ racc (1.7.3)
57
+ rack (3.0.8)
58
+ rack-session (2.0.0)
59
+ rack (>= 3.0.0)
60
+ rack-test (2.1.0)
61
+ rack (>= 1.3)
62
+ rails-dom-testing (2.2.0)
63
+ activesupport (>= 5.0.0)
64
+ minitest
65
+ nokogiri (>= 1.6)
66
+ rails-html-sanitizer (1.6.0)
67
+ loofah (~> 2.21)
68
+ nokogiri (~> 1.14)
69
+ rspec (3.11.0)
70
+ rspec-core (~> 3.11.0)
71
+ rspec-expectations (~> 3.11.0)
72
+ rspec-mocks (~> 3.11.0)
73
+ rspec-core (3.11.0)
74
+ rspec-support (~> 3.11.0)
75
+ rspec-expectations (3.11.0)
76
+ diff-lcs (>= 1.2.0, < 2.0)
77
+ rspec-support (~> 3.11.0)
78
+ rspec-mocks (3.11.1)
79
+ diff-lcs (>= 1.2.0, < 2.0)
80
+ rspec-support (~> 3.11.0)
81
+ rspec-support (3.11.0)
82
+ ruby2_keywords (0.0.5)
83
+ tzinfo (2.0.6)
84
+ concurrent-ruby (~> 1.0)
85
+ zeitwerk (2.6.12)
86
+
87
+ PLATFORMS
88
+ x86_64-linux
89
+
90
+ DEPENDENCIES
91
+ actionpack
92
+ rspec (~> 3.2)
93
+ vanilla_validator!
94
+
95
+ BUNDLED WITH
96
+ 2.3.19
data/README.md ADDED
@@ -0,0 +1,158 @@
1
+ ## 🚧 Project Under Development 🚧
2
+
3
+ ## VanillaValidator
4
+
5
+ VanillaValidator is a lightweight and clean solution for implementing validation in Ruby. It inspired from Laravel Validator and allows you to separate validations from the model layer in your Rails applications. With this gem, you can easily define and enforce validation rules for input data, ensuring the consistency and cleanliness of your application's data.
6
+
7
+ ### Installation:
8
+ To use VanillaValidator in your Ruby project, execute the following command to install it:
9
+
10
+ ```ruby
11
+ $ bundle add vanilla_validator
12
+ ```
13
+
14
+ ### Basic Usage
15
+ There are two options to use VanillaValidator, in the first place you can invoke `validate` method directlly from `VanillaValidator` class and then result would be an object witch contains different methods to determine the result of validation.
16
+
17
+ ```ruby
18
+ params = {
19
+ user: {
20
+ email: 'john@doe.me'
21
+ }
22
+ }
23
+
24
+ validation = VanillaValidator.validate(params, {
25
+ 'user.email' => 'required|email'
26
+ })
27
+
28
+ validation.success?
29
+ validation.failed?
30
+ validation.validated
31
+ validation.errors
32
+
33
+
34
+ # In Rails, you can access the validate method directly:
35
+ params.validate({
36
+ 'user.email' => 'require|email'
37
+ })
38
+
39
+ # If you want validation to stop at the first failure, use a bang sign (!) after 'validate':
40
+ params.validate!({
41
+ 'user.password' => 'require|min:16'
42
+ })
43
+ ```
44
+
45
+ ### Rules Declaration
46
+ You have the flexibility to define validation rules either explicitly or implicitly, depending on your specific requirements. When taking the explicit approach, you must specify the exact attribute you wish to validate. To access nested attributes, you can employ a period (.) in the attribute path, as demonstrated below:
47
+
48
+ ```ruby
49
+ params = {
50
+ user: {
51
+ email: 'john@doe.me',
52
+ preferences: {
53
+ notifications: true
54
+ }
55
+ }
56
+ }
57
+
58
+ VanillaValidator.validate(params, {
59
+ 'user.email' => 'required|email',
60
+ 'user.preferences.notifications' => 'boolean'
61
+ })
62
+ ```
63
+
64
+ In cases where your input consists of a collection of items, you can specify the attributes implicitly. This can be achieved by using a wildcard (\*) in the attribute path, as shown in the following example:
65
+
66
+ ```ruby
67
+ params = {
68
+ user: {
69
+ addresses: [
70
+ { city: 'San Francisco', state: 'CA' },
71
+ { city: 'Los Angeles', state: 'CA' }
72
+ ],
73
+ orders: [
74
+ { total: 50.0, status: 'shipped' },
75
+ { total: 75.0, status: 'delivered' }
76
+ ]
77
+ }
78
+ }
79
+
80
+ validation = VanillaValidator.validate(params, {
81
+ 'user.addresses.*.state' => 'required|string|in:CA,NY',
82
+ 'user.orders.*.total' => 'required|numeric|min:0',
83
+ 'user.orders.*.status' => 'required|string|in:pending,shipped,delivered'
84
+ })
85
+ ```
86
+
87
+ ### Available Validation Rules
88
+ - [After](#after)
89
+ - [AfterOrEqual](#after_or_equal)
90
+ - [Before](#before)
91
+ - [BeforeOrEqual](#before_or_equal)
92
+ - [Boolean](#boolean)
93
+ - [Date](#date)
94
+ - [Email](#email)
95
+ - [EQ](#eq)
96
+ - [Falsy](#falsy)
97
+ - [Gte](#gte)
98
+ - [In](#in)
99
+ - [Like](#like)
100
+ - [Max](#max)
101
+ - [Min](#min)
102
+ - [Numeric](#numeric)
103
+ - [Required](#required)
104
+ - [RequiredIf](#required_if)
105
+ - [Url](#url)
106
+ - [Custom Validation Rules](#custom_validation_rules)
107
+
108
+ ##### After
109
+ The validated field must have a value that is after a specified date.
110
+
111
+ ```ruby
112
+ 'start_date' => 'required|date|after:tomorrow'
113
+ ```
114
+
115
+ ##### After Or Equal
116
+
117
+ ##### Before
118
+
119
+ ##### Before Or Equal
120
+
121
+ ##### Boolean
122
+
123
+ ##### Date
124
+
125
+ ##### Email
126
+
127
+ ##### EQ
128
+
129
+ ##### Falsy
130
+
131
+ ##### Gte
132
+
133
+ ##### In
134
+
135
+ ##### Like
136
+
137
+ ##### Max
138
+
139
+ ##### Min
140
+
141
+ ##### Numeric
142
+
143
+ ##### Required
144
+
145
+ ##### Required If
146
+
147
+ ##### Url
148
+
149
+ ##### Custom Validation Rules:
150
+
151
+ ### Contributions:
152
+ Contributions to this gem are welcome. Please read the [Contribution Guidelines](link-to-contributing) before submitting your contributions.
153
+
154
+ ### Reporting Issues:
155
+ If you encounter any issues or have suggestions for improvements, please open an issue on the [GitHub repository](link-to-issues).
156
+
157
+ ### License:
158
+ This gem is available under the [MIT License](https://choosealicense.com/licenses/mit/).
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ task default: %i[]
data/lib/locale/en.yml ADDED
@@ -0,0 +1,87 @@
1
+ en:
2
+ url: 'The %{attribute} must be a valid URL.'
3
+ after: 'The %{attribute} must be a date after %{date}.'
4
+ after_or_equal: 'The %{attribute} must be a date after or equal to %{date}.'
5
+ before: 'The %{attribute} must be a date before %{date}.'
6
+ before_or_equal: 'The %{attribute} must be a date before or equal to %{date}.'
7
+ alpha: 'The %{attribute} must only contain letters.'
8
+ alpha_dash: 'The %{attribute} must only contain letters, numbers, dashes and underscores.'
9
+ alpha_num: 'The %{attribute} must only contain letters and numbers.'
10
+ array: 'The %{attribute} must be an array.'
11
+ between:
12
+ array: 'The %{attribute} must have between %{min} and %{max} items.'
13
+ file: 'The %{attribute} must be between %{min} and %{max} kilobytes.'
14
+ numeric: 'The %{attribute} must be between %{min} and %{max}.'
15
+ string: 'The %{attribute} must be between %{min} and %{max} characters.'
16
+ boolean: 'The %{attribute} field must be true or false.'
17
+ falsy: 'The %{attribute} is not a falsy value.'
18
+ eq: 'The %{attribute} should be equal to %{value}'
19
+ like: 'The %{attribute} should be like %{other_attribute}'
20
+ date: 'The %{attribute} is not a valid date.'
21
+ date_equals: 'The %{attribute} must be a date equal to %{date}.'
22
+ date_format: 'The %{attribute} does not match the format %{format}.'
23
+ different: 'The %{attribute} and %{other} must be different.'
24
+ digits: 'The %{attribute} must be %{digits} digits.'
25
+ digits_between: 'The %{attribute} must be between %{min} and %{max} digits.'
26
+ distinct: 'The %{attribute} field has a duplicate value.'
27
+ email: 'The %{attribute} must be a valid email address.'
28
+ enum: 'The selected %{attribute} is invalid.'
29
+ exists: 'The selected %{attribute} is invalid.'
30
+ file: 'The %{attribute} must be a file.'
31
+ filled: 'The %{attribute} field must have a value.'
32
+ gt:
33
+ array: 'The %{attribute} must have more than %{value} items.'
34
+ file: 'The %{attribute} must be greater than %{value} kilobytes.'
35
+ numeric: 'The %{attribute} must be greater than %{value}.'
36
+ string: 'The %{attribute} must be greater than %{value} characters.'
37
+ gte:
38
+ array: 'The %{attribute} must have %{value} items or more.'
39
+ file: 'The %{attribute} must be greater than or equal to %{value} kilobytes.'
40
+ numeric: 'The %{attribute} must be greater than or equal to %{value}.'
41
+ string: 'The %{attribute} must be greater than or equal to %{value} characters.'
42
+ image: 'The %{attribute} must be an image.'
43
+ in: 'The selected %{attribute} is invalid.'
44
+ integer: 'The %{attribute} must be an integer.'
45
+ ip: 'The %{attribute} must be a valid IP address.'
46
+ ipv4: 'The %{attribute} must be a valid IPv4 address.'
47
+ ipv6: 'The %{attribute} must be a valid IPv6 address.'
48
+ json: 'The %{attribute} must be a valid JSON string.'
49
+ lt:
50
+ array: 'The %{attribute} must have less than %{value} items.'
51
+ file: 'The %{attribute} must be less than %{value} kilobytes.'
52
+ numeric: 'The %{attribute} must be less than %{value}.'
53
+ string: 'The %{attribute} must be less than %{value} characters.'
54
+ lte:
55
+ array: 'The %{attribute} must not have more than %{value} items.'
56
+ file: 'The %{attribute} must be less than or equal to %{value} kilobytes.'
57
+ numeric: 'The %{attribute} must be less than or equal to %{value}.'
58
+ string: 'The %{attribute} must be less than or equal to %{value} characters.'
59
+ mac_address: 'The %{attribute} must be a valid MAC address.'
60
+ max:
61
+ array: 'The %{attribute} must not have more than %{max} items.'
62
+ file: 'The %{attribute} must not be greater than %{max} kilobytes.'
63
+ numeric: 'The %{attribute} must not be greater than %{max}.'
64
+ string: 'The %{attribute} must not be greater than %{max} characters.'
65
+ max_digits: 'The %{attribute} must not have more than %{max} digits.'
66
+ mimes: 'The %{attribute} must be a file of type: %{values}.'
67
+ mimetypes: 'The %{attribute} must be a file of type: %{values}.'
68
+ min:
69
+ array: 'The %{attribute} must have at least %{min} items.'
70
+ file: 'The %{attribute} must be at least %{min} kilobytes.'
71
+ numeric: 'The %{attribute} must be at least %{min}.'
72
+ string: 'The %{attribute} must be at least %{min} characters.'
73
+ min_digits: 'The %{attribute} must have at least %{min} digits.'
74
+ not_in: 'The selected %{attribute} is invalid.'
75
+ numeric: 'The %{attribute} must be a number.'
76
+ present: 'The %{attribute} field must be present.'
77
+ regex: 'The %{attribute} format is invalid.'
78
+ required: 'The %{attribute} field is required.'
79
+ required_if: 'The %{attribute} field is required when %{other} is %{value}.'
80
+ required_unless: 'The %{attribute} field is required unless %{other} is in %{value}s.'
81
+ unique: 'The %{attribute} has already been taken.'
82
+ custom:
83
+ attribute_name:
84
+ rule_name: 'Message'
85
+ foo:
86
+ proc: 'The %{attribute} does not statisfied'
87
+
@@ -0,0 +1,87 @@
1
+ module VanillaValidator
2
+ # Helpers module contains various utility methods used by the VanillaValidator module.
3
+ module Helpers
4
+ private
5
+
6
+ # Private: Deep clone the input data while nullifying certain values.
7
+ #
8
+ # data - The input data to clone.
9
+ #
10
+ # Returns: A deep clone of the input data with specific values nullified.
11
+ #
12
+ def deep_clone_input(data)
13
+ new_input = Marshal.load(Marshal.dump(data))
14
+ nullify_values(new_input)
15
+ new_input
16
+ end
17
+
18
+ # Private: Recursively nullify values in a hash.
19
+ #
20
+ # hash - The hash in which values are to be nullified.
21
+ #
22
+ # Returns: The hash with specific values replaced by '__missing__'.
23
+ #
24
+ def nullify_values(hash)
25
+ hash.each do |key, value|
26
+ if value.is_a?(Hash)
27
+ nullify_values(value)
28
+ elsif value.is_a?(Array)
29
+ value.each { |item| nullify_values(item) if item.is_a?(Hash) }
30
+ else
31
+ hash[key] = '__missing__'
32
+ end
33
+ end
34
+ end
35
+
36
+ # Private: Set data within a nested structure based on a provided key.
37
+ #
38
+ # array - The nested data structure to modify.
39
+ # key - The key specifying the location to set the value.
40
+ # value - The value to set.
41
+ #
42
+ # Returns: The modified nested data structure.
43
+ #
44
+ def data_set(array, key, value)
45
+ keys = key.split(".")
46
+ last_key = keys.pop
47
+
48
+ target = keys.reduce(array) do |hash, k|
49
+ case k
50
+ when "*"
51
+ hash.last ||= {}
52
+ hash.last
53
+ else
54
+ hash[k] ||= {}
55
+ hash[k]
56
+ end
57
+ end
58
+
59
+ if last_key == "*"
60
+ target << value
61
+ else
62
+ target[last_key] = value
63
+ end
64
+
65
+ array
66
+ end
67
+
68
+ # Private: Remove entries with '__missing__' values from a hash.
69
+ #
70
+ # hash - The hash from which entries with '__missing__' values are to be removed.
71
+ #
72
+ # Returns: The hash with '__missing__' values removed.
73
+ #
74
+ def delete_missing_values(hash)
75
+ hash.each do |k, v|
76
+ if v == '__missing__'
77
+ hash.delete(k)
78
+ elsif v.kind_of?(Array)
79
+ hash[k] = v.reject { |item| item == '__missing__' }
80
+ elsif v.kind_of?(Hash)
81
+ hash[k] = delete_missing_values(v)
82
+ end
83
+ end
84
+ return hash
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The VanillaValidator::Railtie class is responsible for integrating validation
4
+ # methods into ActionController::Parameters in a Ruby on Rails application.
5
+
6
+ require 'rails/railtie'
7
+
8
+ module VanillaValidator
9
+ class Railtie < ::Rails::Railtie
10
+ # Initializes and configures the integration of validation methods.
11
+ initializer 'vanilla_validator' do
12
+ ActiveSupport.on_load :action_controller do
13
+ # Include the ValidationExtensions module within ActionController::Parameters.
14
+ ActionController::Parameters.include(ValidationExtensions)
15
+ end
16
+ end
17
+ end
18
+
19
+ # The ValidationExtensions module provides methods for validating
20
+ # ActionController::Parameters against a specified contract.
21
+ module ValidationExtensions
22
+ # Validates the ActionController::Parameters against the specified contract.
23
+ #
24
+ # @param contract [Hash] A contract specifying validation rules.
25
+ #
26
+ # @return [Object] The result of the validation.
27
+ def validate(contract)
28
+ VanillaValidator.validate(self.to_unsafe_h, contract)
29
+ end
30
+
31
+ # Validates the ActionController::Parameters against the specified contract
32
+ # and raises an exception if the validation fails.
33
+ #
34
+ # @param contract [Hash] A contract specifying validation rules.
35
+ def validate!(contract)
36
+ VanillaValidator.validate!(self.to_unsafe_h, contract)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,15 @@
1
+ module VanillaValidator
2
+ # Result represents the outcome of a validation operation, including validated data and errors.
3
+ class Result
4
+ attr_accessor :validated, :errors
5
+
6
+ def initialize(validated, errors = {})
7
+ @validated = validated
8
+ @errors = errors
9
+ end
10
+
11
+ def valid?
12
+ errors.empty?
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,10 @@
1
+ module VanillaValidator
2
+ # Rule represents a validation rule with a name and optional parameters.
3
+ class Rule
4
+ attr_accessor :name, :parameters
5
+
6
+ def initialize(name, parameters)
7
+ @name, @parameters = name, parameters
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VanillaValidator
4
+ # RuleParser is responsible for parsing validation rules defined in a contract term.
5
+ class RuleParser
6
+ # Public: Parse a contract term to extract validation rules.
7
+ #
8
+ # term - The contract term to parse, which may include one or more validation rules separated by '|'.
9
+ #
10
+ # Returns: An array of Rule objects representing the parsed validation rules.
11
+ #
12
+ # Examples:
13
+ # RuleParser.parse('required|min:5') #=> [Rule.new('required', []), Rule.new('min_length', ['5'])]
14
+ #
15
+ def self.parse(term)
16
+ if term.respond_to?(:call)
17
+ [Rule.new('block_rule', term)]
18
+ elsif term.respond_to?(:valid?)
19
+ [Rule.new('custom_rule', term)]
20
+ else
21
+ parse_rules(term)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ # Private: Parse individual validation rules from a contract term.
28
+ #
29
+ # term - The contract term to parse, which may include one or more validation rules separated by '|'.
30
+ #
31
+ # Returns: An array of Rule objects representing the parsed validation rules.
32
+ #
33
+ def self.parse_rules(term)
34
+ rules = term.split('|')
35
+
36
+ rules.map do |rule|
37
+ rule_name, params = rule.split(':') if rule.respond_to?(:split)
38
+ parameters = params.split(',') if params.respond_to?(:split)
39
+
40
+ Rule.new(rule_name, parameters)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,13 @@
1
+ module VanillaValidator
2
+ module Rules
3
+ class After < BaseRule
4
+ def valid?
5
+ ::Date.parse(value, '%Y-%m-%d') > ::Date.parse(parameters[0], '%Y-%m-%d')
6
+ end
7
+
8
+ def failure_message
9
+ I18n.t('after', attribute: attribute, date: parameters[0])
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module VanillaValidator
2
+ module Rules
3
+ class AfterOrEqual < BaseRule
4
+ def valid?
5
+ ::Date.parse(value, '%Y-%m-%d') >= ::Date.parse(parameters[0], '%Y-%m-%d')
6
+ end
7
+
8
+ def failure_message
9
+ I18n.t('after_or_equal', attribute: attribute, date: parameters[0])
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,17 @@
1
+ module VanillaValidator
2
+ module Rules
3
+ class BaseRule
4
+ attr_accessor :attribute, :value, :parameters
5
+
6
+ def initialize(attribute, value, parameters)
7
+ @attribute, @value, @parameters = attribute, value, parameters
8
+ end
9
+
10
+ private
11
+
12
+ def required?
13
+ Rules::Required.new(attribute, value, parameters).valid?
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,13 @@
1
+ module VanillaValidator
2
+ module Rules
3
+ class Before < BaseRule
4
+ def valid?
5
+ ::Date.parse(value, '%Y-%m-%d') < ::Date.parse(parameters[0], '%Y-%m-%d')
6
+ end
7
+
8
+ def failure_message
9
+ I18n.t('after', attribute: attribute, date: parameters[0])
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module VanillaValidator
2
+ module Rules
3
+ class BeforeOrEqual < BaseRule
4
+ def valid?
5
+ ::Date.parse(value, '%Y-%m-%d') <= ::Date.parse(parameters[0], '%Y-%m-%d')
6
+ end
7
+
8
+ def failure_message
9
+ I18n.t('before_or_equal', attribute: attribute, date: parameters[0])
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,19 @@
1
+ module VanillaValidator
2
+ module Rules
3
+ class BlockRule < BaseRule
4
+
5
+ attr_accessor :message
6
+
7
+ def valid?
8
+ block = parameters
9
+ block.call(attribute, value, ->(msg){ self.message = msg })
10
+
11
+ message.nil? ? true : false
12
+ end
13
+
14
+ def failure_message
15
+ message
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,13 @@
1
+ module VanillaValidator
2
+ module Rules
3
+ class Boolean < BaseRule
4
+ def valid?
5
+ !!value == value
6
+ end
7
+
8
+ def failure_message
9
+ I18n.t('boolean', attribute: attribute)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,19 @@
1
+ module VanillaValidator
2
+ module Rules
3
+ class Date < BaseRule
4
+ def valid?
5
+ begin
6
+ ::Date.strptime(value, '%Y-%m-%d')
7
+ true
8
+ rescue
9
+ # ArgumentError, TypeError
10
+ false
11
+ end
12
+ end
13
+
14
+ def failure_message
15
+ I18n.t('date', attribute: attribute)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,13 @@
1
+ module VanillaValidator
2
+ module Rules
3
+ class Email < BaseRule
4
+ def valid?
5
+ required? && value =~ /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
6
+ end
7
+
8
+ def failure_message
9
+ I18n.t('email', attribute: attribute)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module VanillaValidator
2
+ module Rules
3
+ class Eq < BaseRule
4
+ def valid?
5
+ value == parameters[0].to_i
6
+ end
7
+
8
+ def failure_message
9
+ I18n.t("eq", attribute: attribute, value: parameters[0].to_i)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,14 @@
1
+ module VanillaValidator
2
+ module Rules
3
+ class Falsy < BaseRule
4
+ def valid?
5
+ acceptable = ['no', 'off', '0', 0, false, 'false']
6
+ required? && acceptable.include?(value)
7
+ end
8
+
9
+ def failure_message
10
+ I18n.t('falsy', attribute: attribute)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ module VanillaValidator
2
+ module Rules
3
+ class Gte < BaseRule
4
+ def valid?
5
+ value >= parameters[0].to_i
6
+ end
7
+
8
+ def failure_message
9
+ I18n.t("gte.numeric", attribute: attribute, value: parameters[0].to_i)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module VanillaValidator
2
+ module Rules
3
+ class In < BaseRule
4
+ def valid?
5
+ parameters.include? value.to_s
6
+ end
7
+
8
+ def failure_message
9
+ I18n.t('in', attribute: attribute)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,14 @@
1
+ module VanillaValidator
2
+ module Rules
3
+ class Like < BaseRule
4
+ def valid?
5
+ other_value = ValueExtractor.get(VanillaValidator.raw_input, parameters[0])
6
+ value == other_value
7
+ end
8
+
9
+ def failure_message
10
+ I18n.t('like', attribute: attribute, other_attribute: parameters[0])
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ module VanillaValidator
2
+ module Rules
3
+ class Max < BaseRule
4
+ def valid?
5
+ value.length <= parameters[0].to_i
6
+ end
7
+
8
+ def failure_message
9
+ I18n.t("max.string", attribute: attribute, max: parameters[0].to_i)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module VanillaValidator
2
+ module Rules
3
+ class Min < BaseRule
4
+ def valid?
5
+ value.length >= parameters[0].to_i
6
+ end
7
+
8
+ def failure_message
9
+ I18n.t("min.string", attribute: attribute, min: parameters[0].to_i)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,14 @@
1
+ module VanillaValidator
2
+ module Rules
3
+ class Numeric < BaseRule
4
+ def valid?
5
+ return true if value.kind_of?(Numeric)
6
+ !!(Integer(value) rescue Float(value)) rescue false
7
+ end
8
+
9
+ def failure_message
10
+ I18n.t("numeric", attribute: attribute)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,40 @@
1
+ module VanillaValidator
2
+ module Rules
3
+ class OtherRule
4
+ attr_accessor :name, :attribute, :value, :parameters, :klass
5
+
6
+ DEFAULT_RULE_NAME = 'eq'.freeze
7
+
8
+ def initialize(input, term_array)
9
+ self.attribute = term_array[0]
10
+ self.name = term_array.length == 2 ? DEFAULT_RULE_NAME : term_array[1]
11
+ self.parameters = self.normalize(term_array[name == DEFAULT_RULE_NAME ? 1..-1 : 2..-1])
12
+ self.value = ValueExtractor.get(input, attribute)
13
+ self.klass = self.initialize_rule(name, value, parameters)
14
+ end
15
+
16
+ def self.call(input, term_array)
17
+ self.new(input, term_array)
18
+ end
19
+
20
+ private
21
+
22
+ def normalize(parameters)
23
+ parameters.map do |parameter|
24
+ case parameter.downcase
25
+ when 'true' then true
26
+ when 'false' then false
27
+ when /\A\d+\z/ then parameter.to_i
28
+ when /\A\d+\.\d+\z/ then parameter.to_f
29
+ else parameter
30
+ end
31
+ end
32
+ end
33
+
34
+ def initialize_rule(name, value, parameters)
35
+ rule_class = Rules.const_get(name.camelize)
36
+ rule_class.new(name, value, parameters)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,21 @@
1
+ module VanillaValidator
2
+ module Rules
3
+ class Required < BaseRule
4
+ def valid?
5
+ if value.nil?
6
+ return false
7
+ elsif value.class == String && value.strip == ''
8
+ return false
9
+ elsif value.class == Array && value.count < 1
10
+ return false
11
+ end
12
+
13
+ return true
14
+ end
15
+
16
+ def failure_message
17
+ I18n.t('required', attribute: attribute)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,15 @@
1
+ module VanillaValidator
2
+ module Rules
3
+ class RequiredIf < BaseRule
4
+ def valid?
5
+ other_rule = VanillaValidator::Rules::OtherRule.(VanillaValidator.raw_input, parameters)
6
+
7
+ other_rule.klass.valid? && required?
8
+ end
9
+
10
+ def failure_message
11
+ I18n.t("required_if", attribute: attribute, other: parameters[0].to_i, value: value)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,13 @@
1
+ module VanillaValidator
2
+ module Rules
3
+ class Url < BaseRule
4
+ def valid?
5
+ value =~ URI::regexp
6
+ end
7
+
8
+ def failure_message
9
+ I18n.t('url', attribute: attribute)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VanillaValidator
4
+ class ValueExtractor
5
+ # Public: Extracts data from a nested structure using a path.
6
+ #
7
+ # data - The nested data structure (Hash or Array) to extract data from.
8
+ # path - A string representing the path to the desired data, where
9
+ # individual segments are separated by periods ('.').
10
+ #
11
+ # Raises:
12
+ # - RuntimeError: If the path contains more than 5 segments, which
13
+ # exceeds the maximum allowed.
14
+ #
15
+ # Returns: The extracted data, or nil if the path does not lead to a valid value.
16
+ #
17
+ def self.get(data, path)
18
+ splited_path = path.to_s.split('.')
19
+
20
+ raise "Too many path segments (maximum allowed is 5)" if splited_path.length > 5
21
+
22
+ if path.include?("*")
23
+ self.get_at_wildcard_path(data, splited_path)
24
+ else
25
+ self.get_at_explicit_path(data, splited_path)
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ # Private: Extracts data from a nested structure using an explicit path.
32
+ #
33
+ # data - The nested data structure (Hash or Array) to extract data from.
34
+ # path - An array of path segments.
35
+ #
36
+ # Returns: The extracted data, or nil if the path does not lead to a valid value.
37
+ #
38
+ def self.get_at_explicit_path(data, path)
39
+ data.dig(*steps_from(path))
40
+ end
41
+
42
+ # Private: Converts an array of path segments into appropriate data access steps.
43
+ #
44
+ # path - An array of path segments.
45
+ #
46
+ # Returns: An array of steps for data access (string keys or integer indices).
47
+ #
48
+ def self.steps_from path
49
+ path.map do |step|
50
+ step.match?(/\D/) ? step.to_s : step.to_i
51
+ end
52
+ end
53
+
54
+ # Private: Extracts data from a nested structure using a path that includes wildcard segments ('*').
55
+ #
56
+ # data - The nested data structure (Array) to extract data from.
57
+ # path - An array of path segments with wildcard(s).
58
+ # default - The value to return if the path does not lead to a valid value.
59
+ #
60
+ # Returns: The extracted data or the default value if the path does not lead to a valid value.
61
+ #
62
+ def self.get_at_wildcard_path(data, path, default = nil)
63
+ return data if path.empty?
64
+
65
+ path.each_with_index do |segment, index|
66
+ return data if segment.nil?
67
+
68
+ if segment.eql?("*")
69
+ rpath = path[index.next..-1] || []
70
+
71
+ unless data.is_a?(Array)
72
+ return default
73
+ end
74
+
75
+ result = data.map { |item| get_at_wildcard_path(item, rpath) }
76
+
77
+ return rpath.include?("*") ? result.flatten : result
78
+ end
79
+
80
+ if data.is_a?(Hash) && data.key?(segment)
81
+ data = data[segment]
82
+ elsif target.is_a?(Array) && ( Integer(segment) rescue false )
83
+ target = target[Integer(segment)]
84
+ else
85
+ return default
86
+ end
87
+
88
+ end
89
+
90
+ return data
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VanillaValidator
4
+ VERSION = "0.4.0"
5
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'i18n'
4
+ require 'uri'
5
+ require 'date'
6
+ require 'active_support/inflector'
7
+ require "zeitwerk"
8
+
9
+ # Configure Zeitwerk to load the classes and modules.
10
+ loader = Zeitwerk::Loader.for_gem
11
+ loader.ignore("#{__dir__}/vanilla_validator/railtie.rb")
12
+ loader.setup
13
+
14
+ # Add custom locales for I18n.
15
+ I18n.load_path += Dir[File.dirname(__FILE__) + "/locale/*.yml"]
16
+
17
+ # VanillaValidator is a Ruby module that provides validation functionality.
18
+ module VanillaValidator
19
+ extend self
20
+
21
+ # Extend the module with the methods from the Helpers module.
22
+ extend Helpers
23
+
24
+ # Public: Validate input data against a contract.
25
+ #
26
+ # input - The input data to be validated.
27
+ # contract - A set of validation rules defined as a Hash.
28
+ # options - A Hash of additional options for validation (optional).
29
+ #
30
+ # NOTE: The `@raw_input` instance variable is used in nested rules to prevent excessive parameter passing.
31
+ #
32
+ # Returns a Result object containing the validated attributes and any errors.
33
+ def validate(input, contract, options = {})
34
+ @raw_input = input.dup
35
+
36
+ errors = {}
37
+ validated = deep_clone_input(input)
38
+ stop_on_first_failure = options[:stop_on_first_failure]
39
+
40
+ contract.each do |attribute, term|
41
+ value = ValueExtractor.get(input, attribute)
42
+ rules = RuleParser.parse(term)
43
+
44
+ initialized_rules = rules.map do |rule|
45
+ initialize_rule(attribute, value, rule)
46
+ end
47
+
48
+ invalid_rules = initialized_rules.reject(&:valid?)
49
+
50
+ if invalid_rules.empty?
51
+ data_set(validated, attribute, value)
52
+ else
53
+ invalid_rules.each do |result|
54
+ (errors[attribute] ||= []) << result.failure_message
55
+ end
56
+
57
+ if stop_on_first_failure
58
+ validated_attributes = delete_missing_values(validated)
59
+ return Result.new(validated_attributes, errors)
60
+ end
61
+ end
62
+ end
63
+
64
+ # Remove any missing or invalid attributes from the validated data.
65
+ validated_attributes = delete_missing_values(validated)
66
+
67
+ Result.new(validated_attributes, errors)
68
+ end
69
+
70
+ # Public: Validate input data against a contract, stopping on the first failure.
71
+ #
72
+ # input - The input data to validate.
73
+ # contract - A contract specifying validation rules for input data.
74
+ #
75
+ # Returns: A Result object containing validated data and error messages.
76
+ #
77
+ def validate!(input, contract)
78
+ validate(input, contract, stop_on_first_failure: true)
79
+ end
80
+
81
+ def raw_input
82
+ @raw_input
83
+ end
84
+
85
+ private
86
+
87
+ # Private: Initialize a validation rule based on its name and parameters.
88
+ #
89
+ # attribute - The attribute being validated.
90
+ # value - The value to validate.
91
+ # rule - A rule object containing the name and parameters of the rule.
92
+ #
93
+ # Returns: An instance of the specific validation rule.
94
+ #
95
+ def initialize_rule(attribute, value, rule)
96
+ rule_class = Rules.const_get(rule.name.camelize)
97
+ rule_class.new(attribute, value, rule.parameters)
98
+ end
99
+ end
100
+
101
+ loader.eager_load
102
+
103
+ require 'vanilla_validator/railtie' if defined? Rails
metadata ADDED
@@ -0,0 +1,137 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: vanilla_validator
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.0
5
+ platform: ruby
6
+ authors:
7
+ - Farid Mohammadi
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-01-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: i18n
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.2'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: zeitwerk
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.5'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.5'
55
+ - !ruby/object:Gem::Dependency
56
+ name: activesupport
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: Simple and easy to use validator inspired by Laravel Validator
70
+ email:
71
+ - 1997farid.mohammadi@gmail.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files:
75
+ - README.md
76
+ files:
77
+ - Gemfile
78
+ - Gemfile.lock
79
+ - README.md
80
+ - Rakefile
81
+ - lib/locale/en.yml
82
+ - lib/vanilla_validator.rb
83
+ - lib/vanilla_validator/helpers.rb
84
+ - lib/vanilla_validator/railtie.rb
85
+ - lib/vanilla_validator/result.rb
86
+ - lib/vanilla_validator/rule.rb
87
+ - lib/vanilla_validator/rule_parser.rb
88
+ - lib/vanilla_validator/rules/after.rb
89
+ - lib/vanilla_validator/rules/after_or_equal.rb
90
+ - lib/vanilla_validator/rules/base_rule.rb
91
+ - lib/vanilla_validator/rules/before.rb
92
+ - lib/vanilla_validator/rules/before_or_equal.rb
93
+ - lib/vanilla_validator/rules/block_rule.rb
94
+ - lib/vanilla_validator/rules/boolean.rb
95
+ - lib/vanilla_validator/rules/date.rb
96
+ - lib/vanilla_validator/rules/email.rb
97
+ - lib/vanilla_validator/rules/eq.rb
98
+ - lib/vanilla_validator/rules/falsy.rb
99
+ - lib/vanilla_validator/rules/gte.rb
100
+ - lib/vanilla_validator/rules/in.rb
101
+ - lib/vanilla_validator/rules/like.rb
102
+ - lib/vanilla_validator/rules/max.rb
103
+ - lib/vanilla_validator/rules/min.rb
104
+ - lib/vanilla_validator/rules/numeric.rb
105
+ - lib/vanilla_validator/rules/other_rule.rb
106
+ - lib/vanilla_validator/rules/required.rb
107
+ - lib/vanilla_validator/rules/required_if.rb
108
+ - lib/vanilla_validator/rules/url.rb
109
+ - lib/vanilla_validator/value_extractor.rb
110
+ - lib/vanilla_validator/version.rb
111
+ homepage: https://github.com/leurias/vanilla_validator
112
+ licenses: []
113
+ metadata:
114
+ allowed_push_host: https://rubygems.org
115
+ homepage_uri: https://github.com/leurias/vanilla_validator
116
+ source_code_uri: https://github.com/leurias/vanilla_validator
117
+ changelog_uri: https://github.com/leurias/vanilla_validator/CHANGELOG.md
118
+ post_install_message:
119
+ rdoc_options: []
120
+ require_paths:
121
+ - lib
122
+ required_ruby_version: !ruby/object:Gem::Requirement
123
+ requirements:
124
+ - - ">="
125
+ - !ruby/object:Gem::Version
126
+ version: 2.6.0
127
+ required_rubygems_version: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ requirements: []
133
+ rubygems_version: 3.5.4
134
+ signing_key:
135
+ specification_version: 4
136
+ summary: Easy to use ruby validator
137
+ test_files: []