nice_hash 1.18.7 → 1.20.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: 1c8cb3e741e17cf400b34bc3a31ec874fb030466b2f56431f0ec5aa79e37af7f
4
- data.tar.gz: f023c96b5a5768bac968765032c782d1d01e9705f778748903fb75a0b91d582e
3
+ metadata.gz: 718327558f69c77e13b7d19355f990e171eaf199676e8a42ced4bda0c1063827
4
+ data.tar.gz: d3a086c1262af567acc060fbe6cfb18a6bfbd134b4a1df807e9ee52666f0d4d4
5
5
  SHA512:
6
- metadata.gz: 448740fdbc4843c705ac79d5d33ba6fcfdf453e7ff76d1a0cf77fa2a648c37ca99d441fcef8378030e2de77a100d907832e216938948c9f3b8ebba3e8ef75b8a
7
- data.tar.gz: 6d3acf6139d0612d7d3ae45b5fec8ca19ba66262f9b4f6b71c26e66c4ca1df68f60a7d6f120307e267d06752d3db3fec43265e82004bf1ef9143ec4ea89531ed
6
+ metadata.gz: a29e7907b0feca5db0253faa574a1fba185e37ea654881b999ef3f7ea48da52c42294963ad40f19fee657f121dde0ac11f8e41881d1115a7ad89b505d19afc24
7
+ data.tar.gz: b1eaa9bd0c1f6baf034aaf524222bb584265495c4bb8bf014ceeb5f6404bbe6364db7ce8cf786c4cece3daf670aa2751ca65eea6986c4145930cf5091d065ae3
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)
@@ -22,31 +22,37 @@ To generate the strings following a pattern take a look at the documentation for
22
22
  To use nice_hash on Http connections take a look at nice_http gem: https://github.com/MarioRuiz/nice_http
23
23
 
24
24
  ## Table of contents
