ruby-livr 2.0.0 → 2.1.0

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 (92) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/test.yml +23 -0
  3. data/.gitignore +1 -0
  4. data/Readme.md +2 -2
  5. data/lib/livr/aliased_rule.rb +11 -5
  6. data/lib/livr/rule.rb +13 -0
  7. data/lib/livr/rules/common.rb +1 -2
  8. data/lib/livr/rules/meta.rb +43 -12
  9. data/lib/livr/rules/modifiers.rb +10 -10
  10. data/lib/livr/rules/numeric.rb +5 -5
  11. data/lib/livr/rules/special.rb +14 -9
  12. data/lib/livr/validator.rb +7 -2
  13. data/lib/livr.rb +2 -0
  14. data/ruby-livr.gemspec +5 -4
  15. data/spec/01-test_suite_spec.rb +7 -2
  16. data/spec/02-aliases_spec.rb +30 -1
  17. data/spec/04-custom_filters_spec.rb +12 -1
  18. data/spec/06-primitive_values_spec.rb +133 -0
  19. data/spec/test_suite/aliases_negative/02-address/aliases.json +2 -2
  20. data/spec/test_suite/aliases_negative/02-address/input.json +2 -2
  21. data/spec/test_suite/aliases_positive/01-adult_age/input.json +1 -0
  22. data/spec/test_suite/aliases_positive/01-adult_age/output.json +1 -0
  23. data/spec/test_suite/aliases_positive/01-adult_age/rules.json +2 -0
  24. data/spec/test_suite/aliases_positive/02-address/aliases.json +2 -2
  25. data/spec/test_suite/aliases_positive/02-address/input.json +1 -1
  26. data/spec/test_suite/aliases_positive/02-address/output.json +1 -1
  27. data/spec/test_suite/negative/01-required/errors.json +4 -1
  28. data/spec/test_suite/negative/01-required/input.json +1 -0
  29. data/spec/test_suite/negative/01-required/rules.json +5 -1
  30. data/spec/test_suite/negative/03-one_of/errors.json +2 -0
  31. data/spec/test_suite/negative/03-one_of/input.json +2 -0
  32. data/spec/test_suite/negative/03-one_of/rules.json +9 -7
  33. data/spec/test_suite/negative/12-positive_decimal/errors.json +1 -0
  34. data/spec/test_suite/negative/12-positive_decimal/input.json +1 -0
  35. data/spec/test_suite/negative/12-positive_decimal/rules.json +1 -0
  36. data/spec/test_suite/negative/16-email/errors.json +22 -0
  37. data/spec/test_suite/negative/16-email/input.json +23 -0
  38. data/spec/test_suite/negative/16-email/rules.json +23 -0
  39. data/spec/test_suite/negative/17-equal_to_field/errors.json +4 -0
  40. data/spec/test_suite/negative/17-equal_to_field/input.json +5 -0
  41. data/spec/test_suite/negative/17-equal_to_field/rules.json +4 -0
  42. data/spec/test_suite/negative/18-nested_object/input.json +1 -1
  43. data/spec/test_suite/negative/21-list_of_different_objects/errors.json +6 -1
  44. data/spec/test_suite/negative/21-list_of_different_objects/input.json +23 -1
  45. data/spec/test_suite/negative/21-list_of_different_objects/rules.json +36 -1
  46. data/spec/test_suite/negative/23-url/errors.json +2 -0
  47. data/spec/test_suite/negative/23-url/input.json +2 -0
  48. data/spec/test_suite/negative/23-url/rules.json +2 -0
  49. data/spec/test_suite/negative/24-iso_date/errors.json +6 -0
  50. data/spec/test_suite/negative/24-iso_date/input.json +6 -0
  51. data/spec/test_suite/negative/24-iso_date/rules.json +6 -0
  52. data/spec/test_suite/negative/25-eq/rules.json +7 -7
  53. data/spec/test_suite/negative/28-variable_object/errors.json +9 -1
  54. data/spec/test_suite/negative/28-variable_object/input.json +9 -1
  55. data/spec/test_suite/negative/28-variable_object/rules.json +57 -1
  56. data/spec/test_suite/negative/29-or/errors.json +3 -0
  57. data/spec/test_suite/negative/29-or/input.json +1 -0
  58. data/spec/test_suite/negative/29-or/rules.json +3 -0
  59. data/spec/test_suite/positive/01-required/input.json +5 -1
  60. data/spec/test_suite/positive/01-required/output.json +4 -1
  61. data/spec/test_suite/positive/01-required/rules.json +4 -1
  62. data/spec/test_suite/positive/03-one_of/input.json +7 -3
  63. data/spec/test_suite/positive/03-one_of/output.json +7 -3
  64. data/spec/test_suite/positive/03-one_of/rules.json +8 -4
  65. data/spec/test_suite/positive/16-email/input.json +29 -0
  66. data/spec/test_suite/positive/16-email/output.json +28 -0
  67. data/spec/test_suite/positive/16-email/rules.json +28 -0
  68. data/spec/test_suite/positive/17-equal_to_field/input.json +3 -1
  69. data/spec/test_suite/positive/17-equal_to_field/output.json +2 -1
  70. data/spec/test_suite/positive/17-equal_to_field/rules.json +2 -1
  71. data/spec/test_suite/positive/18-nested_object/input.json +9 -1
  72. data/spec/test_suite/positive/18-nested_object/output.json +5 -0
  73. data/spec/test_suite/positive/18-nested_object/rules.json +10 -1
  74. data/spec/test_suite/positive/24-iso_date/input.json +2 -0
  75. data/spec/test_suite/positive/24-iso_date/output.json +2 -0
  76. data/spec/test_suite/positive/24-iso_date/rules.json +2 -0
  77. data/spec/test_suite/positive/25-eq/input.json +3 -3
  78. data/spec/test_suite/positive/25-eq/output.json +3 -3
  79. data/spec/test_suite/positive/25-eq/rules.json +4 -4
  80. data/spec/test_suite/positive/26-string/input.json +2 -2
  81. data/spec/test_suite/positive/26-string/output.json +2 -2
  82. data/spec/test_suite/positive/27-any_object/input.json +3 -2
  83. data/spec/test_suite/positive/27-any_object/output.json +3 -2
  84. data/spec/test_suite/positive/27-any_object/rules.json +2 -1
  85. data/spec/test_suite/positive/29-or/input.json +2 -0
  86. data/spec/test_suite/positive/29-or/output.json +2 -0
  87. data/spec/test_suite/positive/29-or/rules.json +3 -0
  88. data/spec/test_suite/positive/35-default/output.json +2 -1
  89. data/spec/test_suite/positive/35-default/rules.json +2 -1
  90. metadata +17 -14
  91. data/.travis.yml +0 -4
  92. data/Gemfile.lock +0 -45
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 43a065b27ff89d4be35e4ee14ced13cf49da7cba
4
- data.tar.gz: c92bd4fab2be4a2d5439446de2a1a17830414a10
2
+ SHA256:
3
+ metadata.gz: f6657ac44abc483ea73c0cdd20e26300ef34bdf9dde0e8e65ca6bd90428b2401
4
+ data.tar.gz: a0fb5076c7de93858bfcd224f25981d6e23d04b96f27c85df8b91b8c0e7f96b2
5
5
  SHA512:
