nice_hash 1.20.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 +20 -6
- data/lib/nice/hash/add_to_ruby.rb +4 -4
- data/lib/nice/hash/generate.rb +75 -10
- data/lib/nice/hash/validate.rb +90 -10
- metadata +1 -1
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
|
@@ -96,17 +96,24 @@ require 'nice_hash'
|
|
|
96
96
|
|
|
97
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.
|
|
98
98
|
|
|
99
|
-
For **UUID v4**, **email**, **
|
|
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):
|
|
100
100
|
|
|
101
101
|
```ruby
|
|
102
102
|
my_hash = { id: :uuid, email: :email, iban: :iban, card: :luhn, doomId: :"10:N" }
|
|
103
|
-
# id:
|
|
103
|
+
# id: "550e8400-e29b-41d4-a716-446655440000"
|
|
104
104
|
# email: a generated address
|
|
105
|
-
# iban: a Spanish IBAN
|
|
105
|
+
# iban: a Spanish IBAN
|
|
106
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
|
|
107
112
|
```
|
|
108
113
|
|
|
109
|
-
`:
|
|
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:`.
|
|
110
117
|
|
|
111
118
|
Or use a Regular expression for custom formats:
|
|
112
119
|
|
|
@@ -742,6 +749,13 @@ The possible validation values returned:
|
|
|
742
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.
|
|
743
750
|
:string_set_not_allowed: it will include one or more characters that are not supposed to be on the string.
|
|
744
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
|
+
|
|
745
759
|
You can also supply nested pattern keys for the patterns to be more specific: `{'draws.drawId': :"4:N"}`
|
|
746
760
|
|
|
747
761
|
### Change only one value at a time and return an Array of Hashes
|
|
@@ -899,8 +913,8 @@ Valid patterns:
|
|
|
899
913
|
- selectors, one of the values. Example: "uno|dos|tres"
|
|
900
914
|
- **:uuid** (string_pattern 2.4+): UUID v4 format
|
|
901
915
|
- **:email** (string_pattern 2.5+): email address
|
|
902
|
-
- **:iban** (string_pattern 2.5+): IBAN,
|
|
903
|
-
- **:luhn** (string_pattern 2.5+): 16 digits
|
|
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
|
|
904
918
|
|
|
905
919
|
### Other useful methods
|
|
906
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
|
@@ -91,12 +91,9 @@ class NiceHash
|
|
|
91
91
|
hashv[key] = expected_errors.empty? ? StringPattern.uuid : (StringPattern.uuid[0..-2] + "X")
|
|
92
92
|
elsif value == :email
|
|
93
93
|
hashv[key] = StringPattern.generate("20-40:@", **sp_opts)
|
|
94
|
-
elsif
|
|
95
|
-
# StringPattern.iban
|
|
96
|
-
hashv[key] =
|
|
97
|
-
elsif value == :luhn
|
|
98
|
-
# StringPattern.luhn uses its own Random, so it is not reproducible via seed:.
|
|
99
|
-
hashv[key] = expected_errors.empty? ? StringPattern.luhn(length: 16) : "1234567890123456"
|
|
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)
|
|
100
97
|
elsif value.kind_of?(String) or value.kind_of?(Symbol)
|
|
101
98
|
if ((StringPattern.optimistic and value.kind_of?(String)) or value.kind_of?(Symbol)) and value.to_s.scan(/^!?\d+-?\d*:.+/).size > 0
|
|
102
99
|
hashv[key] = StringPattern.generate(value, **sp_opts)
|
|
@@ -139,9 +136,11 @@ class NiceHash
|
|
|
139
136
|
if value.size == 1
|
|
140
137
|
v = value[0]
|
|
141
138
|
if (v.kind_of?(String) or v.kind_of?(Symbol)) and StringPattern.analyze(v, silent: true).kind_of?(StringPattern::Pattern)
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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) }
|
|
145
144
|
end
|
|
146
145
|
array_pattern = true
|
|
147
146
|
end
|
|
@@ -268,5 +267,71 @@ class NiceHash
|
|
|
268
267
|
n.times.map { NiceHash.generate(pattern_hash, select_hash_key, expected_errors: expected_errors, _rng: rng, **synonyms) }
|
|
269
268
|
end
|
|
270
269
|
|
|
271
|
-
|
|
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
|
|
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
|
|
272
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
|
|
@@ -87,10 +89,8 @@ class NiceHash
|
|
|
87
89
|
results[key] = false unless StringPattern.valid_uuid?(values[key].to_s)
|
|
88
90
|
elsif value == :email
|
|
89
91
|
results[key] = false unless StringPattern.valid_email?(values[key].to_s)
|
|
90
|
-
elsif
|
|
91
|
-
results[key] = false
|
|
92
|
-
elsif value == :luhn
|
|
93
|
-
results[key] = false unless StringPattern.valid_luhn?(values[key].to_s)
|
|
92
|
+
elsif (spec = checksum_spec(value))
|
|
93
|
+
results[key] = false if checksum_problem(spec, values[key])
|
|
94
94
|
elsif value.kind_of?(String) or value.kind_of?(Symbol)
|
|
95
95
|
if ((StringPattern.optimistic and value.kind_of?(String)) or value.kind_of?(Symbol)) and value.to_s.scan(/^!?\d+-?\d*:.+/).size > 0
|
|
96
96
|
res = StringPattern.validate(pattern: value, text: values[key])
|
|
@@ -136,6 +136,9 @@ class NiceHash
|
|
|
136
136
|
results[key] ||= []
|
|
137
137
|
values[key].each do |vk|
|
|
138
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
|
|
139
142
|
results[key] << res
|
|
140
143
|
end
|
|
141
144
|
if results[key].flatten.size == 0
|
|
@@ -159,10 +162,10 @@ class NiceHash
|
|
|
159
162
|
if values[key].class == value.class
|
|
160
163
|
values[key].each do |v|
|
|
161
164
|
if value[0].is_a?(Hash)
|
|
162
|
-
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)
|
|
163
166
|
else
|
|
164
167
|
# for the case {cars: ['Ford|Newton|Seat']}
|
|
165
|
-
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)
|
|
166
169
|
#res = {key => res[:doit]} if res.is_a?(Hash) and res.key?(:doit)
|
|
167
170
|
array_pattern = true
|
|
168
171
|
end
|
|
@@ -179,10 +182,10 @@ class NiceHash
|
|
|
179
182
|
i = 0
|
|
180
183
|
value.each { |v|
|
|
181
184
|
if v.is_a?(Hash)
|
|
182
|
-
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)
|
|
183
186
|
elsif v.is_a?(Array)
|
|
184
187
|
# for the case {cars: ['Ford|Newton|Seat']}
|
|
185
|
-
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)
|
|
186
189
|
array_pattern = true
|
|
187
190
|
else
|
|
188
191
|
res = []
|
|
@@ -212,6 +215,7 @@ class NiceHash
|
|
|
212
215
|
end
|
|
213
216
|
}
|
|
214
217
|
end
|
|
218
|
+
explain_stored_results(results, key, value, values, same_values, explain) if explain
|
|
215
219
|
end
|
|
216
220
|
}
|
|
217
221
|
end
|
|
@@ -219,5 +223,81 @@ class NiceHash
|
|
|
219
223
|
return results
|
|
220
224
|
end
|
|
221
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
|
|
222
300
|
|
|
301
|
+
private_class_method :checksum_problem, :explain_stored_results, :explained_result,
|
|
302
|
+
:described_failure, :explainable_pattern?, :failure_message
|
|
223
303
|
end
|