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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7eae7fae66c4ee14c40481157a8cd6e90ec43781b092779a3344c36a7d0d6308
4
- data.tar.gz: f3071e0977d3e7e0f0e7ced9699f4823535c1daaa880cbfd7d5693d8329e9513
3
+ metadata.gz: ab97a22d06c02e9a17c33221c77ff01c841405e0b3a15711ecb465bbf91eb23c
4
+ data.tar.gz: c358cab3956cd0f4dd90020ad68da0039f42786892d71f3803a2dcbc8be87f6b
5
5
  SHA512:
6
- metadata.gz: 72ced814e688ca65212c8812969947ce77f0496c2e7dbc12e2ab9ef9c396db77da473e5c079788206c6fcc24e05d92b1186c85bfe986c634e75e3b7894208b5a
7
- data.tar.gz: bce43cdcaa33a6e50a06a364737e3a723156e052b34ba3423574dc4f296743f47c7b089723f72ba2ad617ece70f3ec129617dcf500fc6c5d5cddd8cf8f7bba27
6
+ metadata.gz: a4c876a1bd17ee4bf3a845dedb57aee727e73e9173a1145a103ce2da75daeec422c768a8bc3ba82c051734221079e184925c1b432f8d6be4bc240b4e43d2854d
7
+ data.tar.gz: 074570bccde6d2a926286817b13f935be05e34fbc57835a761bfe745c7474be1bd5585a5fef812cfb36d03d64c03e315d08fc6dd4ca2ad65d1afb84b14c80fe3
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # NiceHash
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/nice_hash.svg)](https://rubygems.org/gems/nice_hash)
4
- [![Build Status](https://travis-ci.com/MarioRuiz/nice_hash.svg?branch=master)](https://github.com/MarioRuiz/nice_hash)
4
+ [![CI](https://github.com/MarioRuiz/nice_hash/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/MarioRuiz/nice_hash/actions/workflows/ci.yml)
5
5
  [![Coverage Status](https://coveralls.io/repos/github/MarioRuiz/nice_hash/badge.svg?branch=master)](https://coveralls.io/github/MarioRuiz/nice_hash?branch=master)
6
6
  ![Gem](https://img.shields.io/gem/dt/nice_hash)
7
7
  ![GitHub commit activity](https://img.shields.io/github/commit-activity/y/MarioRuiz/nice_hash)
@@ -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 the shorthand `:uuid` (requires string_pattern 2.4+):
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, key: "Wsdf88888", doomId: :"10:N" }
97
- # id will be like "550e8400-e29b-41d4-a716-446655440000"
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
- With **string_pattern** 2.4+, pass `seed:` to get the same hash every time (useful for tests):
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
- # => array of 5 hashes
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
  ###########################################################################
@@ -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[:errors] if synonyms.keys.include?(:errors)
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
- sp_opts = { expected_errors: expected_errors }.merge(synonyms)
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
- hashv[key] = []
122
- (rand(5)+1).times do
123
- 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) }
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/".gen
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".gen
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".gen
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
- n.times.map { NiceHash.generate(pattern_hash, select_hash_key, expected_errors: expected_errors, **synonyms) }
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
@@ -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
- rex = Regexp.new("^#{value}$")
123
- unless values[key].to_s.match?(rex)
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.19.0
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.4'
18
+ version: '2.5'
19
19
  - - ">="
20
20
  - !ruby/object:Gem::Version
21
- version: 2.4.0
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.4'
28
+ version: '2.5'
29
29
  - - ">="
30
30
  - !ruby/object:Gem::Version
31
- version: 2.4.0
31
+ version: 2.5.0
32
32
  - !ruby/object:Gem::Dependency
33
33
  name: rspec
34
34
  requirement: !ruby/object:Gem::Requirement