6
- metadata.gz: be28f102a0c2e5f1448496904264d1837bc591c4070bf2cf505368397c3433debfa58c9c6ef904ce783193ad4ed333056a822c7e53d9933997cb6e2098234989
7
- data.tar.gz: 715d4b10d9282d7c80a78cc4eb5bcca8f9a43812258de02f3f7e427ee373f08cfc68c03fe7e823a122a0c50ba898d81614bfb8021c75d8cfbbd109c1c99862ff
6
+ metadata.gz: 6fea4e70535d7de501da828e551d4d247764cdaec995f4f7e23dfab4191ac026151eee4672ae570bab6a399278af4da8363f2099d5eede23cc4e40dfbd5c7cf1
7
+ data.tar.gz: 84b829178bc3f29fd8e7e2fd47e2ba85c173a34a33667d2782b86170ba6b128ed4ed377342752a0157dde3b925cc720e6a6bc51e2782bf47801fb3aac0d391df
@@ -0,0 +1,23 @@
1
+ name: test
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ jobs:
8
+ test:
9
+ # ubuntu-24.04, not ubuntu-latest: setup-ruby has no Ruby 2.7-3.1 builds for ubuntu-26.04
10
+ runs-on: ubuntu-24.04
11
+ # head is the next Ruby in development: its failures are early warnings, not a broken build
12
+ continue-on-error: ${{ matrix.ruby == 'head' }}
13
+ strategy:
14
+ fail-fast: false
15
+ matrix:
16
+ ruby: ['2.7', '3.0', '3.1', '3.2', '3.3', '3.4', '4.0', 'head']
17
+ steps:
18
+ - uses: actions/checkout@v7
19
+ - uses: ruby/setup-ruby@v1
20
+ with:
21
+ ruby-version: ${{ matrix.ruby }}
22
+ bundler-cache: true
23
+ - run: bundle exec rspec
data/.gitignore CHANGED
@@ -1,2 +1,3 @@
1
1
  .ruby-version
