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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 718327558f69c77e13b7d19355f990e171eaf199676e8a42ced4bda0c1063827
4
- data.tar.gz: d3a086c1262af567acc060fbe6cfb18a6bfbd134b4a1df807e9ee52666f0d4d4
3
+ metadata.gz: ab97a22d06c02e9a17c33221c77ff01c841405e0b3a15711ecb465bbf91eb23c
4
+ data.tar.gz: c358cab3956cd0f4dd90020ad68da0039f42786892d71f3803a2dcbc8be87f6b
5
5
  SHA512:
6
- metadata.gz: a29e7907b0feca5db0253faa574a1fba185e37ea654881b999ef3f7ea48da52c42294963ad40f19fee657f121dde0ac11f8e41881d1115a7ad89b505d19afc24
7
- data.tar.gz: b1eaa9bd0c1f6baf034aaf524222bb584265495c4bb8bf014ceeb5f6404bbe6364db7ce8cf786c4cece3daf670aa2751ca65eea6986c4145930cf5091d065ae3
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**, **Spanish IBAN** and a **16-digit Luhn** number you can use these shorthands (string_pattern 2.4+ for `:uuid`, 2.5+ for the others):
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: "550e8400-e29b-41d4-a716-446655440000"
103
+ # id: "550e8400-e29b-41d4-a716-446655440000"
104
104
  # email: a generated address
105
- # iban: a Spanish IBAN (country is always ES)
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
- `:uuid`, `:iban` and `:luhn` use their own random generator. `seed:` does not make those three fields repeat.
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, generated as Spanish (`ES`)
903
- - **:luhn** (string_pattern 2.5+): 16 digits with a valid Luhn checksum
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
  ###########################################################################
@@ -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 value == :iban
95
- # StringPattern.iban uses its own Random, so it is not reproducible via seed:.
96
- hashv[key] = expected_errors.empty? ? StringPattern.iban(country: "ES") : "ES00NOTANIBAN"
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
- hashv[key] = []
143
- (rng.rand(5)+1).times do
144
- hashv[key]<<StringPattern.generate(v, **sp_opts)
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
- private_class_method :generation_rng
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
@@ -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 value == :iban
91
- results[key] = false unless StringPattern.valid_iban?(values[key].to_s)
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
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.20.0
4
+ version: 1.21.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mario Ruiz