25
- - [Installation](#installation)
26
- - [Usage](#usage)
27
- * [How to access the different keys](#how-to-access-the-different-keys)
28
- * [Change all values on the keys we specified](#change-all-values-on-the-keys-we-specified)
29
- * [Filtering / Selecting an specific key on the hash and subhashes](#filtering---selecting-an-specific-key-on-the-hash-and-subhashes)
30
- * [How to generate the hash with the criteria we want](#how-to-generate-the-hash-with-the-criteria-we-want)
31
- * [How to generate the hash with wrong values for the string patterns specified on the hash](#how-to-generate-the-hash-with-wrong-values-for-the-string-patterns-specified-on-the-hash)
32
- * [Return the select_fields or the pattern_fields](#return-the-select-fields-or-the-pattern-fields)
33
- * [dig and bury Hash methods](#dig-and-bury-hash-methods)
34
- * [Validating hashes](#validating-hashes)
35
- * [Change only one value at a time and return an Array of Hashes](#change-only-one-value-at-a-time-and-return-an-array-of-hashes)
36
- * [Adding other values on run time when calling `generate` method](#adding-other-values-on-run-time-when-calling--generate--method)
37
- + [Accessing other values of the hash on run time](#accessing-other-values-of-the-hash-on-run-time)
38
- * [Compare the structure of a replica with the supplied structure](#compare-the-structure-of-a-replica-with-the-supplied-structure)
39
- * [Other useful methods](#other-useful-methods)
40
- + [Time stamp](#time-stamp)
41
- + [Random dates](#random-dates)
42
- + [Deep copy of a hash](#deep-copy-of-a-hash)
43
- + [Nested deletion](#nested-deletion)
44
- + [Deep merge of two hashes](#deep-merge-of-two-hashes)
45
- + [Boolean class](#boolean-class)
46
- * [Other tools integration](#other-tools-integration)
47
- + [Tabulo](#tabulo)
48
- - [Contributing](#contributing)
49
- - [License](#license)
25
+ - [NiceHash](#nicehash)
26
+ - [Table of contents](#table-of-contents)
27
+ - [Installation](#installation)
28
+ - [Configuration](#configuration)
29
+ - [Usage](#usage)
30
+ - [How to access the different keys](#how-to-access-the-different-keys)
31
+ - [Change all values on the keys we specified](#change-all-values-on-the-keys-we-specified)
32
+ - [Filtering / Selecting an specific key on the hash and subhashes](#filtering--selecting-an-specific-key-on-the-hash-and-subhashes)
33
+ - [How to generate the hash with the criteria we want](#how-to-generate-the-hash-with-the-criteria-we-want)
34
+ - [How to generate the hash with wrong values for the string patterns specified on the hash](#how-to-generate-the-hash-with-wrong-values-for-the-string-patterns-specified-on-the-hash)
35
+ - [Return the select\_fields or the pattern\_fields](#return-the-select_fields-or-the-pattern_fields)
36
+ - [dig and bury Hash methods](#dig-and-bury-hash-methods)
37
+ - [Validating hashes](#validating-hashes)
38
+ - [Change only one value at a time and return an Array of Hashes](#change-only-one-value-at-a-time-and-return-an-array-of-hashes)
39
+ - [Adding other values on run time when calling `generate` method](#adding-other-values-on-run-time-when-calling-generate-method)
40
+ - [Accessing other values of the hash on run time](#accessing-other-values-of-the-hash-on-run-time)
41
+ - [Compare the structure of a replica with the supplied structure](#compare-the-structure-of-a-replica-with-the-supplied-structure)
42
+ - [Other useful methods](#other-useful-methods)
43
+ - [Hash diff](#hash-diff)
44
+ - [Flatten / unflatten keys](#flatten--unflatten-keys)
45
+ - [Time stamp](#time-stamp)
46
+ - [Random dates](#random-dates)
47
+ - [Deep copy of a hash](#deep-copy-of-a-hash)
48
+ - [Nested deletion](#nested-deletion)
49
+ - [Deep merge of two hashes](#deep-merge-of-two-hashes)
50
+ - [Boolean class](#boolean-class)
51
+ - [in?(array)](#inarray)
52
+ - [Other tools integration](#other-tools-integration)
53
+ - [Tabulo](#tabulo)
54
+ - [Contributing](#contributing)
55
+ - [License](#license)
50
56
 
51
57
  ## Installation
52
58
 
@@ -64,9 +70,45 @@ Or install it yourself as:
64
70
 
65
71
  $ gem install nice_hash
66
72
 
73
+ ## Configuration
74
+
75
+ Set these constants **before** `require 'nice_hash'` if you need to change default behavior:
76
+
77
+ - **`SP_ADD_TO_RUBY`** (default: `true`): When `true`, the gem adds methods to core classes (`Hash`, `Array`, `String`, `Object`, `Date`, `Time`, etc.). Set to `false` to avoid patching core classes and use only `NiceHash` class methods.
78
+ - **`SP_COMPARE_NUMBERS_AS_STRINGS`** (default: `true`): When `true`, `String#==` is overridden so that strings are compared with numbers as strings (e.g. `'300' == 300` and `'' == nil`). Set to `false` if this causes issues with other gems (e.g. some versions of `net/ldap`).
79
+
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
+
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
+
88
+ Example:
89
+
90
+ ```ruby
91
+ SP_COMPARE_NUMBERS_AS_STRINGS = false
92
+ require 'nice_hash'
93
+ ```
94
+
67
95
  ## Usage
68
96
 
69
- 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. All you have to do is to add to a key the value as a Regular expression, for example the key uuid in here will generate a random value like this: "E0BDE5B5-A738-49E6-83C1-9D1FFB313788"
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
+
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):
100
+
101
+ ```ruby
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 (country is always ES)
106
+ # card: 16 digits with a valid Luhn checksum
107
+ ```
108
+
109
+ `:uuid`, `:iban` and `:luhn` use their own random generator. `seed:` does not make those three fields repeat.
110
+
111
+ Or use a Regular expression for custom formats:
70
112
 
71
113
  ```ruby
72
114
  my_hash = {
@@ -371,6 +413,23 @@ new_hash = my_hash.generate(:correct)
371
413
  new_hash = my_hash.select_key(:correct).generate
372
414
  ```
373
415
 
416
+ **Reproducible generation (seed)**
417
+ 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.
418
+
419
+ ```ruby
420
+ new_hash = my_hash.generate(:correct, seed: 42)
421
+ # Same result every time for the same seed
422
+ ```
423
+
424
+ **Generate n hashes at once**
425
+ Use `generate_n` (alias `gen_n`) to build several different hashes in one call (e.g. for bulk or boundary tests):
426
+
427
+ ```ruby
428
+ hashes = NiceHash.generate_n(my_hash, 5, :correct)
429
+ hashes = my_hash.generate_n(5, :correct) # or my_hash.gen_n(5, :correct)
430
+ # With seed: hashes = my_hash.generate_n(5, :correct, seed: 123)
431
+ # the same seed yields the same 5 hashes, and they differ from each other
432
+ ```
374
433
 
375
434
  In case of filtering by :correct new_hash would have a value like this for example:
376
435
 
@@ -838,9 +897,38 @@ Valid patterns:
838
897
  - 10.. (from 10 to infinite) Only from Ruby 2.6
839
898
  - 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
840
899
  - selectors, one of the values. Example: "uno|dos|tres"
900
+ - **:uuid** (string_pattern 2.4+): UUID v4 format
901
+ - **: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
841
904
 
842
905
  ### Other useful methods
843
906
 
907
+ #### Hash diff
908
+ Compare two hashes and get differences with dot-notation paths (useful for API or test assertions):
909
+
910
+ ```ruby
911
+ expected = { user: { address: { city: "Madrid" } } }
912
+ actual = { user: { address: { city: "London" } } }
913
+ NiceHash.diff(expected, actual)
914
+ #=> { "user.address.city" => { expected: "Madrid", got: "London" } }
915
+
916
+ # Or on a hash instance:
917
+ expected.diff(actual)
918
+ ```
919
+
920
+ #### Flatten / unflatten keys
921
+ Convert between nested hashes and flat hashes with dot-notation keys:
922
+
923
+ ```ruby
924
+ h = { user: { address: { city: "Madrid" } } }
925
+ h.flatten_keys
926
+ #=> { "user.address.city" => "Madrid" }
927
+
928
+ { "user.address.city" => "Madrid" }.unflatten_keys
929
+ #=> { user: { address: { city: "Madrid" } } }
930
+ ```
931
+
844
932
  #### Time stamp
845
933
  In case you need the time stamp, we added the method `stamp` to the `Time` class
846
934
 
@@ -166,6 +166,16 @@ class Hash
166
166
  # my_hash.city="Paris"
167
167
  # my_hash.products[1].price.wrong="AAAAA"
168
168
  ###########################################################################
169
+ def respond_to_missing?(method_name, include_private = false)
170
+ sym = (method_name.to_s[0] == "_" ? method_name.to_s[1..-1].to_sym : method_name)
171
+ return true if key?(sym) || key?(sym.to_s)
172
+ if method_name.to_s[-1] == "="
173
+ setter_key = method_name.to_s.chop
174
+ return true if key?(setter_key) || key?(setter_key.to_sym)
175
+ end
176
+ false
177
+ end
178
+
169
179
  def method_missing(m, *arguments, &block)
170
180
  m = m[1..-1].to_sym if m[0] == "_"
171
181
  if key?(m)
@@ -231,6 +241,13 @@ class Hash
231
241
  NiceHash.generate(self, select_hash_key, expected_errors: expected_errors, **synonyms)
232
242
  end
233
243
 
244
+ ###########################################################################
245
+ # Generates n different hashes from the same pattern. More info: NiceHash.generate_n
246
+ ###########################################################################
247
+ def generate_n(n, select_hash_key = nil, expected_errors: [], **synonyms)
248
+ NiceHash.generate_n(self, n, select_hash_key, expected_errors: expected_errors, **synonyms)
249
+ end
250
+
234
251
  ###########################################################################
235
252
  # Validates a given values_hash_to_validate with string patterns and select fields
236
253
  # More info: NiceHash.validate
@@ -298,6 +315,28 @@ class Hash
298
315
  NiceHash.nice_filter(self, keys)
299
316
  end
300
317
 
318
+ ###########################################################################
319
+ # Compares with another hash and returns differences with dot-notation paths.
320
+ # More info: NiceHash.diff
321
+ ###########################################################################
322
+ def diff(other)
323
+ NiceHash.diff(self, other)
324
+ end
325
+
326
+ ###########################################################################
327
+ # Flattens the hash to dot-notation keys. More info: NiceHash.flatten_keys
328
+ ###########################################################################
329
+ def flatten_keys
330
+ NiceHash.flatten_keys(self)
331
+ end
332
+
333
+ ###########################################################################
334
+ # Unflattens a hash with dot-notation keys into nested hash. More info: NiceHash.unflatten_keys
335
+ ###########################################################################
336
+ def unflatten_keys
337
+ NiceHash.unflatten_keys(self)
338
+ end
339
+
301
340
  ###########################################################################
302
341
  # Merging multi-dimensional hashes
303
342
  ###########################################################################
@@ -322,6 +361,7 @@ class Hash
322
361
  end
323
362
 
324
363
  alias gen generate
364
+ alias gen_n generate_n
325
365
  alias val validate
326
366
  alias patterns pattern_fields
327
367
  alias nice_copy deep_copy
@@ -365,6 +405,11 @@ class Array
365
405
  # my_array.city
366
406
  # my_array._name
367
407
  ###########################################################################
408
+ def respond_to_missing?(method_name, include_private = false)
409
+ sym = (method_name.to_s[0] == "_" ? method_name.to_s[1..-1].to_sym : method_name)
410
+ any? { |elem| elem.is_a?(Hash) && (elem.key?(sym) || elem.key?(sym.to_s)) }
411
+ end
412
+
368
413
  def method_missing(m, *arguments, &block)
369
414
  m = m[1..-1].to_sym if m[0] == "_"
370
415
  array = []
@@ -21,6 +21,7 @@ class NiceHash
21
21
  # #>{:user=>{:address=>{:city=>"Madrid", :country=>"Spain"}, :name=>"Peter", :age=>33}, :customer=>true}
22
22
  ##################################################
23
23
  def self.deep_clone(obj)
24
+ return obj.clone unless obj.is_a?(Array) || obj.is_a?(Hash)
24
25
  obj.clone.tap do |new_obj|
25
26
  if new_obj.is_a?(Array)
26
27
  new_obj.each_with_index do |val, i|
@@ -30,8 +31,6 @@ class NiceHash
30
31
  new_obj.each do |key, val|
31
32
  new_obj[key] = deep_clone(val)
32
33
  end
33
- else
34
- new_obj = obj.clone
35
34
  end
36
35
  end
37
36
  end
@@ -27,11 +27,16 @@ class NiceHash
27
27
  if keys.size == 1
28
28
  hash.delete(nested_key.to_sym)
29
29
  else
30
- last_key = keys[-1]
31
- eval("hash.#{keys[0..(keys.size - 2)].join(".")}.delete(:#{last_key})")
30
+ parent = hash
31
+ keys[0..-2].each do |k|
32
+ sym = k.to_sym
33
+ return hash unless parent.is_a?(Hash) && parent.key?(sym)
34
+ parent = parent[sym]
35
+ end
36
+ parent.delete(keys[-1].to_sym) if parent.is_a?(Hash)
32
37
  end
33
38
  return hash
34
39
  end
35
40
 
36
-
37
- end
41
+
42
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ class NiceHash
4
+ ##################################################
5
+ # Compares two hashes and returns differences with dot-notation paths.
6
+ #
7
+ # @param expected [Hash, Array] Expected structure (hash or array of hashes)
8
+ # @param actual [Hash, Array] Actual structure to compare
9
+ # @param prefix [String] Internal: current path prefix for recursion
10
+ #
11
+ # @return [Hash] Path => { expected: value, got: value } for each difference
12
+ #
13
+ # @example
14
+ # expected = { user: { address: { city: "Madrid" } } }
15
+ # actual = { user: { address: { city: "London" } } }
16
+ # NiceHash.diff(expected, actual)
17
+ # #=> { "user.address.city" => { expected: "Madrid", got: "London" } }
18
+ ##################################################
19
+ def self.diff(expected, actual, prefix = "")
20
+ result = {}
21
+ exp_keys = expected.is_a?(Hash) ? expected.keys : (expected.is_a?(Array) ? (0...expected.size) : nil)
22
+ act_keys = actual.is_a?(Hash) ? actual.keys : (actual.is_a?(Array) ? (0...actual.size) : nil)
23
+
24
+ if expected.is_a?(Hash) && actual.is_a?(Hash)
25
+ all_keys = (expected.keys + actual.keys).uniq
26
+ all_keys.each do |k|
27
+ path = prefix.empty? ? k.to_s : "#{prefix}.#{k}"
28
+ exp_v = expected[k]
29
+ act_v = actual[k]
30
+ if !expected.key?(k)
31
+ result[path] = { expected: nil, got: act_v }
32
+ elsif !actual.key?(k)
33
+ result[path] = { expected: exp_v, got: nil }
34
+ elsif exp_v.is_a?(Hash) && act_v.is_a?(Hash)
35
+ result.merge!(diff(exp_v, act_v, path))
36
+ elsif exp_v.is_a?(Array) && act_v.is_a?(Array)
37
+ max_len = [exp_v.size, act_v.size].max
38
+ max_len.times do |i|
39
+ result.merge!(diff(exp_v[i], act_v[i], "#{path}[#{i}]"))
40
+ end
41
+ elsif exp_v != act_v
42
+ result[path] = { expected: exp_v, got: act_v }
43
+ end
44
+ end
45
+ elsif expected.is_a?(Array) && actual.is_a?(Array)
46
+ max_len = [expected.size, actual.size].max
47
+ max_len.times do |i|
48
+ result.merge!(diff(expected[i], actual[i], prefix.empty? ? "[#{i}]" : "#{prefix}[#{i}]"))
49
+ end
50
+ elsif expected != actual
51
+ result[prefix] = { expected: expected, got: actual } unless prefix.empty?
52
+ end
53
+ result
54
+ end
55
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ class NiceHash
4
+ ##################################################
5
+ # Flattens a nested hash so keys become dot-notation strings.
6
+ #
7
+ # @param hash [Hash] The hash to flatten
8
+ # @param prefix [String] Internal: current path prefix for recursion
9
+ #
10
+ # @return [Hash] Flat hash with string keys like "user.address.city"
11
+ #
12
+ # @example
13
+ # NiceHash.flatten_keys({ user: { address: { city: "Madrid" } } })
14
+ # #=> { "user.address.city" => "Madrid" }
15
+ ##################################################
16
+ def self.flatten_keys(hash, prefix = "")
17
+ return {} unless hash.is_a?(Hash)
18
+ result = {}
19
+ hash.each do |k, v|
20
+ path = prefix.empty? ? k.to_s : "#{prefix}.#{k}"
21
+ if v.is_a?(Hash)
22
+ result.merge!(flatten_keys(v, path))
23
+ else
24
+ result[path] = v
25
+ end
26
+ end
27
+ result
28
+ end
29
+
30
+ ##################################################
31
+ # Unflattens a hash with dot-notation keys into a nested hash.
32
+ #
33
+ # @param hash [Hash] Flat hash with string keys like "user.address.city"
34
+ #
35
+ # @return [Hash] Nested hash
36
+ #
37
+ # @example
38
+ # NiceHash.unflatten_keys({ "user.address.city" => "Madrid" })
39
+ # #=> { user: { address: { city: "Madrid" } } }
40
+ ##################################################
41
+ def self.unflatten_keys(hash)
42
+ return {} unless hash.is_a?(Hash)
43
+ result = {}
44
+ hash.each do |key_str, value|
45
+ keys = key_str.to_s.split(".")
46
+ keys = keys.map { |k| k.match?(/\A\d+\z/) ? k.to_i : k.to_sym }
47
+ current = result
48
+ keys[0..-2].each do |k|
49
+ current[k] = {} unless current[k].is_a?(Hash)
50
+ current = current[k]
51
+ end
52
+ current[keys[-1]] = value
53
+ end
54
+ result
55
+ end
56
+ end
@@ -51,13 +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
72
+ rng = generation_rng(synonyms)
73
+ sp_opts = { expected_errors: expected_errors, _rng: rng }.merge(synonyms)
61
74
 
62
75
  if pattern_hash.kind_of?(Hash) and pattern_hash.size > 0
63
76
  pattern_hash.each { |key, value|
@@ -70,14 +83,25 @@ class NiceHash
70
83
  if value.keys.include?(select_hash_key)
71
84
  value = value[select_hash_key]
72
85
  else
73
- value = NiceHash.generate(value, select_hash_key, expected_errors: expected_errors)
86
+ value = NiceHash.generate(value, select_hash_key, expected_errors: expected_errors, _rng: rng, **synonyms)
74
87
  end
75
88
  end
76
- if value.kind_of?(String) or value.kind_of?(Symbol)
89
+ if value == :uuid
90
+ # SecureRandom is not seeded. UUID values are not reproducible via seed:.
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 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"
100
+ elsif value.kind_of?(String) or value.kind_of?(Symbol)
77
101
  if ((StringPattern.optimistic and value.kind_of?(String)) or value.kind_of?(Symbol)) and value.to_s.scan(/^!?\d+-?\d*:.+/).size > 0
78
- hashv[key] = StringPattern.generate(value, expected_errors: expected_errors)
102
+ hashv[key] = StringPattern.generate(value, **sp_opts)
79
103
  elsif ((StringPattern.optimistic and value.kind_of?(String)) or value.kind_of?(Symbol)) and value.to_s.scan(/^([\w\s\-]+\|)+[\w\s\-]+$/).size > 0
80
- if expected_errors.include?(:min_length) or (expected_errors.include?(:length) and rand.round == 0)
104
+ if expected_errors.include?(:min_length) or (expected_errors.include?(:length) and rng.rand.round == 0)
81
105
  min = value.to_s.split("|").min { |a, b| a.size <=> b.size }
82
106
  hashv[key] = min[0..-2] unless min == ""
83
107
  end
@@ -91,20 +115,21 @@ class NiceHash
91
115
  if hashv.keys.include?(key)
92
116
  v = hashv[key]
93
117
  else
94
- v = value.to_s.split("|").sample
118
+ v = value.to_s.split("|").sample(random: rng)
95
119
  end
96
120
  unless expected_errors.include?(:string_set_not_allowed)
97
- v = StringPattern.generate(:"#{v.size}:[#{value.to_s.split("|").join.split(//).uniq.join}]")
121
+ v = StringPattern.generate(:"#{v.size}:[#{value.to_s.split("|").join.split(//).uniq.join}]", **sp_opts)
98
122
  hashv[key] = v unless value.to_s.split("|").include?(v)
99
123
  end
100
124
  unless hashv.keys.include?(key)
101
- one_wrong_letter = StringPattern.generate(:"1:LN$[%#{value.to_s.split("|").join.split(//).uniq.join}%]")
102
- v[rand(v.size)] = one_wrong_letter
125
+ # Need a valid 1-char not in the allowed set (do not pass expected_errors)
126
+ one_wrong_letter = StringPattern.generate(:"1:LN$[%#{value.to_s.split("|").join.split(//).uniq.join}%]", _rng: rng)
127
+ v[rng.rand(v.size)] = one_wrong_letter
103
128
  hashv[key] = v unless value.to_s.split("|").include?(v)
104
129
  end
105
130
  end
106
131
  unless hashv.keys.include?(key)
107
- hashv[key] = value.to_s.split("|").sample
132
+ hashv[key] = value.to_s.split("|").sample(random: rng)
108
133
  end
109
134
  else
110
135
  hashv[key] = value
@@ -115,15 +140,15 @@ class NiceHash
115
140
  v = value[0]
116
141
  if (v.kind_of?(String) or v.kind_of?(Symbol)) and StringPattern.analyze(v, silent: true).kind_of?(StringPattern::Pattern)
117
142
  hashv[key] = []
118
- (rand(5)+1).times do
119
- hashv[key]<<StringPattern.generate(v, expected_errors: expected_errors)
143
+ (rng.rand(5)+1).times do
144
+ hashv[key]<<StringPattern.generate(v, **sp_opts)
120
145
  end
121
146
  array_pattern = true
122
147
  end
123
148
  else
124
149
  value.each { |v|
125
150
  if (v.kind_of?(String) or v.kind_of?(Symbol)) and StringPattern.analyze(v, silent: true).kind_of?(StringPattern::Pattern)
126
- hashv[key] = StringPattern.generate(value, expected_errors: expected_errors)
151
+ hashv[key] = StringPattern.generate(value, **sp_opts)
127
152
  array_pattern = true
128
153
  break
129
154
  end
@@ -133,9 +158,9 @@ class NiceHash
133
158
  value_ret = Array.new
134
159
  value.each { |v|
135
160
  if v.is_a?(Hash)
136
- ret = NiceHash.generate(v, select_hash_key, expected_errors: expected_errors)
161
+ ret = NiceHash.generate(v, select_hash_key, expected_errors: expected_errors, _rng: rng, **synonyms)
137
162
  else
138
- ret = NiceHash.generate({ doit: v }, select_hash_key, expected_errors: expected_errors)
163
+ ret = NiceHash.generate({ doit: v }, select_hash_key, expected_errors: expected_errors, _rng: rng, **synonyms)
139
164
  ret = ret[:doit] if ret.is_a?(Hash) and ret.key?(:doit)
140
165
  end
141
166
  ret = v if ret.kind_of?(Hash) and ret.size == 0
@@ -148,28 +173,28 @@ class NiceHash
148
173
  if value.size == Float::INFINITY
149
174
  value = (value.min..2**64)
150
175
  end
151
- hashv[key] = rand(value)
176
+ hashv[key] = rng.rand(value)
152
177
  else
153
178
  if value.size == Float::INFINITY
154
179
  infinite = true
155
180
  value = (value.min..2**64)
156
181
  else
157
182
  infinite = false
158
- hashv[key] = rand(value)
183
+ hashv[key] = rng.rand(value)
159
184
  end
160
185
  expected_errors.each do |er|
161
186
  if er == :min_length
162
- hashv[key] = rand((value.first - value.last)..value.first - 1)
187
+ hashv[key] = rng.rand((value.first - value.last)..value.first - 1)
163
188
  elsif er == :max_length and !infinite
164
- hashv[key] = rand((value.last + 1)..(value.last * 2))
189
+ hashv[key] = rng.rand((value.last + 1)..(value.last * 2))
165
190
  elsif er == :length
166
- if rand.round == 1 or infinite
167
- hashv[key] = rand((value.first - value.last)..value.first - 1)
191
+ if rng.rand.round == 1 or infinite
192
+ hashv[key] = rng.rand((value.first - value.last)..value.first - 1)
168
193
  else
169
- hashv[key] = rand((value.last + 1)..(value.last * 2))
194
+ hashv[key] = rng.rand((value.last + 1)..(value.last * 2))
170
195
  end
171
196
  elsif er == :value
172
- hashv[key] = :"1-10:N/L/".gen
197
+ hashv[key] = StringPattern.generate(:"1-10:N/L/", _rng: rng)
173
198
  end
174
199
  end
175
200
  end
@@ -184,31 +209,31 @@ class NiceHash
184
209
  elsif er == :max_length
185
210
  hashv[key] = hashv[key] + "Z"
186
211
  elsif er == :length
187
- if rand.round == 1
212
+ if rng.rand.round == 1
188
213
  hashv[key] = hashv[key].chop
189
214
  else
190
215
  hashv[key] = hashv[key] + "Z"
191
216
  end
192
217
  elsif er == :value
193
- hashv[key][rand(hashv[key].size - 1)] = "1:L".gen
218
+ hashv[key][rng.rand(hashv[key].size - 1)] = StringPattern.generate("1:L", _rng: rng)
194
219
  end
195
220
  end
196
221
  end
197
222
  elsif value.kind_of?(Module) and value == Boolean
198
223
  if expected_errors.empty?
199
- hashv[key] = (rand.round == 0)
224
+ hashv[key] = (rng.rand.round == 0)
200
225
  else
201
- hashv[key] = (rand.round == 0)
226
+ hashv[key] = (rng.rand.round == 0)
202
227
  expected_errors.each do |er|
203
228
  if er == :value
204
- hashv[key] = "1-10:L".gen
229
+ hashv[key] = StringPattern.generate("1-10:L", _rng: rng)
205
230
  end
206
231
  end
207
232
  end
208
233
  elsif value.kind_of?(Proc)
209
234
  hashv[key] = value.call
210
235
  elsif value.kind_of?(Regexp)
211
- hashv[key] = value.generate(expected_errors: expected_errors)
236
+ hashv[key] = value.generate(**sp_opts)
212
237
  else
213
238
  hashv[key] = value
214
239
  end
@@ -225,5 +250,23 @@ class NiceHash
225
250
 
226
251
  return hashv
227
252
  end
228
-
229
- end
253
+
254
+ ###########################################################################
255
+ # Generates n different hashes from the same pattern (useful for bulk or boundary tests).
256
+ # input:
257
+ # pattern_hash: (Hash) Hash we want to use to generate the values
258
+ # n: (Integer) Number of hashes to generate
259
+ # select_hash_key: (key value) (optional) The key we want to select on the subhashes
260
+ # expected_errors: (Array) (optional) (alias: errors) Same as in generate
261
+ # output: (Array of Hash)
262
+ # examples:
263
+ # hashes = NiceHash.generate_n(my_hash, 5, :correct)
264
+ # hashes = my_hash.generate_n(5, :correct)
265
+ ###########################################################################
266
+ def NiceHash.generate_n(pattern_hash, n, select_hash_key = nil, expected_errors: [], **synonyms)
267
+ rng = generation_rng(synonyms)
268
+ n.times.map { NiceHash.generate(pattern_hash, select_hash_key, expected_errors: expected_errors, _rng: rng, **synonyms) }
269
+ end
270
+
271
+ private_class_method :generation_rng
272
+ end
@@ -28,27 +28,19 @@ class NiceHash
28
28
  if keys.size == 1
29
29
  hash[nested_key.to_sym] = value unless only_if_exist and !hash.key?(nested_key.to_sym)
30
30
  else
31
- exist = true
32
- if only_if_exist
33
- ht = hash.deep_copy
34
- keys.each do |k|
35
- unless ht.key?(k.to_sym)
36
- exist = false
37
- break
38
- end
39
- ht = ht[k.to_sym]
40
- end
31
+ parent = hash
32
+ keys[0..-2].each do |k|
33
+ sym = k.to_sym
34
+ return hash unless parent.is_a?(Hash) && parent.key?(sym)
35
+ parent = parent[sym]
41
36
  end
42
- if !only_if_exist or (only_if_exist and exist)
43
- if value.is_a?(String)
44
- eval("hash.#{nested_key}='#{value}'")
45
- else
46
- #todo: consider other kind of objects apart of strings
47
- eval("hash.#{nested_key}=#{value}")
48
- end
37
+ return hash unless parent.is_a?(Hash)
38
+ if only_if_exist
39
+ return hash unless parent.key?(keys[-1].to_sym)
49
40
  end
41
+ parent[keys[-1].to_sym] = value
50
42
  end
51
43
  return hash
52
44
  end
53
-
54
- end
45
+
46
+ end
@@ -59,7 +59,7 @@ class NiceHash
59
59
  end
60
60
  values = values_hash_to_validate
61
61
  if pattern_hash.keys.size == get_all_keys(pattern_hash).size and values.keys.size != get_all_keys(values) and
62
- pattern_hash.keys.size == pattern_hash.keys.flatten.size # dont't set_values for the case of same_value
62
+ pattern_hash.keys.size == pattern_hash.keys.flatten.size # dont't set_values for the case of same_value
63
63
  # all patterns on patterns_hash are described on first level, so no same structure than values
64
64
  pattern_hash = values.set_values(pattern_hash)
65
65
  end
@@ -83,7 +83,15 @@ class NiceHash
83
83
  end
84
84
 
85
85
  if values.keys.include?(key)
86
- if value.kind_of?(String) or value.kind_of?(Symbol)
86
+ if value == :uuid
87
+ results[key] = false unless StringPattern.valid_uuid?(values[key].to_s)
88
+ elsif value == :email
89
+ 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)
94
+ elsif value.kind_of?(String) or value.kind_of?(Symbol)
87
95
  if ((StringPattern.optimistic and value.kind_of?(String)) or value.kind_of?(Symbol)) and value.to_s.scan(/^!?\d+-?\d*:.+/).size > 0
88
96
  res = StringPattern.validate(pattern: value, text: values[key])
89
97
  results[key] = res if res.size > 0
@@ -117,10 +125,8 @@ class NiceHash
117
125
  elsif value.kind_of?(Module) and value == Boolean
118
126
  results[key] = false unless values[key].is_a?(Boolean)
119
127
  elsif value.kind_of?(Regexp)
120
- rex = Regexp.new("^#{value}$")
121
- unless values[key].to_s.match?(rex)
122
- results[key] = false
123
- 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?)
124
130
  elsif value.kind_of?(Array)
125
131
  array_pattern = false
126
132
  complex_data = false
@@ -133,7 +139,7 @@ class NiceHash
133
139
  results[key] << res
134
140
  end
135
141
  if results[key].flatten.size == 0
136
- results.delete(key)
142
+ results.delete(key)
137
143
  end
138
144
  else
139
145
  res = StringPattern.validate(pattern: value, text: values[key])
@@ -213,5 +219,5 @@ class NiceHash
213
219
  return results
214
220
  end
215
221
 
216
-
217
- end
222
+
223
+ end
data/lib/nice_hash.rb CHANGED
@@ -5,6 +5,8 @@ require_relative "nice/hash/add_to_ruby" if SP_ADD_TO_RUBY
5
5
  require_relative "nice/hash/change_one_by_one"
6
6
  require_relative "nice/hash/compare_structure"
7
7
  require_relative "nice/hash/delete_nested"
8
+ require_relative "nice/hash/diff"
9
+ require_relative "nice/hash/flatten_keys"
8
10
  require_relative "nice/hash/generate"
9
11
  require_relative "nice/hash/get_all_keys"
10
12
  require_relative "nice/hash/get_values"
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nice_hash
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.18.7
4
+ version: 1.20.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mario Ruiz
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-03-19 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: string_pattern
@@ -16,20 +15,20 @@ dependencies:
16
15
  requirements:
17
16
  - - "~>"
18
17
  - !ruby/object:Gem::Version
19
- version: '2.3'
18
+ version: '2.5'
20
19
  - - ">="
21
20
  - !ruby/object:Gem::Version
22
- version: 2.3.0
21
+ version: 2.5.0
23
22
  type: :runtime
24
23
  prerelease: false
25
24
  version_requirements: !ruby/object:Gem::Requirement
26
25
  requirements:
27
26
  - - "~>"
28
27
  - !ruby/object:Gem::Version
29
- version: '2.3'
28
+ version: '2.5'
30
29
  - - ">="
31
30
  - !ruby/object:Gem::Version
32
- version: 2.3.0
31
+ version: 2.5.0
33
32
  - !ruby/object:Gem::Dependency
34
33
  name: rspec
35
34
  requirement: !ruby/object:Gem::Requirement
@@ -71,6 +70,8 @@ files:
71
70
  - lib/nice/hash/compare_structure.rb
72
71
  - lib/nice/hash/deep_clone.rb
73
72
  - lib/nice/hash/delete_nested.rb
73
+ - lib/nice/hash/diff.rb
74
+ - lib/nice/hash/flatten_keys.rb
74
75
  - lib/nice/hash/generate.rb
75
76
  - lib/nice/hash/get_all_keys.rb
76
77
  - lib/nice/hash/get_values.rb
@@ -95,15 +96,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
95
96
  requirements:
96
97
  - - ">="
97
98
  - !ruby/object:Gem::Version
98
- version: '0'
99
+ version: '3.0'
99
100
  required_rubygems_version: !ruby/object:Gem::Requirement
100
101
  requirements:
101
102
  - - ">="
102
103
  - !ruby/object:Gem::Version
103
104
  version: '0'
104
105
  requirements: []
105
- rubygems_version: 3.4.19
106
- signing_key:
106
+ rubygems_version: 4.0.6
107
107
  specification_version: 4
108
108
  summary: NiceHash creates hashes following certain patterns so your testing will be
109
109
  much easier. Parse and filter JSON. Perfect to be used in test data factories