2
2
  *.gem
3
+ Gemfile.lock
data/Readme.md CHANGED
@@ -1,4 +1,4 @@
1
- [![Build Status](https://travis-ci.org/fuksito/ruby-livr.svg?branch=master)](https://travis-ci.org/fuksito/ruby-livr)
1
+ [![test](https://github.com/fuksito/ruby-livr/actions/workflows/test.yml/badge.svg)](https://github.com/fuksito/ruby-livr/actions/workflows/test.yml)
2
2
 
3
3
  # LIVR Validator
4
4
  LIVR.Validator - Lightweight validator supporting Language Independent Validation Rules Specification (LIVR)
@@ -41,7 +41,7 @@ validator = LIVR::Validator.new({
41
41
 
42
42
  Feel free to register your own rules:
43
43
 
44
- You can use aliases(prefferable, syntax covered by the specification) for a lot of cases:
44
+ You can use aliases (prefferable, syntax covered by the specification) for a lot of cases:
45
45
 
46
46
  ```ruby
47
47
  validator = LIVR::Validator.new({
@@ -6,20 +6,26 @@ module LIVR
6
6
  raise "Alias rules required" unless _alias["rules"].present?
7
7
 
8
8
  @alias = _alias
9
- @validator = Validator.new(value: Array.wrap(@alias["rules"]))
10
9
  end
11
10
 
11
+ # A rule for every use: each one gets the rules of the validator that uses it
12
12
  def new(*args)
13
- self
13
+ dup
14
+ end
15
+
16
+ def rule_builders=(rule_builders)
17
+ @validator = Validator.new(value: Array.wrap(@alias["rules"])).register_rules(rule_builders).prepare
14
18
  end
15
19
 
16
20
  def call(value, user_data, field_results)
17
- result = @validator.validate({ value: value })
21
+ self.rule_builders = {} unless @validator # built outside of a validator: default rules only
22
+ # A missing value stays missing, unless the rules change it
23
+ result = @validator.validate(value.nil? ? {} : { value: value })
18
24
  if result
19
- field_results << result[:value]
25
+ field_results << result[:value] if result.has_key?(:value)
20
26
  return
21
27
  else
22
- return @alias.fetch("error", @validator.get_errors)
28
+ return @alias["error"] || @validator.get_errors[:value]
23
29
  end
24
30
  end
25
31
  end
data/lib/livr/rule.rb CHANGED
@@ -17,5 +17,18 @@ module LIVR
17
17
  def is_no_value(value)
18
18
  value == nil || value == ""
19
19
  end
20
+
21
+ # Base 10: Integer() alone reads "010" as an octal and "0x10" as a hexadecimal number.
22
+ # No underscores: Integer() and Float() read "1_000" as 1000, like a Ruby literal.
23
+ def to_integer(value)
24
+ return if value.to_s.include?("_")
25
+ Integer(value.to_s, 10) rescue nil
26
+ end
27
+
28
+ # Integers stay integers: "10" is 10, "1.5" is 1.5
29
+ def to_number(value)
30
+ return if value.to_s.include?("_")
31
+ to_integer(value) || (Float(value.to_s) rescue nil)
32
+ end
20
33
  end
21
34
  end
@@ -4,8 +4,7 @@ module LIVR
4
4
 
5
5
  class Required < Rule
6
6
  def call(value, user_data, field_results)
7
- return if value.in?([[], {}])
8
- "REQUIRED" if value.blank?
7
+ "REQUIRED" if is_no_value(value)
9
8
  end
10
9
  end
11
10
 
@@ -4,10 +4,15 @@ module LIVR
4
4
 
5
5
  class NestedObject < Rule
6
6
  def initialize(livr)
7
- @validator = Validator.new(livr).prepare
7
+ @livr = livr
8
+ end
9
+
10
+ def rule_builders=(rule_builders)
11
+ @validator = Validator.new(@livr).register_rules(rule_builders).prepare
8
12
  end
9
13
 
10
14
  def call(nested_object, user_data, field_results)
15
+ self.rule_builders = {} unless @validator # built outside of a validator: default rules only
11
16
  return if is_no_value(nested_object)
12
17
  return 'FORMAT_ERROR' unless nested_object.is_a?(Hash)
13
18
 
@@ -25,13 +30,18 @@ module LIVR
25
30
  class VariableObject < Rule
26
31
  def initialize(selector_field, livrs)
27
32
  @selector_field = selector_field
33
+ @livrs = livrs
34
+ end
35
+
36
+ def rule_builders=(rule_builders)
28
37
  @validators = {}
29
- livrs.each do |selector_value, livr|
30
- @validators[selector_value] = Validator.new(livr).prepare
38
+ @livrs.each do |selector_value, livr|
39
+ @validators[selector_value] = Validator.new(livr).register_rules(rule_builders).prepare
31
40
  end
32
41
  end
33
42
 
34
43
  def call(object, user_data, field_results)
44
+ self.rule_builders = {} unless @validators # built outside of a validator: default rules only
35
45
  return if is_no_value(object)
36
46
  if ( !object.is_a?(Hash) || !object[@selector_field] || !@validators[ object[@selector_field] ] )
37
47
  return 'FORMAT_ERROR'
@@ -51,11 +61,15 @@ module LIVR
51
61
 
52
62
  class ListOf < Rule
53
63
  def initialize(*rules)
54
- livr = { field: rules.flatten }
55
- @validator = Validator.new(livr).prepare
64
+ @livr = { field: rules.flatten }
65
+ end
66
+
67
+ def rule_builders=(rule_builders)
68
+ @validator = Validator.new(@livr).register_rules(rule_builders).prepare
56
69
  end
57
70
 
58
71
  def call(values, user_data, field_results)
72
+ self.rule_builders = {} unless @validator # built outside of a validator: default rules only
59
73
  return if is_no_value(values)
60
74
  return 'FORMAT_ERROR' unless values.is_a?(Array)
61
75
 
@@ -87,10 +101,15 @@ module LIVR
87
101
 
88
102
  class ListOfObjects < Rule
89
103
  def initialize(livr)
90
- @validator = Validator.new(livr).prepare
104
+ @livr = livr
105
+ end
106
+
107
+ def rule_builders=(rule_builders)
108
+ @validator = Validator.new(@livr).register_rules(rule_builders).prepare
91
109
  end
92
110
 
93
111
  def call(objects, user_data, field_results)
112
+ self.rule_builders = {} unless @validator # built outside of a validator: default rules only
94
113
  return if is_no_value(objects)
95
114
  return 'FORMAT_ERROR' unless objects.is_a?(Array)
96
115
 
@@ -124,13 +143,18 @@ module LIVR
124
143
  class ListOfDifferentObjects < Rule
125
144
  def initialize(selector_field, livrs)
126
145
  @selector_field = selector_field
146
+ @livrs = livrs
147
+ end
148
+
149
+ def rule_builders=(rule_builders)
127
150
  @validators = {}
128
- livrs.each do |selector_value, livr|
129
- @validators[selector_value] = Validator.new(livr).prepare
151
+ @livrs.each do |selector_value, livr|
152
+ @validators[selector_value] = Validator.new(livr).register_rules(rule_builders).prepare
130
153
  end
131
154
  end
132
155
 
133
156
  def call(objects, user_data, field_results)
157
+ self.rule_builders = {} unless @validators # built outside of a validator: default rules only
134
158
  return if is_no_value(objects)
135
159
  return 'FORMAT_ERROR' unless objects.is_a?(Array)
136
160
 
@@ -140,6 +164,7 @@ module LIVR
140
164
 
141
165
  objects.each do |object|
142
166
  if !object.is_a?(Hash) || !object[@selector_field] || !@validators[ object[@selector_field] ]
167
+ has_errors = true
143
168
  errors << 'FORMAT_ERROR'
144
169
  next
145
170
  end
@@ -170,20 +195,26 @@ module LIVR
170
195
  class Or < Rule
171
196
  def initialize(*rule_sets)
172
197
  @rule_sets = rule_sets
198
+ end
199
+
200
+ def rule_builders=(rule_builders)
173
201
  @validators = @rule_sets.map do |rules|
174
202
  livr = { field: rules }
175
- Validator.new(livr).prepare
203
+ Validator.new(livr).register_rules(rule_builders).prepare
176
204
  end
177
205
  end
178
206
 
179
207
  def call(value, user_data, field_results)
180
- return if is_no_value(value)
208
+ self.rule_builders = {} unless @validators # built outside of a validator: default rules only
209
+ # Empty and missing values go to the rule sets too ("required" can be one of them).
210
+ # A missing value stays missing, unless a rule set changes it.
211
+ input = value.nil? ? {} : { field: value }
181
212
 
182
213
  last_error = nil
183
214
  @validators.each do |validator|
184
- result = validator.validate(field: value)
215
+ result = validator.validate(input)
185
216
  if result
186
- field_results << result[:field]
217
+ field_results << result[:field] if result.has_key?(:field)
187
218
  return nil
188
219
  else
189
220
  last_error = validator.get_errors[:field]
@@ -16,24 +16,24 @@ module LIVR
16
16
 
17
17
  class Trim < Rule
18
18
  def call(value, user_data, field_results)
19
- return if is_no_value(value) || value.is_a?(Hash)
20
- field_results << value.to_s.mb_chars.strip
19
+ return if is_no_value(value) || value.is_a?(Hash) || value.is_a?(Array)
20
+ field_results << value.to_s.strip
21
21
  return
22
22
  end
23
23
  end
24
24
 
25
25
  class ToLc < Rule
26
26
  def call(value, user_data, field_results)
27
- return if is_no_value(value) || value.is_a?(Hash)
28
- field_results << value.to_s.mb_chars.downcase
27
+ return if is_no_value(value) || value.is_a?(Hash) || value.is_a?(Array)
28
+ field_results << value.to_s.downcase
29
29
  return
30
30
  end
31
31
  end
32
32
 
33
33
  class ToUc < Rule
34
34
  def call(value, user_data, field_results)
35
- return if is_no_value(value) || value.is_a?(Hash)
36
- field_results << value.to_s.mb_chars.upcase
35
+ return if is_no_value(value) || value.is_a?(Hash) || value.is_a?(Array)
36
+ field_results << value.to_s.upcase
37
37
  return
38
38
  end
39
39
  end
@@ -45,8 +45,8 @@ module LIVR
45
45
  end
46
46
 
47
47
  def call(value, user_data, field_results)
48
- return if is_no_value(value) || value.is_a?(Hash)
49
- field_results << value.to_s.mb_chars.gsub(@re, '')
48
+ return if is_no_value(value) || value.is_a?(Hash) || value.is_a?(Array)
49
+ field_results << value.to_s.gsub(@re, '')
50
50
  return
51
51
  end
52
52
  end
@@ -58,8 +58,8 @@ module LIVR
58
58
  end
59
59
 
60
60
  def call(value, user_data, field_results)
61
- return if is_no_value(value) || value.is_a?(Hash)
62
- field_results << value.to_s.mb_chars.gsub(@re, '')
61
+ return if is_no_value(value) || value.is_a?(Hash) || value.is_a?(Array)
62
+ field_results << value.to_s.gsub(@re, '')
63
63
  return
64
64
  end
65
65
  end
@@ -7,7 +7,7 @@ module LIVR
7
7
  return if is_no_value(value)
8
8
  return 'FORMAT_ERROR' if !is_primitive(value)
9
9
 
10
- value_int = Integer(value.to_s) rescue nil
10
+ value_int = to_integer(value)
11
11
  if value_int.nil?
12
12
  return "NOT_INTEGER"
13
13
  end
@@ -23,7 +23,7 @@ module LIVR
23
23
  return if is_no_value(value)
24
24
  return 'FORMAT_ERROR' if !is_primitive(value)
25
25
 
26
- value_int = Integer(value.to_s) rescue nil
26
+ value_int = to_integer(value)
27
27
 
28
28
  return "NOT_POSITIVE_INTEGER" if value_int.nil?
29
29
  return "NOT_POSITIVE_INTEGER" unless value_int.positive?
@@ -70,7 +70,7 @@ module LIVR
70
70
  return if is_no_value(value)
71
71
  return 'FORMAT_ERROR' if !is_primitive(value)
72
72
 
73
- value = Float(value.to_s) rescue nil
73
+ value = to_number(value)
74
74
  if value.nil?
75
75
  return "NOT_NUMBER"
76
76
  end
@@ -93,7 +93,7 @@ module LIVR
93
93
  return if is_no_value(value)
94
94
  return 'FORMAT_ERROR' if !is_primitive(value)
95
95
 
96
- value = Float(value.to_s) rescue nil
96
+ value = to_number(value)
97
97
  if value.nil?
98
98
  return "NOT_NUMBER"
99
99
  end
@@ -117,7 +117,7 @@ module LIVR
117
117
  return if is_no_value(value)
118
118
  return 'FORMAT_ERROR' if !is_primitive(value)
119
119
 
120
- value = Float(value.to_s) rescue nil
120
+ value = to_number(value)
121
121
  if value.nil?
122
122
  return "NOT_NUMBER"
123
123
  end
@@ -3,15 +3,15 @@ module LIVR
3
3
  module Special
4
4
 
5
5
  class Email < Rule
6
+ # The same expression as in the reference implementation: one "@" only and no "_" in the domain.
7
+ # [:space:] is Unicode-aware, like \s in JavaScript. \A and \z, not ^ and $: they match at line breaks.
8
+ EMAIL_RE = /\A(?:[^<>()\[\]\\.,;:[:space:]@"]+(?:\.[^<>()\[\]\\.,;:[:space:]@"]+)*|"[^@\n\r\u2028\u2029]+")@(?:\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\]|(?:[a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,})\z/
9
+
6
10
  def call(value, user_data, field_results)
7
11
  return if is_no_value(value)
8
12
  return 'FORMAT_ERROR' if !is_primitive(value)
9
13
 
10
- value = value.to_s
11
- email_re = /^([\w\-_+]+(?:\.[\w\-_+]+)*)@((?:[\w\-]+\.)*\w[\w\-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$/i
12
- return "WRONG_EMAIL" unless email_re.match(value)
13
- return "WRONG_EMAIL" if /\@.*\@/.match(value)
14
- return "WRONG_EMAIL" if /\@.*_/.match(value)
14
+ return "WRONG_EMAIL" unless EMAIL_RE.match(value.to_s)
15
15
  return
16
16
  end
17
17
  end
@@ -25,12 +25,15 @@ module LIVR
25
25
  return if is_no_value(value)
26
26
  return 'FORMAT_ERROR' if !is_primitive(value)
27
27
 
28
- return "FIELDS_NOT_EQUAL" if value != user_data[@field]
28
+ # Compared as strings: "10" and 10 are equal. A missing field is not equal to any value.
29
+ other_value = user_data[@field]
30
+ return "FIELDS_NOT_EQUAL" unless is_primitive(other_value) && value.to_s == other_value.to_s
29
31
  end
30
32
  end
31
33
 
32
34
  class Url < Rule
33
- URL_RE_STR = '^(?:(?:http|https)://)(?:\\S+(?::\\S*)?@)?(?:(?:(?:[1-9]\\d?|1\\d\\d|2[0-1]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[0-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))\\.?|localhost)(?::\\d{2,5})?(?:[/?#]\\S*)?$'
35
+ # \A and \z, not ^ and $: they match at line breaks in Ruby.
36
+ URL_RE_STR = '\\A(?:(?:http|https)://)(?:\\S+(?::\\S*)?@)?(?:(?:(?:[1-9]\\d?|1\\d\\d|2[0-1]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[0-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))\\.?|localhost)(?::\\d{2,5})?(?:[/?#]\\S*)?\\z'
34
37
  URL_RE = Regexp.compile(URL_RE_STR, "i")
35
38
  MAX_URL_LENGTH = 2083
36
39
 
@@ -38,7 +41,7 @@ module LIVR
38
41
  return if is_no_value(value)
39
42
  return 'FORMAT_ERROR' if !is_primitive(value)
40
43
 
41
- if value.length < MAX_URL_LENGTH && URL_RE.match(value)
44
+ if value.to_s.length < MAX_URL_LENGTH && URL_RE.match(value.to_s)
42
45
  return
43
46
  else
44
47
  'WRONG_URL'
@@ -51,7 +54,9 @@ module LIVR
51
54
  return if is_no_value(value)
52
55
  return 'FORMAT_ERROR' if !is_primitive(value)
53
56
 
54
- return "WRONG_DATE" unless value.length == "YYYY-MM-DD".length
57
+ value = value.to_s
58
+ # Date.strptime alone accepts "2020-01-1x" and "+020-01-01"
59
+ return "WRONG_DATE" unless value =~ /\A\d{4}-\d{2}-\d{2}\z/
55
60
 
56
61
  date = Date.strptime(value, '%Y-%m-%d') rescue nil
57
62
  unless date
@@ -3,7 +3,7 @@ module LIVR
3
3
  def initialize(rules, is_auto_trim=false)
4
4
  @rules = rules
5
5
  @is_prepared = false
6
- @validator_builders = {}
6
+ @validator_builders = {}.with_indifferent_access
7
7
  @validators = {}
8
8
  @is_auto_trim = is_auto_trim
9
9
 
@@ -110,7 +110,12 @@ module LIVR
110
110
  if @validator_builders[name].blank?
111
111
  raise "Rule ['#{name}'] not registered"
112
112
  end
113
- @validator_builders[name].new(*args)
113
+ validator = @validator_builders[name].new(*args)
114
+
115
+ # Meta rules and aliases validate with validators of their own. They get the rules
116
+ # of this validator, so that aliases and custom rules can be used inside them.
117
+ validator.rule_builders = @validator_builders if validator.respond_to?(:rule_builders=)
118
+ validator
114
119
  end
115
120
 
116
121
  private
data/lib/livr.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  require 'json'
2
2
  require 'date'
3
+ # activesupport 6.x uses Logger without loading it, which fails with concurrent-ruby 1.3.5 and later
4
+ require 'logger'
3
5
  require 'active_support/all'
4
6
 
5
7
  require_relative "livr/validator.rb"
data/ruby-livr.gemspec CHANGED
@@ -1,7 +1,6 @@
1
1
  Gem::Specification.new do |gem|
2
2
  gem.name = "ruby-livr"
3
- gem.version = "2.0.0"
4
- gem.date = "2017-12-22"
3
+ gem.version = "2.1.0"
5
4
  gem.summary = "Implementation of LIVR v2.0"
6
5
  gem.description = "Language Independent Validation Rules. Implements 2.0 specification"
7
6
  gem.authors = ["Vitaliy Yanchuk"]
@@ -14,6 +13,8 @@ Gem::Specification.new do |gem|
14
13
  gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
15
14
  gem.require_paths = ["lib"]
16
15
 
17
- gem.add_dependency "activesupport", '~> 5'
18
- gem.add_development_dependency "rspec", "3.7"
16
+ gem.required_ruby_version = ">= 2.7"
17
+
18
+ gem.add_dependency "activesupport", '>= 5', '< 9'
19
+ gem.add_development_dependency "rspec", "~> 3.7"
19
20
  end
@@ -5,9 +5,13 @@ def read_file(dir, filename)
5
5
  JSON.parse(data)
6
6
  end
7
7
 
8
+ def read_aliases(dir)
9
+ dir.join("aliases.json").exist? ? read_file(dir, "aliases") : []
10
+ end
11
+
8
12
  RSpec.describe "test_suite " do
9
13
 
10
- %w[ positive negative ].each do |spec_dir|
14
+ %w[ positive negative aliases_positive aliases_negative ].each do |spec_dir|
11
15
  context spec_dir do
12
16
  dirs = Pathname.new("#{SPEC_ROOT}/test_suite/#{spec_dir}").children.select(&:directory?)
13
17
  dirs.each do |dir|
@@ -16,10 +20,11 @@ RSpec.describe "test_suite " do
16
20
  input = read_file(dir, "input")
17
21
 
18
22
  validator = LIVR::Validator.new(rules)
23
+ read_aliases(dir).each { |_alias| validator.register_aliased_rule(_alias) }
19
24
  result = validator.validate(input)
20
25
  errors = validator.get_errors
21
26
 
22
- if spec_dir == 'positive'
27
+ if spec_dir.end_with?('positive')
23
28
  expected_result = read_file(dir, "output")
24
29
  expect(result).to eq(expected_result)
25
30
  else
@@ -18,4 +18,33 @@ RSpec.describe "aliases" do
18
18
  expect(validator.get_errors).to eq({"password" => "WEAK_PASSWORD"})
19
19
  end
20
20
 
21
- end
21
+ it "Validates with aliases and custom rules inside meta rules" do
22
+ weak_password = Class.new(LIVR::Rule) do
23
+ def call(value, user_data, field_results)
24
+ "WEAK_PASSWORD" if value.to_s.length < 6
25
+ end
26
+ end
27
+
28
+ validator = LIVR::Validator.new({
29
+ "user" => "user",
30
+ "passwords" => { "list_of" => "weak_password" }
31
+ })
32
+ validator.register_aliased_rule({
33
+ "name" => "user",
34
+ "rules" => { "nested_object" => { "age" => "adult_age" } }
35
+ })
36
+ validator.register_aliased_rule({
37
+ "name" => "adult_age",
38
+ "rules" => [ "positive_integer", { "min_number" => 18 } ]
39
+ })
40
+ validator.register_rules("weak_password" => weak_password)
41
+
42
+ output = validator.validate({ "user" => { "age" => 10 }, "passwords" => [ "abc", "abcdefg" ] })
43
+ expect(output).to eq(false)
44
+ expect(validator.get_errors).to eq({
45
+ "user" => { "age" => "TOO_LOW" },
46
+ "passwords" => [ "WEAK_PASSWORD", nil ]
47
+ })
48
+ end
49
+
50
+ end
@@ -50,5 +50,16 @@ RSpec.describe "custom_filters" do
50
50
  )
51
51
  end
52
52
 
53
- end
53
+ it 'Validate data with rules registered with symbol names' do
54
+ validator = LIVR::Validator.new(
55
+ "word1" => ['my_trim2'],
56
+ "word2" => [{ my_trim2: [] }]
57
+ )
58
+ validator.register_rules(my_trim2: MyTrim)
59
+
60
+ output = validator.validate("word1" => ' wordOne ', "word2" => ' wordTwo ')
54
61
 
62
+ expect(output).to eq("word1" => 'wordOne', "word2" => 'wordTwo')
63
+ end
64
+
65
+ end