nice_hash 1.19.0 → 1.21.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.
- checksums.yaml +4 -4
- data/README.md +34 -6
- data/lib/nice/hash/add_to_ruby.rb +4 -4
- data/lib/nice/hash/generate.rb +116 -27
- data/lib/nice/hash/validate.rb +94 -10
- metadata +5 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ab97a22d06c02e9a17c33221c77ff01c841405e0b3a15711ecb465bbf91eb23c
|
|
4
|
+
data.tar.gz: c358cab3956cd0f4dd90020ad68da0039f42786892d71f3803a2dcbc8be87f6b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a4c876a1bd17ee4bf3a845dedb57aee727e73e9173a1145a103ce2da75daeec422c768a8bc3ba82c051734221079e184925c1b432f8d6be4bc240b4e43d2854d
|
|
7
|
+
data.tar.gz: 074570bccde6d2a926286817b13f935be05e34fbc57835a761bfe745c7474be1bd5585a5fef812cfb36d03d64c03e315d08fc6dd4ca2ad65d1afb84b14c80fe3
|
data/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# NiceHash
|
|
2
2
|
|
|
3
3
|
[](https://rubygems.org/gems/nice_hash)
|
|
4
|
-
[](https://github.com/MarioRuiz/nice_hash/actions/workflows/ci.yml)
|
|
5
5
|
[](https://coveralls.io/github/MarioRuiz/nice_hash?branch=master)
|
|
6
6
|

|
|
7
7
|

|
|
@@ -79,6 +79,12 @@ Set these constants **before** `require 'nice_hash'` if you need to change defau
|
|
|
79
79
|
|
|
80
80
|
When using **string_pattern** 2.4+, you can also set **`StringPattern.logger`** (e.g. `Logger.new($stderr)`) to redirect generation/validation messages, and **`StringPattern.raise_on_error = true`** to raise on invalid patterns or impossible generation instead of returning empty strings.
|
|
81
81
|
|
|
82
|
+
**Upgrading to 1.20** (requires string_pattern 2.5):
|
|
83
|
+
|
|
84
|
+
- Update assertions that pin a value produced with `seed:` in 1.19. The same seed now walks one sequence for patterns, ranges, booleans and selectors, so those strings change. Do not expect `:uuid`, `:iban` or `:luhn` to repeat from `seed:`.
|
|
85
|
+
- If `StringPattern.block_list` contains regular expressions, set `StringPattern.block_list_regexp = true`. Otherwise each entry is a literal.
|
|
86
|
+
- A `Regexp` pattern must match the whole value. Update fixtures that used to pass because `$` matched before a newline.
|
|
87
|
+
|
|
82
88
|
Example:
|
|
83
89
|
|
|
84
90
|
```ruby
|
|
@@ -90,13 +96,25 @@ require 'nice_hash'
|
|
|
90
96
|
|
|
91
97
|
Remember!! To generate the strings following a pattern take a look at the documentation for string_pattern gem: https://github.com/MarioRuiz/string_pattern. You can also generate Spanish or English words. We added support for generating strings from regular expressions but it is only working for the ´generate´ method, use it with caution since it is still on an early stage of development.
|
|
92
98
|
|
|
93
|
-
For **UUID v4** you can use
|
|
99
|
+
For **UUID v4**, **email**, **IBAN** and a **Luhn** number you can use these shorthands (string_pattern 2.4+ for `:uuid`, 2.5+ for the others):
|
|
94
100
|
|
|
95
101
|
```ruby
|
|
96
|
-
my_hash = { id: :uuid,
|
|
97
|
-
# id
|
|
102
|
+
my_hash = { id: :uuid, email: :email, iban: :iban, card: :luhn, doomId: :"10:N" }
|
|
103
|
+
# id: "550e8400-e29b-41d4-a716-446655440000"
|
|
104
|
+
# email: a generated address
|
|
105
|
+
# iban: a Spanish IBAN
|
|
106
|
+
# card: 16 digits with a valid Luhn checksum
|
|
107
|
+
|
|
108
|
+
my_hash = { iban: [:iban, "DE"], card: [:luhn, 15], tags: [:"3:L"] }
|
|
109
|
+
# iban: a German IBAN. Countries: ES, DE, FR, GB, PT, IT, NL, BE
|
|
110
|
+
# card: 15 digits with a valid Luhn checksum
|
|
111
|
+
# tags: 1 to 5 different strings from that pattern
|
|
98
112
|
```
|
|
99
113
|
|
|
114
|
+
`:iban` still generates a Spanish IBAN, and validating `:iban` accepts any well-formed IBAN. `[:iban, "DE"]` generates and requires that country. `:luhn` is 16 digits; `[:luhn, 15]` sets the length.
|
|
115
|
+
|
|
116
|
+
`:uuid`, `:iban` and `:luhn` use their own random generator. `seed:` does not make those three fields repeat. A one-element pattern array does follow `seed:`.
|
|
117
|
+
|
|
100
118
|
Or use a Regular expression for custom formats:
|
|
101
119
|
|
|
102
120
|
```ruby
|
|
@@ -403,7 +421,7 @@ new_hash = my_hash.select_key(:correct).generate
|
|
|
403
421
|
```
|
|
404
422
|
|
|
405
423
|
**Reproducible generation (seed)**
|
|
406
|
-
|
|
424
|
+
Pass `seed:` to get the same hash every time, including string patterns, ranges, booleans and selectors. One seed advances through the whole hash, so two fields with the same pattern get different values. `:uuid`, `:iban` and `:luhn` stay outside that sequence.
|
|
407
425
|
|
|
408
426
|
```ruby
|
|
409
427
|
new_hash = my_hash.generate(:correct, seed: 42)
|
|
@@ -417,7 +435,7 @@ Use `generate_n` (alias `gen_n`) to build several different hashes in one call (
|
|
|
417
435
|
hashes = NiceHash.generate_n(my_hash, 5, :correct)
|
|
418
436
|
hashes = my_hash.generate_n(5, :correct) # or my_hash.gen_n(5, :correct)
|
|
419
437
|
# With seed: hashes = my_hash.generate_n(5, :correct, seed: 123)
|
|
420
|
-
#
|
|
438
|
+
# the same seed yields the same 5 hashes, and they differ from each other
|
|
421
439
|
```
|
|
422
440
|
|
|
423
441
|
In case of filtering by :correct new_hash would have a value like this for example:
|
|
@@ -731,6 +749,13 @@ The possible validation values returned:
|
|
|
731
749
|
:excluded_data: the resultant string will include one or more characters that should be excluded. It works only if excluded data supplied on the pattern.
|
|
732
750
|
:string_set_not_allowed: it will include one or more characters that are not supposed to be on the string.
|
|
733
751
|
|
|
752
|
+
Pass `explain: true` to get a short description instead of `false` or the error list:
|
|
753
|
+
|
|
754
|
+
```ruby
|
|
755
|
+
{ name: :"6:N" }.validate({ name: "ab" }, explain: true)
|
|
756
|
+
#=> { name: { valid: false, errors: [:min_length, :length, :value, :string_set_not_allowed], message: "invalid: min_length, length, value, string_set_not_allowed", index: nil } }
|
|
757
|
+
```
|
|
758
|
+
|
|
734
759
|
You can also supply nested pattern keys for the patterns to be more specific: `{'draws.drawId': :"4:N"}`
|
|
735
760
|
|
|
736
761
|
### Change only one value at a time and return an Array of Hashes
|
|
@@ -887,6 +912,9 @@ Valid patterns:
|
|
|
887
912
|
- DateTime: it will verify if the value is following Time stamp string '2019-06-20T12:01:09.971Z' or if the object is a Time, Date or DateTime class
|
|
888
913
|
- selectors, one of the values. Example: "uno|dos|tres"
|
|
889
914
|
- **:uuid** (string_pattern 2.4+): UUID v4 format
|
|
915
|
+
- **:email** (string_pattern 2.5+): email address
|
|
916
|
+
- **:iban** (string_pattern 2.5+): any IBAN. `[:iban, "DE"]` requires that country
|
|
917
|
+
- **:luhn** (string_pattern 2.5+): Luhn checksum. `:luhn` is 16 digits; `[:luhn, 15]` sets the length
|
|
890
918
|
|
|
891
919
|
### Other useful methods
|
|
892
920
|
|
|
@@ -253,16 +253,16 @@ class Hash
|
|
|
253
253
|
# More info: NiceHash.validate
|
|
254
254
|
# alias: val
|
|
255
255
|
###########################################################################
|
|
256
|
-
def validate(select_hash_key = nil, values_hash_to_validate)
|
|
257
|
-
NiceHash.validate([self, select_hash_key], values_hash_to_validate, only_patterns: false)
|
|
256
|
+
def validate(select_hash_key = nil, values_hash_to_validate, explain: false)
|
|
257
|
+
NiceHash.validate([self, select_hash_key], values_hash_to_validate, only_patterns: false, explain: explain)
|
|
258
258
|
end
|
|
259
259
|
|
|
260
260
|
###########################################################################
|
|
261
261
|
# Validates a given values_hash_to_validate with string patterns
|
|
262
262
|
# More info: NiceHash.validate
|
|
263
263
|
###########################################################################
|
|
264
|
-
def validate_patterns(select_hash_key = nil, values_hash_to_validate)
|
|
265
|
-
NiceHash.validate([self, select_hash_key], values_hash_to_validate, only_patterns: true)
|
|
264
|
+
def validate_patterns(select_hash_key = nil, values_hash_to_validate, explain: false)
|
|
265
|
+
NiceHash.validate([self, select_hash_key], values_hash_to_validate, only_patterns: true, explain: explain)
|
|
266
266
|
end
|
|
267
267
|
|
|
268
268
|
###########################################################################
|
data/lib/nice/hash/generate.rb
CHANGED
|
@@ -51,14 +51,26 @@ class NiceHash
|
|
|
51
51
|
# new_hash = my_hash.gen(:correct)
|
|
52
52
|
# new_hash = my_hash.generate(:correct, errors: [:min_length])
|
|
53
53
|
###########################################################################
|
|
54
|
+
# One RNG for the whole hash. string_pattern 2.5 no longer seeds Kernel#rand,
|
|
55
|
+
# so passing seed: on every StringPattern.generate call would restart the sequence.
|
|
56
|
+
def self.generation_rng(synonyms)
|
|
57
|
+
rng = synonyms.delete(:_rng)
|
|
58
|
+
return rng unless rng.nil?
|
|
59
|
+
|
|
60
|
+
seed = synonyms.delete(:seed)
|
|
61
|
+
base = seed.nil? ? Random.new : Random.new(seed)
|
|
62
|
+
StringPattern::RandomSource.new(base)
|
|
63
|
+
end
|
|
64
|
+
|
|
54
65
|
def NiceHash.generate(pattern_hash, select_hash_key = nil, expected_errors: [], **synonyms)
|
|
55
66
|
hashv = Hash.new()
|
|
56
67
|
same_values = Hash.new()
|
|
57
|
-
expected_errors = synonyms
|
|
68
|
+
expected_errors = synonyms.delete(:errors) if synonyms.key?(:errors)
|
|
58
69
|
if expected_errors.kind_of?(Symbol)
|
|
59
70
|
expected_errors = [expected_errors]
|
|
60
71
|
end
|
|
61
|
-
|
|
72
|
+
rng = generation_rng(synonyms)
|
|
73
|
+
sp_opts = { expected_errors: expected_errors, _rng: rng }.merge(synonyms)
|
|
62
74
|
|
|
63
75
|
if pattern_hash.kind_of?(Hash) and pattern_hash.size > 0
|
|
64
76
|
pattern_hash.each { |key, value|
|
|
@@ -71,16 +83,22 @@ class NiceHash
|
|
|
71
83
|
if value.keys.include?(select_hash_key)
|
|
72
84
|
value = value[select_hash_key]
|
|
73
85
|
else
|
|
74
|
-
value = NiceHash.generate(value, select_hash_key, expected_errors: expected_errors, **synonyms)
|
|
86
|
+
value = NiceHash.generate(value, select_hash_key, expected_errors: expected_errors, _rng: rng, **synonyms)
|
|
75
87
|
end
|
|
76
88
|
end
|
|
77
89
|
if value == :uuid
|
|
90
|
+
# SecureRandom is not seeded. UUID values are not reproducible via seed:.
|
|
78
91
|
hashv[key] = expected_errors.empty? ? StringPattern.uuid : (StringPattern.uuid[0..-2] + "X")
|
|
92
|
+
elsif value == :email
|
|
93
|
+
hashv[key] = StringPattern.generate("20-40:@", **sp_opts)
|
|
94
|
+
elsif (spec = checksum_spec(value))
|
|
95
|
+
# StringPattern.iban and StringPattern.luhn use their own Random, so they are not reproducible via seed:.
|
|
96
|
+
hashv[key] = generated_checksum(spec, expected_errors)
|
|
79
97
|
elsif value.kind_of?(String) or value.kind_of?(Symbol)
|
|
80
98
|
if ((StringPattern.optimistic and value.kind_of?(String)) or value.kind_of?(Symbol)) and value.to_s.scan(/^!?\d+-?\d*:.+/).size > 0
|
|
81
99
|
hashv[key] = StringPattern.generate(value, **sp_opts)
|
|
82
100
|
elsif ((StringPattern.optimistic and value.kind_of?(String)) or value.kind_of?(Symbol)) and value.to_s.scan(/^([\w\s\-]+\|)+[\w\s\-]+$/).size > 0
|
|
83
|
-
if expected_errors.include?(:min_length) or (expected_errors.include?(:length) and rand.round == 0)
|
|
101
|
+
if expected_errors.include?(:min_length) or (expected_errors.include?(:length) and rng.rand.round == 0)
|
|
84
102
|
min = value.to_s.split("|").min { |a, b| a.size <=> b.size }
|
|
85
103
|
hashv[key] = min[0..-2] unless min == ""
|
|
86
104
|
end
|
|
@@ -94,7 +112,7 @@ class NiceHash
|
|
|
94
112
|
if hashv.keys.include?(key)
|
|
95
113
|
v = hashv[key]
|
|
96
114
|
else
|
|
97
|
-
v = value.to_s.split("|").sample
|
|
115
|
+
v = value.to_s.split("|").sample(random: rng)
|
|
98
116
|
end
|
|
99
117
|
unless expected_errors.include?(:string_set_not_allowed)
|
|
100
118
|
v = StringPattern.generate(:"#{v.size}:[#{value.to_s.split("|").join.split(//).uniq.join}]", **sp_opts)
|
|
@@ -102,13 +120,13 @@ class NiceHash
|
|
|
102
120
|
end
|
|
103
121
|
unless hashv.keys.include?(key)
|
|
104
122
|
# Need a valid 1-char not in the allowed set (do not pass expected_errors)
|
|
105
|
-
one_wrong_letter = StringPattern.generate(:"1:LN$[%#{value.to_s.split("|").join.split(//).uniq.join}%]")
|
|
106
|
-
v[rand(v.size)] = one_wrong_letter
|
|
123
|
+
one_wrong_letter = StringPattern.generate(:"1:LN$[%#{value.to_s.split("|").join.split(//).uniq.join}%]", _rng: rng)
|
|
124
|
+
v[rng.rand(v.size)] = one_wrong_letter
|
|
107
125
|
hashv[key] = v unless value.to_s.split("|").include?(v)
|
|
108
126
|
end
|
|
109
127
|
end
|
|
110
128
|
unless hashv.keys.include?(key)
|
|
111
|
-
hashv[key] = value.to_s.split("|").sample
|
|
129
|
+
hashv[key] = value.to_s.split("|").sample(random: rng)
|
|
112
130
|
end
|
|
113
131
|
else
|
|
114
132
|
hashv[key] = value
|
|
@@ -118,9 +136,11 @@ class NiceHash
|
|
|
118
136
|
if value.size == 1
|
|
119
137
|
v = value[0]
|
|
120
138
|
if (v.kind_of?(String) or v.kind_of?(Symbol)) and StringPattern.analyze(v, silent: true).kind_of?(StringPattern::Pattern)
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
139
|
+
count = rng.rand(5) + 1
|
|
140
|
+
hashv[key] = if expected_errors.empty?
|
|
141
|
+
distinct_strings(v, count, sp_opts)
|
|
142
|
+
else
|
|
143
|
+
count.times.map { StringPattern.generate(v, **sp_opts) }
|
|
124
144
|
end
|
|
125
145
|
array_pattern = true
|
|
126
146
|
end
|
|
@@ -137,9 +157,9 @@ class NiceHash
|
|
|
137
157
|
value_ret = Array.new
|
|
138
158
|
value.each { |v|
|
|
139
159
|
if v.is_a?(Hash)
|
|
140
|
-
ret = NiceHash.generate(v, select_hash_key, expected_errors: expected_errors, **synonyms)
|
|
160
|
+
ret = NiceHash.generate(v, select_hash_key, expected_errors: expected_errors, _rng: rng, **synonyms)
|
|
141
161
|
else
|
|
142
|
-
ret = NiceHash.generate({ doit: v }, select_hash_key, expected_errors: expected_errors, **synonyms)
|
|
162
|
+
ret = NiceHash.generate({ doit: v }, select_hash_key, expected_errors: expected_errors, _rng: rng, **synonyms)
|
|
143
163
|
ret = ret[:doit] if ret.is_a?(Hash) and ret.key?(:doit)
|
|
144
164
|
end
|
|
145
165
|
ret = v if ret.kind_of?(Hash) and ret.size == 0
|
|
@@ -152,28 +172,28 @@ class NiceHash
|
|
|
152
172
|
if value.size == Float::INFINITY
|
|
153
173
|
value = (value.min..2**64)
|
|
154
174
|
end
|
|
155
|
-
hashv[key] = rand(value)
|
|
175
|
+
hashv[key] = rng.rand(value)
|
|
156
176
|
else
|
|
157
177
|
if value.size == Float::INFINITY
|
|
158
178
|
infinite = true
|
|
159
179
|
value = (value.min..2**64)
|
|
160
180
|
else
|
|
161
181
|
infinite = false
|
|
162
|
-
hashv[key] = rand(value)
|
|
182
|
+
hashv[key] = rng.rand(value)
|
|
163
183
|
end
|
|
164
184
|
expected_errors.each do |er|
|
|
165
185
|
if er == :min_length
|
|
166
|
-
hashv[key] = rand((value.first - value.last)..value.first - 1)
|
|
186
|
+
hashv[key] = rng.rand((value.first - value.last)..value.first - 1)
|
|
167
187
|
elsif er == :max_length and !infinite
|
|
168
|
-
hashv[key] = rand((value.last + 1)..(value.last * 2))
|
|
188
|
+
hashv[key] = rng.rand((value.last + 1)..(value.last * 2))
|
|
169
189
|
elsif er == :length
|
|
170
|
-
if rand.round == 1 or infinite
|
|
171
|
-
hashv[key] = rand((value.first - value.last)..value.first - 1)
|
|
190
|
+
if rng.rand.round == 1 or infinite
|
|
191
|
+
hashv[key] = rng.rand((value.first - value.last)..value.first - 1)
|
|
172
192
|
else
|
|
173
|
-
hashv[key] = rand((value.last + 1)..(value.last * 2))
|
|
193
|
+
hashv[key] = rng.rand((value.last + 1)..(value.last * 2))
|
|
174
194
|
end
|
|
175
195
|
elsif er == :value
|
|
176
|
-
hashv[key] = :"1-10:N/L/"
|
|
196
|
+
hashv[key] = StringPattern.generate(:"1-10:N/L/", _rng: rng)
|
|
177
197
|
end
|
|
178
198
|
end
|
|
179
199
|
end
|
|
@@ -188,24 +208,24 @@ class NiceHash
|
|
|
188
208
|
elsif er == :max_length
|
|
189
209
|
hashv[key] = hashv[key] + "Z"
|
|
190
210
|
elsif er == :length
|
|
191
|
-
if rand.round == 1
|
|
211
|
+
if rng.rand.round == 1
|
|
192
212
|
hashv[key] = hashv[key].chop
|
|
193
213
|
else
|
|
194
214
|
hashv[key] = hashv[key] + "Z"
|
|
195
215
|
end
|
|
196
216
|
elsif er == :value
|
|
197
|
-
hashv[key][rand(hashv[key].size - 1)] = "1:L"
|
|
217
|
+
hashv[key][rng.rand(hashv[key].size - 1)] = StringPattern.generate("1:L", _rng: rng)
|
|
198
218
|
end
|
|
199
219
|
end
|
|
200
220
|
end
|
|
201
221
|
elsif value.kind_of?(Module) and value == Boolean
|
|
202
222
|
if expected_errors.empty?
|
|
203
|
-
hashv[key] = (rand.round == 0)
|
|
223
|
+
hashv[key] = (rng.rand.round == 0)
|
|
204
224
|
else
|
|
205
|
-
hashv[key] = (rand.round == 0)
|
|
225
|
+
hashv[key] = (rng.rand.round == 0)
|
|
206
226
|
expected_errors.each do |er|
|
|
207
227
|
if er == :value
|
|
208
|
-
hashv[key] = "1-10:L"
|
|
228
|
+
hashv[key] = StringPattern.generate("1-10:L", _rng: rng)
|
|
209
229
|
end
|
|
210
230
|
end
|
|
211
231
|
end
|
|
@@ -243,6 +263,75 @@ class NiceHash
|
|
|
243
263
|
# hashes = my_hash.generate_n(5, :correct)
|
|
244
264
|
###########################################################################
|
|
245
265
|
def NiceHash.generate_n(pattern_hash, n, select_hash_key = nil, expected_errors: [], **synonyms)
|
|
246
|
-
|
|
266
|
+
rng = generation_rng(synonyms)
|
|
267
|
+
n.times.map { NiceHash.generate(pattern_hash, select_hash_key, expected_errors: expected_errors, _rng: rng, **synonyms) }
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# :iban and :luhn, or [:iban, country] and [:luhn, length].
|
|
271
|
+
# Country nil means "generate ES, accept any IBAN on validate".
|
|
272
|
+
def self.checksum_spec(value)
|
|
273
|
+
case value
|
|
274
|
+
when :iban
|
|
275
|
+
[:iban, nil]
|
|
276
|
+
when :luhn
|
|
277
|
+
[:luhn, 16]
|
|
278
|
+
when Array
|
|
279
|
+
return nil unless (1..2).cover?(value.size)
|
|
280
|
+
|
|
281
|
+
case value[0]
|
|
282
|
+
when :iban
|
|
283
|
+
return [:iban, nil] if value[1].nil?
|
|
284
|
+
|
|
285
|
+
country = value[1].to_s.strip.upcase
|
|
286
|
+
raise ArgumentError, "unknown IBAN country: #{country.inspect}" unless StringPattern::IBAN_LENGTHS.key?(country)
|
|
287
|
+
|
|
288
|
+
[:iban, country]
|
|
289
|
+
when :luhn
|
|
290
|
+
length = value[1].nil? ? 16 : Integer(value[1])
|
|
291
|
+
raise ArgumentError, "Luhn length must be >= 2" if length < 2
|
|
292
|
+
|
|
293
|
+
[:luhn, length]
|
|
294
|
+
end
|
|
295
|
+
end
|
|
247
296
|
end
|
|
297
|
+
|
|
298
|
+
def self.generated_checksum(spec, expected_errors)
|
|
299
|
+
kind, arg = spec
|
|
300
|
+
if kind == :iban
|
|
301
|
+
country = arg || "ES"
|
|
302
|
+
return StringPattern.iban(country: country) if expected_errors.empty?
|
|
303
|
+
|
|
304
|
+
"#{country}00NOTANIBAN"
|
|
305
|
+
elsif expected_errors.empty?
|
|
306
|
+
StringPattern.luhn(length: arg)
|
|
307
|
+
else
|
|
308
|
+
invalid_luhn(arg)
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def self.invalid_luhn(length)
|
|
313
|
+
good = StringPattern.luhn(length: length)
|
|
314
|
+
digit = good[-1] == "0" ? "1" : "0"
|
|
315
|
+
good[0..-2] + digit
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Distinct values from one pattern, drawing from the shared RNG.
|
|
319
|
+
# Stops once the pattern cannot yield another new string.
|
|
320
|
+
def self.distinct_strings(pattern, count, sp_opts)
|
|
321
|
+
results = []
|
|
322
|
+
seen = {}
|
|
323
|
+
attempts = 0
|
|
324
|
+
while results.size < count && attempts < count * 20
|
|
325
|
+
attempts += 1
|
|
326
|
+
generated = StringPattern.generate(pattern, **sp_opts)
|
|
327
|
+
break if generated.nil? || generated.empty?
|
|
328
|
+
next if seen[generated]
|
|
329
|
+
|
|
330
|
+
seen[generated] = true
|
|
331
|
+
results << generated
|
|
332
|
+
end
|
|
333
|
+
results
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
private_class_method :generation_rng, :checksum_spec, :generated_checksum, :invalid_luhn, :distinct_strings
|
|
248
337
|
end
|
data/lib/nice/hash/validate.rb
CHANGED
|
@@ -45,8 +45,10 @@ class NiceHash
|
|
|
45
45
|
#
|
|
46
46
|
# results = my_hash.validate_patterns(:correct, values_to_validate)
|
|
47
47
|
# results = my_hash.validate(:correct, values_to_validate)
|
|
48
|
+
# results = my_hash.validate(values_to_validate, explain: true)
|
|
49
|
+
# explain: true replaces false or an error list with { valid:, errors:, message:, index: }.
|
|
48
50
|
###########################################################################
|
|
49
|
-
def NiceHash.validate(patterns_hash, values_hash_to_validate, only_patterns: true)
|
|
51
|
+
def NiceHash.validate(patterns_hash, values_hash_to_validate, only_patterns: true, explain: false)
|
|
50
52
|
if patterns_hash.kind_of?(Array)
|
|
51
53
|
pattern_hash = patterns_hash[0]
|
|
52
54
|
select_hash_key = patterns_hash[1]
|
|
@@ -76,7 +78,7 @@ class NiceHash
|
|
|
76
78
|
if !select_hash_key.nil? and value.keys.include?(select_hash_key)
|
|
77
79
|
value = value[select_hash_key]
|
|
78
80
|
elsif values.keys.include?(key) and values[key].kind_of?(Hash)
|
|
79
|
-
res = NiceHash.validate([value, select_hash_key], values[key], only_patterns: only_patterns)
|
|
81
|
+
res = NiceHash.validate([value, select_hash_key], values[key], only_patterns: only_patterns, explain: explain)
|
|
80
82
|
results[key] = res if res.size > 0
|
|
81
83
|
next
|
|
82
84
|
end
|
|
@@ -85,6 +87,10 @@ class NiceHash
|
|
|
85
87
|
if values.keys.include?(key)
|
|
86
88
|
if value == :uuid
|
|
87
89
|
results[key] = false unless StringPattern.valid_uuid?(values[key].to_s)
|
|
90
|
+
elsif value == :email
|
|
91
|
+
results[key] = false unless StringPattern.valid_email?(values[key].to_s)
|
|
92
|
+
elsif (spec = checksum_spec(value))
|
|
93
|
+
results[key] = false if checksum_problem(spec, values[key])
|
|
88
94
|
elsif value.kind_of?(String) or value.kind_of?(Symbol)
|
|
89
95
|
if ((StringPattern.optimistic and value.kind_of?(String)) or value.kind_of?(Symbol)) and value.to_s.scan(/^!?\d+-?\d*:.+/).size > 0
|
|
90
96
|
res = StringPattern.validate(pattern: value, text: values[key])
|
|
@@ -119,10 +125,8 @@ class NiceHash
|
|
|
119
125
|
elsif value.kind_of?(Module) and value == Boolean
|
|
120
126
|
results[key] = false unless values[key].is_a?(Boolean)
|
|
121
127
|
elsif value.kind_of?(Regexp)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
results[key] = false
|
|
125
|
-
end
|
|
128
|
+
res = StringPattern.validate(text: values[key].to_s, pattern: value)
|
|
129
|
+
results[key] = false unless res == true || (res.is_a?(Array) && res.empty?)
|
|
126
130
|
elsif value.kind_of?(Array)
|
|
127
131
|
array_pattern = false
|
|
128
132
|
complex_data = false
|
|
@@ -132,6 +136,9 @@ class NiceHash
|
|
|
132
136
|
results[key] ||= []
|
|
133
137
|
values[key].each do |vk|
|
|
134
138
|
res = StringPattern.validate(pattern: value[0], text: vk)
|
|
139
|
+
if explain && res.is_a?(Array) && !res.empty?
|
|
140
|
+
res = described_failure(value[0], vk, res, true)
|
|
141
|
+
end
|
|
135
142
|
results[key] << res
|
|
136
143
|
end
|
|
137
144
|
if results[key].flatten.size == 0
|
|
@@ -155,10 +162,10 @@ class NiceHash
|
|
|
155
162
|
if values[key].class == value.class
|
|
156
163
|
values[key].each do |v|
|
|
157
164
|
if value[0].is_a?(Hash)
|
|
158
|
-
res = NiceHash.validate([value[0], select_hash_key], v, only_patterns: only_patterns)
|
|
165
|
+
res = NiceHash.validate([value[0], select_hash_key], v, only_patterns: only_patterns, explain: explain)
|
|
159
166
|
else
|
|
160
167
|
# for the case {cars: ['Ford|Newton|Seat']}
|
|
161
|
-
res = NiceHash.validate([{ key => value[0] }, select_hash_key], { key => v }, only_patterns: only_patterns)
|
|
168
|
+
res = NiceHash.validate([{ key => value[0] }, select_hash_key], { key => v }, only_patterns: only_patterns, explain: explain)
|
|
162
169
|
#res = {key => res[:doit]} if res.is_a?(Hash) and res.key?(:doit)
|
|
163
170
|
array_pattern = true
|
|
164
171
|
end
|
|
@@ -175,10 +182,10 @@ class NiceHash
|
|
|
175
182
|
i = 0
|
|
176
183
|
value.each { |v|
|
|
177
184
|
if v.is_a?(Hash)
|
|
178
|
-
res = NiceHash.validate([v, select_hash_key], values[key][i], only_patterns: only_patterns)
|
|
185
|
+
res = NiceHash.validate([v, select_hash_key], values[key][i], only_patterns: only_patterns, explain: explain)
|
|
179
186
|
elsif v.is_a?(Array)
|
|
180
187
|
# for the case {cars: ['Ford|Newton|Seat']}
|
|
181
|
-
res = NiceHash.validate([{ key => v }, select_hash_key], { key => values[key][i] }, only_patterns: only_patterns)
|
|
188
|
+
res = NiceHash.validate([{ key => v }, select_hash_key], { key => values[key][i] }, only_patterns: only_patterns, explain: explain)
|
|
182
189
|
array_pattern = true
|
|
183
190
|
else
|
|
184
191
|
res = []
|
|
@@ -208,6 +215,7 @@ class NiceHash
|
|
|
208
215
|
end
|
|
209
216
|
}
|
|
210
217
|
end
|
|
218
|
+
explain_stored_results(results, key, value, values, same_values, explain) if explain
|
|
211
219
|
end
|
|
212
220
|
}
|
|
213
221
|
end
|
|
@@ -215,5 +223,81 @@ class NiceHash
|
|
|
215
223
|
return results
|
|
216
224
|
end
|
|
217
225
|
|
|
226
|
+
def self.checksum_problem(spec, text)
|
|
227
|
+
kind, arg = spec
|
|
228
|
+
raw = text.to_s
|
|
229
|
+
if kind == :iban
|
|
230
|
+
compact = raw.gsub(/\s+/, "").upcase
|
|
231
|
+
return [[:value], "invalid IBAN"] unless StringPattern.valid_iban?(raw)
|
|
232
|
+
return nil if arg.nil? || compact.start_with?(arg)
|
|
233
|
+
|
|
234
|
+
[[:value], "IBAN country is not #{arg}"]
|
|
235
|
+
else
|
|
236
|
+
return [[:value], "invalid Luhn number"] unless StringPattern.valid_luhn?(raw)
|
|
237
|
+
return nil if raw.size == arg
|
|
238
|
+
|
|
239
|
+
[[:length], "expected #{arg} digits"]
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def self.explain_stored_results(results, key, pattern, values, same_values, _explain)
|
|
244
|
+
keys = [key]
|
|
245
|
+
keys.concat(same_values[key]) if same_values.include?(key)
|
|
246
|
+
keys.each do |result_key|
|
|
247
|
+
payload = results[result_key]
|
|
248
|
+
next if payload.nil?
|
|
249
|
+
|
|
250
|
+
if payload == false || (payload.is_a?(Array) && !payload.empty? && payload.all? { |item| item.is_a?(Symbol) })
|
|
251
|
+
results[result_key] = explained_result(pattern, values[result_key], payload)
|
|
252
|
+
elsif payload.is_a?(String)
|
|
253
|
+
results[result_key] = { valid: false, errors: [:value], message: payload, index: nil }
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def self.explained_result(pattern, text, plain)
|
|
259
|
+
spec = checksum_spec(pattern)
|
|
260
|
+
if spec && (problem = checksum_problem(spec, text))
|
|
261
|
+
errors, message = problem
|
|
262
|
+
return { valid: false, errors: errors, message: message, index: nil }
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
described_failure(pattern, text, plain, true)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def self.described_failure(pattern, text, plain, explain)
|
|
269
|
+
return plain unless explain
|
|
270
|
+
return StringPattern.explain(text: text, pattern: pattern) if explainable_pattern?(pattern)
|
|
271
|
+
return { valid: false, errors: [:value], message: plain, index: nil } if plain.is_a?(String)
|
|
272
|
+
|
|
273
|
+
errors = plain.is_a?(Array) ? plain : [:value]
|
|
274
|
+
{ valid: false, errors: errors, message: failure_message(pattern, errors), index: nil }
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def self.explainable_pattern?(pattern)
|
|
278
|
+
return true if pattern.is_a?(Regexp)
|
|
279
|
+
if pattern.is_a?(String) || pattern.is_a?(Symbol)
|
|
280
|
+
return StringPattern.analyze(pattern, silent: true).is_a?(StringPattern::Pattern)
|
|
281
|
+
end
|
|
282
|
+
return false unless pattern.is_a?(Array) && pattern.size > 1
|
|
283
|
+
|
|
284
|
+
pattern.all? { |part|
|
|
285
|
+
part.is_a?(Regexp) || (
|
|
286
|
+
(part.is_a?(String) || part.is_a?(Symbol)) &&
|
|
287
|
+
StringPattern.analyze(part, silent: true).is_a?(StringPattern::Pattern)
|
|
288
|
+
)
|
|
289
|
+
}
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def self.failure_message(pattern, errors)
|
|
293
|
+
case pattern
|
|
294
|
+
when :uuid then "invalid UUID"
|
|
295
|
+
when :email then "invalid email"
|
|
296
|
+
else
|
|
297
|
+
errors == [:value] ? "invalid" : "invalid: #{errors.join(", ")}"
|
|
298
|
+
end
|
|
299
|
+
end
|
|
218
300
|
|
|
301
|
+
private_class_method :checksum_problem, :explain_stored_results, :explained_result,
|
|
302
|
+
:described_failure, :explainable_pattern?, :failure_message
|
|
219
303
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: nice_hash
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.21.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mario Ruiz
|
|
@@ -15,20 +15,20 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '2.
|
|
18
|
+
version: '2.5'
|
|
19
19
|
- - ">="
|
|
20
20
|
- !ruby/object:Gem::Version
|
|
21
|
-
version: 2.
|
|
21
|
+
version: 2.5.0
|
|
22
22
|
type: :runtime
|
|
23
23
|
prerelease: false
|
|
24
24
|
version_requirements: !ruby/object:Gem::Requirement
|
|
25
25
|
requirements:
|
|
26
26
|
- - "~>"
|
|
27
27
|
- !ruby/object:Gem::Version
|
|
28
|
-
version: '2.
|
|
28
|
+
version: '2.5'
|
|
29
29
|
- - ">="
|
|
30
30
|
- !ruby/object:Gem::Version
|
|
31
|
-
version: 2.
|
|
31
|
+
version: 2.5.0
|
|
32
32
|
- !ruby/object:Gem::Dependency
|
|
33
33
|
name: rspec
|
|
34
34
|
requirement: !ruby/object:Gem::Requirement
|