string_pattern 2.4.0 → 2.5.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/CHANGELOG.md +48 -0
- data/README.md +46 -1
- data/lib/string/pattern/add_to_ruby.rb +7 -0
- data/lib/string/pattern/checksum.rb +106 -0
- data/lib/string/pattern/generate.rb +129 -138
- data/lib/string/pattern/validate.rb +100 -12
- data/lib/string_pattern/version.rb +5 -0
- data/lib/string_pattern.rb +84 -5
- metadata +8 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8769f150212e86d1c798377384ec17edccc22ba4c09d4b15d75bdac5ea1772b0
|
|
4
|
+
data.tar.gz: '0249dda55facee2d54e3a9229ef3bfb5e587eeac6adc72849c6a374fa15eb9dd'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a6247d3b5b23256e06bdd1a4079197f238b856fd8f492878bfca79373abd257cf7b5b6002a6bcf363fc21ce5402222fc7c666002c0c2bb05ae429d4f9b9ce8bf
|
|
7
|
+
data.tar.gz: e72ce0732bb53fcc97b96623d43b76d73b774c2c3033c9ef4143edb53935120d734d4cbb7677e3db26cb0504047c2827576a3dbf068894f47a7092d1c9e586c2
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
|
+
|
|
7
|
+
## [2.5.0] - 2026-09-23
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Added `StringPattern.with` to apply configuration for the duration of a block, and `StringPattern.reset!` to restore defaults and clear caches.
|
|
11
|
+
- Added `StringPattern::VERSION`.
|
|
12
|
+
- Added full-string `Regexp` validation via `StringPattern.validate`, `StringPattern.valid?`, and `Regexp#validate` / `#val`.
|
|
13
|
+
- Added `StringPattern.explain` with the failing segment index for pattern arrays.
|
|
14
|
+
- Added `StringPattern.iban` / `valid_iban?` (ISO 13616) and `StringPattern.luhn` / `valid_luhn?`.
|
|
15
|
+
- Added `block_list_regexp` to opt into the previous regular-expression block list.
|
|
16
|
+
- `StringPattern.sample` forwards `seed:` and other generate options.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- Generation uses its own `Random` instance. `seed:` no longer calls `srand`, so it does not change `Kernel#rand`. Sequences for a given seed may differ from 2.4.0.
|
|
20
|
+
- `block_list` matches literals (case-insensitive) unless `block_list_regexp` is true.
|
|
21
|
+
- Spanish word generation loads all `data/spanish/palabras*.json` files instead of one random file.
|
|
22
|
+
- Invalid patterns and impossible generation honor `raise_on_error` on every failure path.
|
|
23
|
+
- Required Ruby version is 3.0 or newer.
|
|
24
|
+
|
|
25
|
+
## [2.4.0] - 2026-02-11
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
- Added `StringPattern.valid_email?` helper and centralized email format checks.
|
|
29
|
+
- Added `StringPattern.valid?(text:, pattern:)` for boolean validation.
|
|
30
|
+
- Added `StringPattern.sample(pattern, n)` for batch generation of distinct values.
|
|
31
|
+
- Added `StringPattern.uuid` and `StringPattern.valid_uuid?`.
|
|
32
|
+
- Added support for `seed:` in generation for reproducible outputs.
|
|
33
|
+
- Added support for `block_list` as a `Proc`.
|
|
34
|
+
- Added `StringPattern::InvalidPatternError` and `StringPattern::GenerationImpossibleError`.
|
|
35
|
+
- Added `StringPattern.logger` and `StringPattern.raise_on_error` configuration.
|
|
36
|
+
- Added `spec/string/pattern/analyze_spec.rb`.
|
|
37
|
+
- Added GitHub Actions CI workflow.
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
- Fixed email-domain comparison bug in email validation/generation paths.
|
|
41
|
+
- Replaced internal direct `puts` calls with centralized logging (`StringPattern.log_message`).
|
|
42
|
+
- Updated README with analyze/validation/error-handling/new-features documentation.
|
|
43
|
+
- Updated CI target Ruby versions (3.0, 3.1, 3.2, 3.3).
|
|
44
|
+
|
|
45
|
+
## [2.3.0] - 2025-xx-xx
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
- Previous release notes not yet backfilled.
|
data/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# StringPattern
|
|
2
2
|
|
|
3
3
|
[](https://rubygems.org/gems/string_pattern)
|
|
4
|
-
[](https://github.com/MarioRuiz/string_pattern/actions/workflows/ci.yml)
|
|
5
5
|
[](https://coveralls.io/github/MarioRuiz/string_pattern?branch=master)
|
|
6
6
|

|
|
7
7
|

|
|
@@ -503,6 +503,51 @@ StringPattern.block_list = ->(s) { s.include?("forbidden") }
|
|
|
503
503
|
StringPattern.block_list_enabled = true
|
|
504
504
|
```
|
|
505
505
|
|
|
506
|
+
Entries in `block_list` are matched as case-insensitive literals. Set `StringPattern.block_list_regexp = true` when an entry should be a regular expression, which was the behavior before 2.5.0.
|
|
507
|
+
|
|
508
|
+
#### Scoped configuration
|
|
509
|
+
|
|
510
|
+
`StringPattern.with` applies settings for one block and restores the previous values afterwards, including when the block raises:
|
|
511
|
+
|
|
512
|
+
```ruby
|
|
513
|
+
StringPattern.with(dont_repeat: true, raise_on_error: true) do
|
|
514
|
+
"6:N".gen
|
|
515
|
+
end
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
`StringPattern.reset!` restores the default configuration and clears the pattern cache and the generated-value cache.
|
|
519
|
+
|
|
520
|
+
#### Validate a regular expression
|
|
521
|
+
|
|
522
|
+
`valid?` and `validate` accept a `Regexp` and match the whole string. The same methods are available on the regexp itself:
|
|
523
|
+
|
|
524
|
+
```ruby
|
|
525
|
+
StringPattern.valid?(text: "abc", pattern: /[a-z]+/) # => true
|
|
526
|
+
StringPattern.valid?(text: "abc1", pattern: /[a-z]+/) # => false
|
|
527
|
+
/[a-z]+/.validate("abc") # => []
|
|
528
|
+
/[a-z]+/.val("abc1") # => [:value]
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
#### Explain a validation
|
|
532
|
+
|
|
533
|
+
```ruby
|
|
534
|
+
StringPattern.explain(text: "ab", pattern: "6:N")
|
|
535
|
+
# => { valid: false, errors: [:min_length, :length, :value, :string_set_not_allowed], message: "invalid: ...", index: nil }
|
|
536
|
+
|
|
537
|
+
StringPattern.explain(text: "333111", pattern: ["3:n", "3:x"])
|
|
538
|
+
# => { valid: false, errors: [...], message: "segment 1 does not match: ...", index: 1 }
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
#### IBAN and Luhn
|
|
542
|
+
|
|
543
|
+
International checksums for test data. `iban` requires a country code and only generates countries whose length is known (ES, DE, FR, GB, PT, IT, NL, BE). The BBAN is random. `valid_iban?` accepts any well-formed IBAN. `luhn` builds a digit string with a valid Luhn check digit and does not represent a real card.
|
|
544
|
+
|
|
545
|
+
```ruby
|
|
546
|
+
StringPattern.iban(country: "DE") # => "DE..."
|
|
547
|
+
StringPattern.valid_iban?("DE89 3704 0044 0532 0130 00") # => true
|
|
548
|
+
StringPattern.luhn(length: 16) # => "16 digits"
|
|
549
|
+
StringPattern.valid_luhn?("79927398713") # => true
|
|
550
|
+
```
|
|
506
551
|
|
|
507
552
|
## Contributing
|
|
508
553
|
|
|
@@ -78,6 +78,13 @@ class Regexp
|
|
|
78
78
|
|
|
79
79
|
alias gen generate
|
|
80
80
|
|
|
81
|
+
# Validates +string_to_validate+ against this regular expression as a full-string match.
|
|
82
|
+
def validate(string_to_validate, expected_errors: [], not_expected_errors: [], **synonyms)
|
|
83
|
+
StringPattern.validate(text: string_to_validate, pattern: self, expected_errors: expected_errors, not_expected_errors: not_expected_errors, **synonyms)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
alias val validate
|
|
87
|
+
|
|
81
88
|
# adds method to convert a Regexp to StringPattern
|
|
82
89
|
# returns an array of string patterns or just one string pattern
|
|
83
90
|
def to_sp
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class StringPattern
|
|
4
|
+
# IBAN lengths we can generate. Validation accepts any well-formed IBAN.
|
|
5
|
+
IBAN_LENGTHS = {
|
|
6
|
+
"ES" => 24,
|
|
7
|
+
"DE" => 22,
|
|
8
|
+
"FR" => 27,
|
|
9
|
+
"GB" => 22,
|
|
10
|
+
"PT" => 25,
|
|
11
|
+
"IT" => 27,
|
|
12
|
+
"NL" => 18,
|
|
13
|
+
"BE" => 16
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
# Generates a random IBAN for +country+ (ISO 13616). The BBAN is random, not a real bank account.
|
|
17
|
+
# @param country [String, Symbol] two-letter country code
|
|
18
|
+
# @return [String]
|
|
19
|
+
# @raise [ArgumentError] when the country is not in {IBAN_LENGTHS}
|
|
20
|
+
def self.iban(country:)
|
|
21
|
+
code = country.to_s.upcase
|
|
22
|
+
length = IBAN_LENGTHS[code]
|
|
23
|
+
raise ArgumentError, "unknown IBAN country: #{country.inspect}" unless length
|
|
24
|
+
|
|
25
|
+
rng = Random.new
|
|
26
|
+
alphabet = [*("0".."9"), *("A".."Z")]
|
|
27
|
+
bban = Array.new(length - 4) { alphabet.sample(random: rng) }.join
|
|
28
|
+
"#{code}#{iban_check_digits(code, bban)}#{bban}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Returns true when +str+ is a well-formed IBAN (format and mod-97), ignoring spaces.
|
|
32
|
+
# @param str [String]
|
|
33
|
+
# @return [Boolean]
|
|
34
|
+
def self.valid_iban?(str)
|
|
35
|
+
return false unless str.is_a?(String)
|
|
36
|
+
|
|
37
|
+
compact = str.gsub(/\s+/, "").upcase
|
|
38
|
+
return false unless compact.match?(/\A[A-Z]{2}\d{2}[A-Z0-9]+\z/)
|
|
39
|
+
return false unless (15..34).cover?(compact.length)
|
|
40
|
+
|
|
41
|
+
rearranged = compact[4..] + compact[0, 4]
|
|
42
|
+
iban_mod97(iban_to_digits(rearranged)) == 1
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Generates a digit string of +length+ that satisfies the Luhn checksum.
|
|
46
|
+
# @param length [Integer] total number of digits, at least 2
|
|
47
|
+
# @return [String]
|
|
48
|
+
# @raise [ArgumentError] when length is below 2
|
|
49
|
+
def self.luhn(length:)
|
|
50
|
+
length = Integer(length)
|
|
51
|
+
raise ArgumentError, "length must be >= 2" if length < 2
|
|
52
|
+
|
|
53
|
+
rng = Random.new
|
|
54
|
+
digits = Array.new(length - 1) { rng.rand(10) }
|
|
55
|
+
digits << luhn_check_digit(digits)
|
|
56
|
+
digits.join
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Returns true when +str+ is a digit string with a valid Luhn checksum.
|
|
60
|
+
# @param str [String]
|
|
61
|
+
# @return [Boolean]
|
|
62
|
+
def self.valid_luhn?(str)
|
|
63
|
+
return false unless str.is_a?(String) && str.match?(/\A\d{2,}\z/)
|
|
64
|
+
|
|
65
|
+
luhn_sum(str.chars.map(&:to_i)) % 10 == 0
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.iban_check_digits(country, bban)
|
|
69
|
+
remainder = iban_mod97(iban_to_digits("#{bban}#{country}00"))
|
|
70
|
+
format("%02d", 98 - remainder)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def self.iban_to_digits(value)
|
|
74
|
+
value.each_char.map { |char|
|
|
75
|
+
if char >= "A" && char <= "Z"
|
|
76
|
+
(char.ord - 55).to_s
|
|
77
|
+
else
|
|
78
|
+
char
|
|
79
|
+
end
|
|
80
|
+
}.join
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def self.iban_mod97(digit_string)
|
|
84
|
+
remainder = 0
|
|
85
|
+
digit_string.scan(/.{1,9}/).each do |chunk|
|
|
86
|
+
remainder = "#{remainder}#{chunk}".to_i % 97
|
|
87
|
+
end
|
|
88
|
+
remainder
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def self.luhn_check_digit(payload)
|
|
92
|
+
(10 - (luhn_sum(payload + [0]) % 10)) % 10
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def self.luhn_sum(digits)
|
|
96
|
+
digits.reverse.each_with_index.sum do |digit, index|
|
|
97
|
+
value = digit.to_i
|
|
98
|
+
if index.odd?
|
|
99
|
+
value *= 2
|
|
100
|
+
value -= 9 if value > 9
|
|
101
|
+
end
|
|
102
|
+
value
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
private_class_method :iban_check_digits, :iban_to_digits, :iban_mod97, :luhn_check_digit, :luhn_sum
|
|
106
|
+
end
|
|
@@ -64,8 +64,11 @@ class StringPattern
|
|
|
64
64
|
# the generated string
|
|
65
65
|
###############################################
|
|
66
66
|
def StringPattern.generate(pattern, expected_errors: [], **synonyms)
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
rng = synonyms.delete(:_rng)
|
|
68
|
+
if rng.nil?
|
|
69
|
+
base = synonyms.key?(:seed) ? Random.new(synonyms[:seed]) : Random.new
|
|
70
|
+
rng = StringPattern::RandomSource.new(base)
|
|
71
|
+
end
|
|
69
72
|
tries = 0
|
|
70
73
|
begin
|
|
71
74
|
good_result = true
|
|
@@ -81,30 +84,32 @@ class StringPattern
|
|
|
81
84
|
if pattern.kind_of?(Array)
|
|
82
85
|
pattern.each { |pat|
|
|
83
86
|
if pat.kind_of?(Array) # for the case it is one of the values
|
|
84
|
-
pat = pat.sample
|
|
87
|
+
pat = pat.sample(random: rng)
|
|
85
88
|
end
|
|
86
89
|
|
|
87
90
|
if pat.kind_of?(Symbol)
|
|
88
91
|
if pat.to_s.scan(/^!?\d+-?\d*:.+/).size > 0
|
|
89
|
-
string << StringPattern.generate(pat.to_s, expected_errors: expected_errors)
|
|
92
|
+
string << StringPattern.generate(pat.to_s, expected_errors: expected_errors, _rng: rng)
|
|
90
93
|
else
|
|
91
94
|
string << pat.to_s
|
|
92
95
|
end
|
|
93
96
|
elsif pat.kind_of?(String)
|
|
94
97
|
if @optimistic and pat.to_s.scan(/^!?\d+-?\d*:.+/).size > 0
|
|
95
|
-
string << StringPattern.generate(pat.to_s, expected_errors: expected_errors)
|
|
98
|
+
string << StringPattern.generate(pat.to_s, expected_errors: expected_errors, _rng: rng)
|
|
96
99
|
else
|
|
97
100
|
string << pat
|
|
98
101
|
end
|
|
99
102
|
else
|
|
100
|
-
StringPattern.
|
|
101
|
-
return ""
|
|
103
|
+
return StringPattern.fail_generation("StringPattern.generate: it seems you supplied wrong array of patterns: #{pattern.inspect}, expected_errors: #{expected_errors.inspect}", StringPattern::InvalidPatternError)
|
|
102
104
|
end
|
|
103
105
|
}
|
|
104
106
|
return string
|
|
105
107
|
elsif pattern.kind_of?(String) or pattern.kind_of?(Symbol)
|
|
106
108
|
patt = StringPattern.analyze(pattern).clone
|
|
107
|
-
|
|
109
|
+
unless patt.kind_of?(Struct)
|
|
110
|
+
raise StringPattern::InvalidPatternError, "pattern argument not valid on StringPattern.generate: #{pattern.inspect}" if @raise_on_error
|
|
111
|
+
return ""
|
|
112
|
+
end
|
|
108
113
|
|
|
109
114
|
min_length = patt.min_length.clone
|
|
110
115
|
max_length = patt.max_length.clone
|
|
@@ -122,17 +127,18 @@ class StringPattern
|
|
|
122
127
|
}
|
|
123
128
|
unless excluded_data.size == 0
|
|
124
129
|
if (required_chars.flatten & excluded_data.flatten).size > 0
|
|
125
|
-
StringPattern.
|
|
126
|
-
return ""
|
|
130
|
+
return StringPattern.fail_generation("pattern argument not valid on StringPattern.generate, a character cannot be required and excluded at the same time: #{pattern.inspect}, expected_errors: #{expected_errors.inspect}", StringPattern::InvalidPatternError)
|
|
127
131
|
end
|
|
128
132
|
end
|
|
129
133
|
end
|
|
130
134
|
|
|
131
135
|
string_set_not_allowed = Array.new
|
|
132
136
|
elsif pattern.kind_of?(Regexp)
|
|
133
|
-
return generate(pattern.to_sp, expected_errors: expected_errors)
|
|
137
|
+
return generate(pattern.to_sp, expected_errors: expected_errors, _rng: rng)
|
|
134
138
|
else
|
|
135
|
-
|
|
139
|
+
message = "pattern argument not valid on StringPattern.generate: #{pattern.inspect}, expected_errors: #{expected_errors.inspect}"
|
|
140
|
+
raise StringPattern::InvalidPatternError, message if @raise_on_error
|
|
141
|
+
StringPattern.log_message(message)
|
|
136
142
|
return pattern.to_s
|
|
137
143
|
end
|
|
138
144
|
|
|
@@ -141,8 +147,8 @@ class StringPattern
|
|
|
141
147
|
if symbol_type[0..0] == "!"
|
|
142
148
|
deny_pattern = true
|
|
143
149
|
possible_errors = [:length, :value, :string_set_not_allowed]
|
|
144
|
-
(rand(possible_errors.size) + 1).times {
|
|
145
|
-
expected_errors << possible_errors.sample
|
|
150
|
+
(rng.rand(possible_errors.size) + 1).times {
|
|
151
|
+
expected_errors << possible_errors.sample(random: rng)
|
|
146
152
|
}
|
|
147
153
|
expected_errors.uniq!
|
|
148
154
|
if symbol_type[1..1] == "0"
|
|
@@ -171,21 +177,18 @@ class StringPattern
|
|
|
171
177
|
|
|
172
178
|
unless deny_pattern
|
|
173
179
|
if required_data.size == 0 and expected_errors_left.include?(:required_data)
|
|
174
|
-
StringPattern.
|
|
175
|
-
return ""
|
|
180
|
+
return StringPattern.fail_generation("required data not supplied on pattern so it won't be possible to generate a wrong string. StringPattern.generate: #{pattern.inspect}, expected_errors: #{expected_errors.inspect}", StringPattern::GenerationImpossibleError)
|
|
176
181
|
end
|
|
177
182
|
|
|
178
183
|
if excluded_data.size == 0 and expected_errors_left.include?(:excluded_data)
|
|
179
|
-
StringPattern.
|
|
180
|
-
return ""
|
|
184
|
+
return StringPattern.fail_generation("excluded data not supplied on pattern so it won't be possible to generate a wrong string. StringPattern.generate: #{pattern.inspect}, expected_errors: #{expected_errors.inspect}", StringPattern::GenerationImpossibleError)
|
|
181
185
|
end
|
|
182
186
|
|
|
183
187
|
if expected_errors_left.include?(:string_set_not_allowed)
|
|
184
188
|
string_set_not_allowed = all_characters_set - string_set
|
|
185
189
|
|
|
186
190
|
if string_set_not_allowed.size == 0
|
|
187
|
-
StringPattern.
|
|
188
|
-
return ""
|
|
191
|
+
return StringPattern.fail_generation("all characters are allowed so it won't be possible to generate a wrong string. StringPattern.generate: #{pattern.inspect}, expected_errors: #{expected_errors.inspect}", StringPattern::GenerationImpossibleError)
|
|
189
192
|
end
|
|
190
193
|
end
|
|
191
194
|
end
|
|
@@ -194,12 +197,12 @@ class StringPattern
|
|
|
194
197
|
expected_errors_left.include?(:max_length) or
|
|
195
198
|
expected_errors_left.include?(:length)
|
|
196
199
|
if expected_errors_left.include?(:min_length) or
|
|
197
|
-
(min_length > 0 and expected_errors_left.include?(:length) and rand(2) == 0)
|
|
200
|
+
(min_length > 0 and expected_errors_left.include?(:length) and rng.rand(2) == 0)
|
|
198
201
|
if min_length > 0
|
|
199
202
|
if allow_empty
|
|
200
|
-
length = rand(min_length).to_i
|
|
203
|
+
length = rng.rand(min_length).to_i
|
|
201
204
|
else
|
|
202
|
-
length = rand(min_length - 1).to_i + 1
|
|
205
|
+
length = rng.rand(min_length - 1).to_i + 1
|
|
203
206
|
end
|
|
204
207
|
if required_data.size > length and required_data.size < min_length
|
|
205
208
|
length = required_data.size
|
|
@@ -207,22 +210,21 @@ class StringPattern
|
|
|
207
210
|
expected_errors_left.delete(:length)
|
|
208
211
|
expected_errors_left.delete(:min_length)
|
|
209
212
|
else
|
|
210
|
-
StringPattern.
|
|
211
|
-
return ""
|
|
213
|
+
return StringPattern.fail_generation("min_length is 0 so it won't be possible to generate a wrong string smaller than 0 characters. StringPattern.generate: #{pattern.inspect}, expected_errors: #{expected_errors.inspect}", StringPattern::GenerationImpossibleError)
|
|
212
214
|
end
|
|
213
215
|
elsif expected_errors_left.include?(:max_length) or expected_errors_left.include?(:length)
|
|
214
|
-
length = max_length + 1 + rand(max_length).to_i
|
|
216
|
+
length = max_length + 1 + rng.rand(max_length).to_i
|
|
215
217
|
expected_errors_left.delete(:length)
|
|
216
218
|
expected_errors_left.delete(:max_length)
|
|
217
219
|
end
|
|
218
220
|
else
|
|
219
|
-
if allow_empty and rand(7) == 1
|
|
221
|
+
if allow_empty and rng.rand(7) == 1
|
|
220
222
|
length = 0
|
|
221
223
|
else
|
|
222
224
|
if max_length == min_length
|
|
223
225
|
length = min_length
|
|
224
226
|
else
|
|
225
|
-
length = min_length + rand(max_length - min_length + 1)
|
|
227
|
+
length = min_length + rng.rand(max_length - min_length + 1)
|
|
226
228
|
end
|
|
227
229
|
end
|
|
228
230
|
end
|
|
@@ -253,28 +255,27 @@ class StringPattern
|
|
|
253
255
|
if symbol_type != "@" and symbol_type != "!@" and length != 0 and string_set.size != 0
|
|
254
256
|
if string_set.size != 0
|
|
255
257
|
1.upto(length) { |i|
|
|
256
|
-
string << string_set.sample.to_s
|
|
258
|
+
string << string_set.sample(random: rng).to_s
|
|
257
259
|
}
|
|
258
260
|
end
|
|
259
261
|
if required_data.size > 0
|
|
260
262
|
positions_to_set = (0..(string.size - 1)).to_a
|
|
261
263
|
required_data.each { |rd|
|
|
262
264
|
if (string.chars & rd).size > 0
|
|
263
|
-
rd_to_set = (string.chars & rd).sample
|
|
265
|
+
rd_to_set = (string.chars & rd).sample(random: rng)
|
|
264
266
|
else
|
|
265
|
-
rd_to_set = rd.sample
|
|
267
|
+
rd_to_set = rd.sample(random: rng)
|
|
266
268
|
end
|
|
267
269
|
if ((0...string.length).find_all { |i| string[i, 1] == rd_to_set }).size == 0
|
|
268
270
|
if positions_to_set.size == 0
|
|
269
|
-
StringPattern.
|
|
270
|
-
return ""
|
|
271
|
+
return StringPattern.fail_generation("pattern not valid on StringPattern.generate, not possible to generate a valid string: #{pattern.inspect}, expected_errors: #{expected_errors.inspect}", StringPattern::GenerationImpossibleError)
|
|
271
272
|
else
|
|
272
|
-
k = positions_to_set.sample
|
|
273
|
+
k = positions_to_set.sample(random: rng)
|
|
273
274
|
string[k] = rd_to_set
|
|
274
275
|
positions_to_set.delete(k)
|
|
275
276
|
end
|
|
276
277
|
else
|
|
277
|
-
k = ((0...string.length).find_all { |i| string[i, 1] == rd_to_set }).sample
|
|
278
|
+
k = ((0...string.length).find_all { |i| string[i, 1] == rd_to_set }).sample(random: rng)
|
|
278
279
|
positions_to_set.delete(k)
|
|
279
280
|
end
|
|
280
281
|
}
|
|
@@ -282,7 +283,7 @@ class StringPattern
|
|
|
282
283
|
excluded_data.each { |ed|
|
|
283
284
|
if (string.chars & ed).size > 0
|
|
284
285
|
(string.chars & ed).each { |s|
|
|
285
|
-
string.gsub!(s, string_set.sample)
|
|
286
|
+
string.gsub!(s, string_set.sample(random: rng))
|
|
286
287
|
}
|
|
287
288
|
end
|
|
288
289
|
}
|
|
@@ -291,28 +292,27 @@ class StringPattern
|
|
|
291
292
|
string_set_not_allowed = all_characters_set - string_set if string_set_not_allowed.size == 0
|
|
292
293
|
|
|
293
294
|
if string_set_not_allowed.size == 0
|
|
294
|
-
StringPattern.
|
|
295
|
-
return ""
|
|
295
|
+
return StringPattern.fail_generation("Not possible to generate a non valid string on StringPattern.generate: #{pattern.inspect}, expected_errors: #{expected_errors.inspect}", StringPattern::GenerationImpossibleError)
|
|
296
296
|
end
|
|
297
|
-
(rand(string.size) + 1).times {
|
|
298
|
-
string[rand(string.size)] = (all_characters_set - string_set).sample
|
|
297
|
+
(rng.rand(string.size) + 1).times {
|
|
298
|
+
string[rng.rand(string.size)] = (all_characters_set - string_set).sample(random: rng)
|
|
299
299
|
}
|
|
300
300
|
expected_errors_left.delete(:value)
|
|
301
301
|
end
|
|
302
302
|
|
|
303
303
|
if expected_errors_left.include?(:required_data) and required_data.size > 0
|
|
304
|
-
(rand(required_data.size) + 1).times {
|
|
305
|
-
chars_to_remove = required_data.sample
|
|
304
|
+
(rng.rand(required_data.size) + 1).times {
|
|
305
|
+
chars_to_remove = required_data.sample(random: rng)
|
|
306
306
|
chars_to_remove.each { |char_to_remove|
|
|
307
|
-
string.gsub!(char_to_remove, (string_set - chars_to_remove).sample)
|
|
307
|
+
string.gsub!(char_to_remove, (string_set - chars_to_remove).sample(random: rng))
|
|
308
308
|
}
|
|
309
309
|
}
|
|
310
310
|
expected_errors_left.delete(:required_data)
|
|
311
311
|
end
|
|
312
312
|
|
|
313
313
|
if expected_errors_left.include?(:excluded_data) and excluded_data.size > 0
|
|
314
|
-
(rand(string.size) + 1).times {
|
|
315
|
-
string[rand(string.size)] = excluded_data.sample.sample
|
|
314
|
+
(rng.rand(string.size) + 1).times {
|
|
315
|
+
string[rng.rand(string.size)] = excluded_data.sample(random: rng).sample(random: rng)
|
|
316
316
|
}
|
|
317
317
|
expected_errors_left.delete(:excluded_data)
|
|
318
318
|
end
|
|
@@ -320,67 +320,21 @@ class StringPattern
|
|
|
320
320
|
if expected_errors_left.include?(:string_set_not_allowed)
|
|
321
321
|
string_set_not_allowed = all_characters_set - string_set if string_set_not_allowed.size == 0
|
|
322
322
|
if string_set_not_allowed.size > 0
|
|
323
|
-
(rand(string.size) + 1).times {
|
|
324
|
-
string[rand(string.size)] = string_set_not_allowed.sample
|
|
323
|
+
(rng.rand(string.size) + 1).times {
|
|
324
|
+
string[rng.rand(string.size)] = string_set_not_allowed.sample(random: rng)
|
|
325
325
|
}
|
|
326
326
|
expected_errors_left.delete(:string_set_not_allowed)
|
|
327
327
|
end
|
|
328
328
|
end
|
|
329
329
|
elsif (symbol_type == "W" or symbol_type == "P" or symbol_type == "w" or symbol_type == "p") and length > 0
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
filename = File.join Pathname(File.dirname(__FILE__)), "../../../data", "english/nouns.json"
|
|
337
|
-
nouns = JSON.parse(File.read(filename))
|
|
338
|
-
filename = File.join Pathname(File.dirname(__FILE__)), "../../../data", "english/adjs.json"
|
|
339
|
-
adjs = JSON.parse(File.read(filename))
|
|
340
|
-
nouns = nouns.map(&:to_camel_case)
|
|
341
|
-
adjs = adjs.map(&:to_camel_case)
|
|
342
|
-
@words_camel = adjs + nouns
|
|
343
|
-
@words_camel_short = @words_camel.sample(2000)
|
|
344
|
-
end
|
|
345
|
-
words = @words_camel
|
|
346
|
-
words_short = @words_camel_short
|
|
347
|
-
elsif symbol_type == "w"
|
|
348
|
-
if @words.empty?
|
|
349
|
-
require "pathname"
|
|
350
|
-
require "json"
|
|
351
|
-
filename = File.join Pathname(File.dirname(__FILE__)), "../../../data", "english/nouns.json"
|
|
352
|
-
nouns = JSON.parse(File.read(filename))
|
|
353
|
-
filename = File.join Pathname(File.dirname(__FILE__)), "../../../data", "english/adjs.json"
|
|
354
|
-
adjs = JSON.parse(File.read(filename))
|
|
355
|
-
@words = adjs + nouns
|
|
356
|
-
@words_short = @words.sample(2000)
|
|
357
|
-
end
|
|
358
|
-
words = @words
|
|
359
|
-
words_short = @words_short
|
|
360
|
-
elsif symbol_type == "P"
|
|
361
|
-
if @palabras_camel.empty?
|
|
362
|
-
require "pathname"
|
|
363
|
-
require "json"
|
|
364
|
-
filename = File.join Pathname(File.dirname(__FILE__)), "../../../data", "spanish/palabras#{rand(12)}.json"
|
|
365
|
-
palabras = JSON.parse(File.read(filename))
|
|
366
|
-
palabras = palabras.map(&:to_camel_case)
|
|
367
|
-
@palabras_camel = palabras
|
|
368
|
-
@palabras_camel_short = @palabras_camel.sample(2000)
|
|
369
|
-
end
|
|
370
|
-
words = @palabras_camel
|
|
371
|
-
words_short = @palabras_camel_short
|
|
372
|
-
elsif symbol_type == "p"
|
|
373
|
-
if @palabras.empty?
|
|
374
|
-
require "pathname"
|
|
375
|
-
require "json"
|
|
376
|
-
filename = File.join Pathname(File.dirname(__FILE__)), "../../../data", "spanish/palabras#{rand(12)}.json"
|
|
377
|
-
palabras = JSON.parse(File.read(filename))
|
|
378
|
-
@palabras = palabras
|
|
379
|
-
@palabras_short = @palabras.sample(2000)
|
|
380
|
-
end
|
|
381
|
-
words = @palabras
|
|
382
|
-
words_short = @palabras_short
|
|
330
|
+
if symbol_type == "W" || symbol_type == "w"
|
|
331
|
+
load_english_words(camel: symbol_type == "W")
|
|
332
|
+
words = symbol_type == "W" ? @words_camel : @words
|
|
333
|
+
else
|
|
334
|
+
load_spanish_words(camel: symbol_type == "P")
|
|
335
|
+
words = symbol_type == "P" ? @palabras_camel : @palabras
|
|
383
336
|
end
|
|
337
|
+
words_short = words.sample(2000, random: rng)
|
|
384
338
|
|
|
385
339
|
wordr = ""
|
|
386
340
|
wordr_array = []
|
|
@@ -394,16 +348,16 @@ class StringPattern
|
|
|
394
348
|
end
|
|
395
349
|
if symbol_type == "w" or symbol_type == "p"
|
|
396
350
|
length = length - 1 if wordr_array.size > 0
|
|
397
|
-
res = (words_short.select { |word| word.length <= length && word.length != length - 1 && word.length != length - 2 && word.length != length - 3 }).sample.to_s
|
|
351
|
+
res = (words_short.select { |word| word.length <= length && word.length != length - 1 && word.length != length - 2 && word.length != length - 3 }).sample(random: rng).to_s
|
|
398
352
|
unless res.to_s == ""
|
|
399
353
|
wordr_array << res
|
|
400
354
|
wordr = wordr_array.join(@word_separator)
|
|
401
355
|
end
|
|
402
356
|
else
|
|
403
|
-
wordr += (words_short.select { |word| word.length <= length && word.length != length - 1 && word.length != length - 2 && word.length != length - 3 }).sample.to_s
|
|
357
|
+
wordr += (words_short.select { |word| word.length <= length && word.length != length - 1 && word.length != length - 2 && word.length != length - 3 }).sample(random: rng).to_s
|
|
404
358
|
end
|
|
405
359
|
if (tries % 100) == 0
|
|
406
|
-
words_short = words.sample(2000)
|
|
360
|
+
words_short = words.sample(2000, random: rng)
|
|
407
361
|
end
|
|
408
362
|
end
|
|
409
363
|
good_result = true
|
|
@@ -436,60 +390,60 @@ class StringPattern
|
|
|
436
390
|
at_sign = "@"
|
|
437
391
|
|
|
438
392
|
if expected_errors_left.include?(:value)
|
|
439
|
-
if rand(2) == 1
|
|
440
|
-
extension = (all_characters_set - ["."]).sample.dup
|
|
393
|
+
if rng.rand(2) == 1
|
|
394
|
+
extension = (all_characters_set - ["."]).sample(random: rng).dup
|
|
441
395
|
expected_errors_left.delete(:value)
|
|
442
396
|
expected_errors_left.delete(:required_data)
|
|
443
397
|
end
|
|
444
398
|
|
|
445
|
-
if rand(2) == 1
|
|
446
|
-
1.upto(rand(7)) { |i|
|
|
447
|
-
extension << alpha_set.sample.downcase
|
|
399
|
+
if rng.rand(2) == 1
|
|
400
|
+
1.upto(rng.rand(7)) { |i|
|
|
401
|
+
extension << alpha_set.sample(random: rng).downcase
|
|
448
402
|
}
|
|
449
403
|
|
|
450
|
-
(rand(extension.size) + 1).times {
|
|
451
|
-
extension[rand(extension.size)] = (string_set - alpha_set - ["."]).sample
|
|
404
|
+
(rng.rand(extension.size) + 1).times {
|
|
405
|
+
extension[rng.rand(extension.size)] = (string_set - alpha_set - ["."]).sample(random: rng)
|
|
452
406
|
}
|
|
453
407
|
|
|
454
408
|
expected_errors_left.delete(:value)
|
|
455
409
|
else
|
|
456
|
-
1.upto(rand(3) + 2) { |i|
|
|
457
|
-
extension << alpha_set.sample.downcase
|
|
410
|
+
1.upto(rng.rand(3) + 2) { |i|
|
|
411
|
+
extension << alpha_set.sample(random: rng).downcase
|
|
458
412
|
}
|
|
459
413
|
end
|
|
460
414
|
|
|
461
|
-
if rand(2) == 1
|
|
462
|
-
at_sign = (string_set - ["@"]).sample.dup
|
|
415
|
+
if rng.rand(2) == 1
|
|
416
|
+
at_sign = (string_set - ["@"]).sample(random: rng).dup
|
|
463
417
|
expected_errors_left.delete(:value)
|
|
464
418
|
expected_errors_left.delete(:required_data)
|
|
465
419
|
end
|
|
466
420
|
else
|
|
467
421
|
if length > 6
|
|
468
|
-
1.upto(rand(3) + 2) { |i|
|
|
469
|
-
extension << alpha_set.sample.downcase
|
|
422
|
+
1.upto(rng.rand(3) + 2) { |i|
|
|
423
|
+
extension << alpha_set.sample(random: rng).downcase
|
|
470
424
|
}
|
|
471
425
|
else
|
|
472
426
|
1.upto(2) { |i|
|
|
473
|
-
extension << alpha_set.sample.downcase
|
|
427
|
+
extension << alpha_set.sample(random: rng).downcase
|
|
474
428
|
}
|
|
475
429
|
end
|
|
476
430
|
end
|
|
477
431
|
length_e = length - extension.size - 1
|
|
478
|
-
length1 = rand(length_e - 1) + 1
|
|
432
|
+
length1 = rng.rand(length_e - 1) + 1
|
|
479
433
|
length2 = length_e - length1
|
|
480
|
-
1.upto(length1) { |i| string << string_set.sample }
|
|
434
|
+
1.upto(length1) { |i| string << string_set.sample(random: rng) }
|
|
481
435
|
|
|
482
436
|
string << at_sign
|
|
483
437
|
|
|
484
438
|
domain = ""
|
|
485
439
|
domain_set = alpha_set + NUMBER_SET.clone + ["."] + ["-"]
|
|
486
440
|
1.upto(length2) { |i|
|
|
487
|
-
domain << domain_set.sample.downcase
|
|
441
|
+
domain << domain_set.sample(random: rng).downcase
|
|
488
442
|
}
|
|
489
443
|
|
|
490
|
-
if expected_errors.include?(:value) and rand(2) == 1 and domain.size > 0
|
|
491
|
-
(rand(domain.size) + 1).times {
|
|
492
|
-
domain[rand(domain.size)] = (all_characters_set - domain_set).sample
|
|
444
|
+
if expected_errors.include?(:value) and rng.rand(2) == 1 and domain.size > 0
|
|
445
|
+
(rng.rand(domain.size) + 1).times {
|
|
446
|
+
domain[rng.rand(domain.size)] = (all_characters_set - domain_set).sample(random: rng)
|
|
493
447
|
}
|
|
494
448
|
expected_errors_left.delete(:value)
|
|
495
449
|
end
|
|
@@ -497,8 +451,8 @@ class StringPattern
|
|
|
497
451
|
string << domain << extension
|
|
498
452
|
|
|
499
453
|
if expected_errors_left.include?(:value) or expected_errors_left.include?(:string_set_not_allowed)
|
|
500
|
-
(rand(string.size) + 1).times {
|
|
501
|
-
string[rand(string.size)] = string_set_not_allowed.sample
|
|
454
|
+
(rng.rand(string.size) + 1).times {
|
|
455
|
+
string[rng.rand(string.size)] = string_set_not_allowed.sample(random: rng)
|
|
502
456
|
}
|
|
503
457
|
expected_errors_left.delete(:value)
|
|
504
458
|
expected_errors_left.delete(:string_set_not_allowed)
|
|
@@ -529,10 +483,7 @@ class StringPattern
|
|
|
529
483
|
end
|
|
530
484
|
end until good_result or tries > 100
|
|
531
485
|
unless good_result
|
|
532
|
-
|
|
533
|
-
raise StringPattern::GenerationImpossibleError, msg if @raise_on_error
|
|
534
|
-
StringPattern.log_message(msg)
|
|
535
|
-
return ""
|
|
486
|
+
return StringPattern.fail_generation("Not possible to generate an email on StringPattern.generate: #{pattern.inspect}, expected_errors: #{expected_errors.inspect}", StringPattern::GenerationImpossibleError)
|
|
536
487
|
end
|
|
537
488
|
end
|
|
538
489
|
if @dont_repeat
|
|
@@ -563,10 +514,20 @@ class StringPattern
|
|
|
563
514
|
if @block_list.respond_to?(:call)
|
|
564
515
|
good_result = false if @block_list.call(string)
|
|
565
516
|
elsif @block_list.is_a?(Array)
|
|
566
|
-
@
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
517
|
+
if @block_list_regexp
|
|
518
|
+
@block_list.each do |bl|
|
|
519
|
+
if string.match?(/#{bl}/i)
|
|
520
|
+
good_result = false
|
|
521
|
+
break
|
|
522
|
+
end
|
|
523
|
+
end
|
|
524
|
+
else
|
|
525
|
+
downcased = string.downcase
|
|
526
|
+
@block_list.each do |bl|
|
|
527
|
+
if downcased.include?(bl.to_s.downcase)
|
|
528
|
+
good_result = false
|
|
529
|
+
break
|
|
530
|
+
end
|
|
570
531
|
end
|
|
571
532
|
end
|
|
572
533
|
end
|
|
@@ -575,12 +536,42 @@ class StringPattern
|
|
|
575
536
|
unless good_result
|
|
576
537
|
msg = "Not possible to generate the string on StringPattern.generate: #{pattern.inspect}, expected_errors: #{expected_errors.inspect}"
|
|
577
538
|
msg += "\nTake in consideration if you are using StringPattern.dont_repeat=true that you don't try to generate more strings that are possible to be generated"
|
|
578
|
-
|
|
579
|
-
StringPattern.log_message(msg)
|
|
580
|
-
return ""
|
|
539
|
+
return StringPattern.fail_generation(msg, StringPattern::GenerationImpossibleError)
|
|
581
540
|
end
|
|
582
541
|
return string
|
|
583
|
-
|
|
584
|
-
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
def self.fail_generation(message, error_class)
|
|
545
|
+
raise error_class, message if @raise_on_error
|
|
546
|
+
log_message(message)
|
|
547
|
+
""
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
def self.word_data_path(*parts)
|
|
551
|
+
File.join(__dir__, "..", "..", "..", "data", *parts)
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
def self.load_english_words(camel: false)
|
|
555
|
+
require "json"
|
|
556
|
+
if @words.nil? || @words.empty?
|
|
557
|
+
nouns = JSON.parse(File.read(word_data_path("english", "nouns.json")))
|
|
558
|
+
adjs = JSON.parse(File.read(word_data_path("english", "adjs.json")))
|
|
559
|
+
@words = adjs + nouns
|
|
560
|
+
end
|
|
561
|
+
if camel && (@words_camel.nil? || @words_camel.empty?)
|
|
562
|
+
@words_camel = @words.map(&:to_camel_case)
|
|
563
|
+
end
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
def self.load_spanish_words(camel: false)
|
|
567
|
+
require "json"
|
|
568
|
+
if @palabras.nil? || @palabras.empty?
|
|
569
|
+
@palabras = (0..11).flat_map do |i|
|
|
570
|
+
JSON.parse(File.read(word_data_path("spanish", "palabras#{i}.json")))
|
|
571
|
+
end
|
|
572
|
+
end
|
|
573
|
+
if camel && (@palabras_camel.nil? || @palabras_camel.empty?)
|
|
574
|
+
@palabras_camel = @palabras.map(&:to_camel_case)
|
|
575
|
+
end
|
|
585
576
|
end
|
|
586
577
|
end
|
|
@@ -32,6 +32,11 @@ class StringPattern
|
|
|
32
32
|
if (not_expected_errors.include?(:min_length) or not_expected_errors.include?(:max_length)) and !not_expected_errors.include?(:length)
|
|
33
33
|
not_expected_errors.push(:length)
|
|
34
34
|
end
|
|
35
|
+
if pattern.is_a?(Regexp)
|
|
36
|
+
return false if text_to_validate.nil?
|
|
37
|
+
detected_errors = StringPattern.regexp_full_match?(pattern, text_to_validate.to_s) ? [] : [:value]
|
|
38
|
+
return StringPattern.validation_decision(detected_errors, expected_errors, not_expected_errors)
|
|
39
|
+
end
|
|
35
40
|
if pattern.kind_of?(Array) and pattern.size == 1
|
|
36
41
|
pattern = pattern[0]
|
|
37
42
|
elsif pattern.kind_of?(Array) and pattern.size > 1
|
|
@@ -133,7 +138,9 @@ class StringPattern
|
|
|
133
138
|
required_chars << rd if rd.size == 1
|
|
134
139
|
}
|
|
135
140
|
if (required_chars.flatten & excluded_data.flatten).size > 0
|
|
136
|
-
|
|
141
|
+
message = "pattern argument not valid on StringPattern.validate, a character cannot be required and excluded at the same time: #{pattern.inspect}, expected_errors: #{expected_errors.inspect}"
|
|
142
|
+
raise StringPattern::InvalidPatternError, message if @raise_on_error
|
|
143
|
+
StringPattern.log_message(message)
|
|
137
144
|
return ""
|
|
138
145
|
end
|
|
139
146
|
end
|
|
@@ -190,18 +197,99 @@ class StringPattern
|
|
|
190
197
|
end
|
|
191
198
|
end
|
|
192
199
|
|
|
193
|
-
|
|
194
|
-
|
|
200
|
+
StringPattern.validation_decision(detected_errors, expected_errors, not_expected_errors)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def self.validation_decision(detected_errors, expected_errors, not_expected_errors)
|
|
204
|
+
if expected_errors.size == 0 && not_expected_errors.size == 0
|
|
205
|
+
detected_errors.uniq
|
|
206
|
+
elsif (expected_errors & detected_errors) == expected_errors && (not_expected_errors & detected_errors).empty?
|
|
207
|
+
true
|
|
195
208
|
else
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
209
|
+
false
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# True when +text+ matches the whole +regexp+, not a substring.
|
|
214
|
+
def self.regexp_full_match?(regexp, text)
|
|
215
|
+
return false unless text.is_a?(String)
|
|
216
|
+
anchored = Regexp.new("\\A(?:#{regexp.source})\\z", regexp.options)
|
|
217
|
+
anchored.match?(text)
|
|
218
|
+
rescue RegexpError
|
|
219
|
+
false
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Describes why +text+ does or does not match +pattern+.
|
|
223
|
+
# @return [Hash] :valid, :errors, :message, and :index (segment index for arrays)
|
|
224
|
+
def self.explain(text: nil, pattern: nil, **synonyms)
|
|
225
|
+
text = synonyms[:text_to_validate] if text.nil? && synonyms.key?(:text_to_validate)
|
|
226
|
+
text = synonyms[:validate] if text.nil? && synonyms.key?(:validate)
|
|
227
|
+
|
|
228
|
+
if pattern.is_a?(Array) && pattern.size > 1
|
|
229
|
+
return explain_array(text, pattern)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
errors = validate(text: text, pattern: pattern)
|
|
233
|
+
if errors == true || (errors.is_a?(Array) && errors.empty?)
|
|
234
|
+
{ valid: true, errors: [], message: "valid", index: nil }
|
|
235
|
+
elsif errors.is_a?(Array)
|
|
236
|
+
{ valid: false, errors: errors, message: "invalid: #{errors.join(', ')}", index: nil }
|
|
237
|
+
else
|
|
238
|
+
{ valid: false, errors: [:value], message: "invalid", index: nil }
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def self.explain_array(text, patterns)
|
|
243
|
+
ok, index, errors = match_segments(text.to_s, patterns, 0)
|
|
244
|
+
if ok
|
|
245
|
+
{ valid: true, errors: [], message: "valid", index: nil }
|
|
246
|
+
else
|
|
247
|
+
{ valid: false, errors: errors, message: "segment #{index} does not match: #{errors.join(', ')}", index: index }
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def self.match_segments(text, patterns, index)
|
|
252
|
+
if patterns.empty?
|
|
253
|
+
return [true, nil, []] if text.empty?
|
|
254
|
+
return [false, index, [:length]]
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
if patterns.size == 1
|
|
258
|
+
errors = validate(text: text, pattern: patterns[0])
|
|
259
|
+
return [true, nil, []] if errors == true || (errors.is_a?(Array) && errors.empty?)
|
|
260
|
+
errs = errors.is_a?(Array) ? errors : [:value]
|
|
261
|
+
return [false, index, errs]
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
info = segment_bounds(patterns[0])
|
|
265
|
+
return [false, index, [:value]] if info.nil?
|
|
266
|
+
|
|
267
|
+
best = nil
|
|
268
|
+
(info[:min]..info[:max]).each do |n|
|
|
269
|
+
next if n > text.length
|
|
270
|
+
errors = validate(text: text[0, n], pattern: patterns[0])
|
|
271
|
+
next unless errors == true || (errors.is_a?(Array) && errors.empty?)
|
|
272
|
+
ok, idx, errs = match_segments(text[n..], patterns[1..], index + 1)
|
|
273
|
+
return [true, nil, []] if ok
|
|
274
|
+
best = [idx, errs] if best.nil? || idx > best[0]
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
return [false, best[0], best[1]] if best
|
|
278
|
+
|
|
279
|
+
errors = validate(text: text, pattern: patterns[0])
|
|
280
|
+
errs = errors.is_a?(Array) ? errors : [:value]
|
|
281
|
+
errs = [:value] if errs.empty?
|
|
282
|
+
[false, index, errs]
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def self.segment_bounds(pat)
|
|
286
|
+
pattern_like = pat.to_s.scan(/(\d+)-(\d+):(.+)/).size > 0 || pat.to_s.scan(/^!?(\d+):(.+)/).size > 0
|
|
287
|
+
if pat.is_a?(String) && (!optimistic || !pattern_like)
|
|
288
|
+
{ min: pat.length, max: pat.length }
|
|
289
|
+
elsif pat.is_a?(Symbol) || pat.is_a?(String)
|
|
290
|
+
analyzed = analyze(pat, silent: true)
|
|
291
|
+
return nil unless analyzed.is_a?(Struct)
|
|
292
|
+
{ min: analyzed.min_length, max: analyzed.max_length }
|
|
205
293
|
end
|
|
206
294
|
end
|
|
207
295
|
end
|
data/lib/string_pattern.rb
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
SP_ADD_TO_RUBY = true if !defined?(SP_ADD_TO_RUBY)
|
|
2
|
+
require_relative "string_pattern/version"
|
|
2
3
|
require_relative "string/pattern/add_to_ruby" if SP_ADD_TO_RUBY
|
|
3
4
|
require_relative "string/pattern/analyze"
|
|
5
|
+
require_relative "string/pattern/checksum"
|
|
4
6
|
require_relative "string/pattern/email"
|
|
5
7
|
require_relative "string/pattern/generate"
|
|
6
8
|
require_relative "string/pattern/validate"
|
|
@@ -41,13 +43,36 @@ class StringPattern
|
|
|
41
43
|
# Raised when generation is impossible (e.g. exhausted by dont_repeat) and {raise_on_error} is true.
|
|
42
44
|
GenerationImpossibleError = Class.new(StandardError)
|
|
43
45
|
|
|
46
|
+
# Wraps Random so +rand+ matches Kernel#rand for negative limits and zero.
|
|
47
|
+
# Array#sample calls +rand+ with a positive integer.
|
|
48
|
+
class RandomSource
|
|
49
|
+
def initialize(random)
|
|
50
|
+
@random = random
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def rand(limit = nil)
|
|
54
|
+
return @random.rand if limit.nil?
|
|
55
|
+
if limit.is_a?(Integer) && limit.negative?
|
|
56
|
+
@random.rand(-limit)
|
|
57
|
+
elsif limit == 0
|
|
58
|
+
@random.rand
|
|
59
|
+
else
|
|
60
|
+
@random.rand(limit)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
44
65
|
class << self
|
|
45
66
|
# @return [String, nil] When set, warning messages are sent here instead of +puts+.
|
|
46
67
|
attr_accessor :logger
|
|
47
68
|
# @return [Boolean] When true, invalid patterns or impossible generation raise instead of returning "".
|
|
48
69
|
attr_accessor :raise_on_error
|
|
49
|
-
attr_accessor :national_chars, :optimistic, :dont_repeat, :cache, :cache_values, :default_infinite, :word_separator, :block_list, :block_list_enabled
|
|
70
|
+
attr_accessor :national_chars, :optimistic, :dont_repeat, :cache, :cache_values, :default_infinite, :word_separator, :block_list, :block_list_enabled, :block_list_regexp
|
|
50
71
|
end
|
|
72
|
+
SCOPE_KEYS = %i[
|
|
73
|
+
dont_repeat block_list block_list_enabled block_list_regexp
|
|
74
|
+
national_chars optimistic word_separator raise_on_error logger
|
|
75
|
+
].freeze
|
|
51
76
|
@national_chars = (("a".."z").to_a + ("A".."Z").to_a).join
|
|
52
77
|
@optimistic = true
|
|
53
78
|
@cache = Hash.new()
|
|
@@ -59,6 +84,7 @@ class StringPattern
|
|
|
59
84
|
@block_list = []
|
|
60
85
|
@logger = nil
|
|
61
86
|
@raise_on_error = false
|
|
87
|
+
@block_list_regexp = false
|
|
62
88
|
NUMBER_SET = ("0".."9").to_a
|
|
63
89
|
SPECIAL_SET = [" ", "~", "!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "-", "_", "+", "=", "{", "}", "[", "]", "'", ";", ":", "?", ">", "<", "`", "|", "/", '"']
|
|
64
90
|
ALPHA_SET_LOWER = ("a".."z").to_a
|
|
@@ -101,25 +127,78 @@ class StringPattern
|
|
|
101
127
|
end
|
|
102
128
|
|
|
103
129
|
# Generates up to +n+ distinct strings for +pattern+. Uses a temporary dont_repeat state.
|
|
130
|
+
# Keyword arguments, including +seed:+, are forwarded to {generate}. A seed advances one
|
|
131
|
+
# Random across the whole sample so successive values differ.
|
|
104
132
|
# @param pattern [String, Symbol, Array, Regexp]
|
|
105
133
|
# @param n [Integer]
|
|
106
134
|
# @return [Array<String>]
|
|
107
|
-
def self.sample(pattern, n)
|
|
135
|
+
def self.sample(pattern, n, seed: nil, **options)
|
|
108
136
|
return [] if n <= 0
|
|
137
|
+
|
|
138
|
+
rng = StringPattern::RandomSource.new(seed.nil? ? Random.new : Random.new(seed))
|
|
109
139
|
old_dont = @dont_repeat
|
|
110
140
|
old_cache = @cache_values.dup
|
|
141
|
+
started = true
|
|
111
142
|
@dont_repeat = true
|
|
112
143
|
@cache_values = {}
|
|
113
144
|
results = []
|
|
114
145
|
n.times do
|
|
115
|
-
s = generate(pattern)
|
|
146
|
+
s = generate(pattern, _rng: rng, **options)
|
|
116
147
|
break if s.nil? || s.empty?
|
|
117
148
|
results << s
|
|
118
149
|
end
|
|
119
150
|
results
|
|
120
151
|
ensure
|
|
121
|
-
|
|
122
|
-
|
|
152
|
+
if started
|
|
153
|
+
@dont_repeat = old_dont
|
|
154
|
+
@cache_values = old_cache
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Runs +block+ with the given settings and restores the previous values afterwards.
|
|
159
|
+
# @return [Object] the block's result
|
|
160
|
+
def self.with(**options)
|
|
161
|
+
unknown = options.keys - SCOPE_KEYS
|
|
162
|
+
raise ArgumentError, "unknown option: #{unknown.join(', ')}" unless unknown.empty?
|
|
163
|
+
|
|
164
|
+
saved = {}
|
|
165
|
+
SCOPE_KEYS.each { |key| saved[key] = instance_variable_get("@#{key}") }
|
|
166
|
+
options.each do |key, value|
|
|
167
|
+
if key == :national_chars
|
|
168
|
+
self.national_chars = value
|
|
169
|
+
else
|
|
170
|
+
instance_variable_set("@#{key}", value)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
yield
|
|
174
|
+
ensure
|
|
175
|
+
if saved
|
|
176
|
+
saved.each do |key, value|
|
|
177
|
+
if key == :national_chars
|
|
178
|
+
self.national_chars = value unless @national_chars == value
|
|
179
|
+
else
|
|
180
|
+
instance_variable_set("@#{key}", value)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Restores configuration defaults and clears pattern and generated-value caches.
|
|
187
|
+
# @return [void]
|
|
188
|
+
def self.reset!
|
|
189
|
+
self.national_chars = (("a".."z").to_a + ("A".."Z").to_a).join
|
|
190
|
+
@optimistic = true
|
|
191
|
+
@dont_repeat = false
|
|
192
|
+
@default_infinite = 10
|
|
193
|
+
@word_separator = "_"
|
|
194
|
+
@block_list_enabled = false
|
|
195
|
+
@block_list = []
|
|
196
|
+
@block_list_regexp = false
|
|
197
|
+
@logger = nil
|
|
198
|
+
@raise_on_error = false
|
|
199
|
+
@cache = {}
|
|
200
|
+
@cache_values = {}
|
|
201
|
+
nil
|
|
123
202
|
end
|
|
124
203
|
|
|
125
204
|
# Generates a random UUID v4 (e.g. "550e8400-e29b-41d4-a716-446655440000").
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: string_pattern
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mario Ruiz
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: regexp_parser
|
|
@@ -39,10 +38,12 @@ email: marioruizs@gmail.com
|
|
|
39
38
|
executables: []
|
|
40
39
|
extensions: []
|
|
41
40
|
extra_rdoc_files:
|
|
41
|
+
- CHANGELOG.md
|
|
42
42
|
- LICENSE
|
|
43
43
|
- README.md
|
|
44
44
|
files:
|
|
45
45
|
- ".yardopts"
|
|
46
|
+
- CHANGELOG.md
|
|
46
47
|
- LICENSE
|
|
47
48
|
- README.md
|
|
48
49
|
- data/english/adjs.json
|
|
@@ -61,15 +62,16 @@ files:
|
|
|
61
62
|
- data/spanish/palabras9.json
|
|
62
63
|
- lib/string/pattern/add_to_ruby.rb
|
|
63
64
|
- lib/string/pattern/analyze.rb
|
|
65
|
+
- lib/string/pattern/checksum.rb
|
|
64
66
|
- lib/string/pattern/email.rb
|
|
65
67
|
- lib/string/pattern/generate.rb
|
|
66
68
|
- lib/string/pattern/validate.rb
|
|
67
69
|
- lib/string_pattern.rb
|
|
70
|
+
- lib/string_pattern/version.rb
|
|
68
71
|
homepage: https://github.com/MarioRuiz/string_pattern
|
|
69
72
|
licenses:
|
|
70
73
|
- MIT
|
|
71
74
|
metadata: {}
|
|
72
|
-
post_install_message:
|
|
73
75
|
rdoc_options: []
|
|
74
76
|
require_paths:
|
|
75
77
|
- lib
|
|
@@ -77,15 +79,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
77
79
|
requirements:
|
|
78
80
|
- - ">="
|
|
79
81
|
- !ruby/object:Gem::Version
|
|
80
|
-
version: '0'
|
|
82
|
+
version: '3.0'
|
|
81
83
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
82
84
|
requirements:
|
|
83
85
|
- - ">="
|
|
84
86
|
- !ruby/object:Gem::Version
|
|
85
87
|
version: '0'
|
|
86
88
|
requirements: []
|
|
87
|
-
rubygems_version:
|
|
88
|
-
signing_key:
|
|
89
|
+
rubygems_version: 4.0.6
|
|
89
90
|
specification_version: 4
|
|
90
91
|
summary: 'Generate easily random strings following a simple pattern or regular expression.
|
|
91
92
|
''10-20:Xn/x/''.generate #>qBstvc6JN8ra. Also generate words in English or Spanish.
|