Skip to content

Commit f409ca6

Browse files
authored
Merge pull request #9 from MarioRuiz/release-2.4.0
Release 2.4.0: email validation, analyze, UUID, seed/sample, valid?, …
2 parents 8f539e3 + f1536d9 commit f409ca6

File tree

12 files changed

+404
-65
lines changed

12 files changed

+404
-65
lines changed

.travis.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
language: ruby
22
rvm:
3-
- 2.7
43
- 3.0
4+
- 3.1
5+
- 3.2
6+
- 3.3
57
- ruby-head
68
branches:
79
except:

README.md

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -318,12 +318,9 @@ Examples:
318318

319319
If you need to validate if a specific text is fulfilling the pattern you can use the validate method.
320320

321-
If a string pattern supplied and no other parameters supplied the output will be an array with the errors detected.
321+
When you supply a single pattern and do **not** supply `expected_errors` or `not_expected_errors`, the method returns an **array of error symbols**: an empty array `[]` when the text is valid, or one or more of `:min_length`, `:max_length`, `:length`, `:value`, `:string_set_not_allowed`, `:required_data`, `:excluded_data` when invalid.
322322

323-
324-
Possible output values, empty array (validation without errors detected) or one or more of: :min_length, :max_length, :length, :value, :string_set_not_allowed, :required_data, :excluded_data
325-
326-
In case an array of patterns supplied it will return only true or false
323+
When an array of patterns is supplied, the method returns only `true` or `false`.
327324

328325
Examples:
329326

@@ -443,6 +440,69 @@ StringPattern.block_list_enabled = true
443440
"2-20:Tn".gen #>AAñ34Ef99éNOP
444441
```
445442

443+
#### StringPattern.analyze
444+
445+
To inspect how a pattern is parsed without generating or validating:
446+
447+
```ruby
448+
p = StringPattern.analyze("10-20:LN/x/")
449+
# => #<Struct min_length=10, max_length=20, symbol_type="LN/x/", required_data=..., string_set=..., unique=false>
450+
p.min_length # => 10
451+
p.max_length # => 20
452+
p.symbol_type # => "LN/x/"
453+
```
454+
455+
Useful for debugging or building tools on top of the pattern DSL. Invalid patterns return the pattern string; use `silent: true` to avoid logging.
456+
457+
#### Error handling and logging
458+
459+
By default, when generation is impossible (e.g. invalid pattern or `dont_repeat` exhausted), `generate` returns an empty string `""` and a message is printed. You can:
460+
461+
- Set `StringPattern.logger = Logger.new($stderr)` to send messages to a logger instead of `puts`.
462+
- Set `StringPattern.raise_on_error = true` to raise `StringPattern::GenerationImpossibleError` or `StringPattern::InvalidPatternError` instead of returning `""`.
463+
464+
#### Reproducible generation (seed)
465+
466+
Pass `seed:` to get the same string for the same pattern in tests:
467+
468+
```ruby
469+
"10:N".gen(seed: 42) # => same result every time
470+
```
471+
472+
#### Batch generation (sample)
473+
474+
Generate up to `n` distinct strings without mutating the global dont_repeat cache:
475+
476+
```ruby
477+
StringPattern.sample("4:N", 10) # => array of 10 distinct 4-digit strings
478+
```
479+
480+
#### Boolean validation (valid?)
481+
482+
Check if text matches a pattern without building the full error list:
483+
484+
```ruby
485+
StringPattern.valid?(text: "user@domain.com", pattern: "14-40:@") # => true
486+
```
487+
488+
#### UUID
489+
490+
Generate a random UUID v4 or validate one:
491+
492+
```ruby
493+
StringPattern.uuid # => "550e8400-e29b-41d4-a716-446655440000"
494+
StringPattern.valid_uuid?(some_str) # => true or false
495+
```
496+
497+
#### block_list as Proc
498+
499+
You can set `block_list` to a Proc for custom blocking:
500+
501+
```ruby
502+
StringPattern.block_list = ->(s) { s.include?("forbidden") }
503+
StringPattern.block_list_enabled = true
504+
```
505+
446506

447507
## Contributing
448508

lib/string/pattern/add_to_ruby.rb

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ def to_sp
106106
elsif token == :literal and text.size == 2
107107
text = text[1]
108108
else
109-
puts "Report token not controlled: type: #{type}, token: #{token}, text: '#{text}' [#{ts}..#{te}]"
109+
StringPattern.log_message("Report token not controlled: type: #{type}, token: #{token}, text: '#{text}' [#{ts}..#{te}]")
110110
end
111111
end
112112

@@ -165,7 +165,7 @@ def to_sp
165165
set_negate = false
166166
else
167167
pats += "]"
168-
end
168+
end
169169

170170
end
171171
elsif type == :group
@@ -190,7 +190,6 @@ def to_sp
190190
patg << pats
191191
pats = ""
192192
elsif patg.empty?
193-
# for the case the first element was not added to patg and was on pata fex: (a+|b|c)
194193
patg << pata.pop
195194
end
196195
end
@@ -299,11 +298,11 @@ def to_sp
299298
end
300299
if pats != ""
301300
if pata.empty?
302-
if pats[0] == "[" and pats[-1] == "]" #fex: /[12ab]/
301+
if pats[0] == "[" and pats[-1] == "]"
303302
pata = ["1:#{pats}"]
304303
end
305304
else
306-
pata[-1] += pats[1] #fex: /allo/
305+
pata[-1] += pats[1]
307306
end
308307
end
309308
if pata.size == 1 and pata[0].kind_of?(String)
@@ -325,7 +324,7 @@ def generate(pattern, expected_errors: [], **synonyms)
325324
if pattern.is_a?(String) || pattern.is_a?(Array) || pattern.is_a?(Symbol) || pattern.is_a?(Regexp)
326325
StringPattern.generate(pattern, expected_errors: expected_errors, **synonyms)
327326
else
328-
puts " Kernel generate method: class not recognized:#{pattern.class}"
327+
StringPattern.log_message(" Kernel generate method: class not recognized:#{pattern.class}")
329328
end
330329
end
331330

lib/string/pattern/analyze.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
class StringPattern
2-
###############################################
3-
# Analyze the pattern supplied and returns an object of Pattern structure including:
4-
# min_length, max_length, symbol_type, required_data, excluded_data, data_provided, string_set, all_characters_set
5-
###############################################
2+
# Analyzes a pattern string and returns a Pattern struct.
3+
# @param pattern [String, Symbol] Pattern in format "length:type" or "min-max:type" (e.g. "10:N", "5-15:L")
4+
# @param silent [Boolean] If true, invalid patterns do not log a message.
5+
# @return [Struct, String] Pattern struct with min_length, max_length, symbol_type, required_data, excluded_data, data_provided, string_set, all_characters_set, unique; or the pattern string if invalid.
66
def StringPattern.analyze(pattern, silent: false)
77
#unless @cache[pattern.to_s].nil?
88
# return Pattern.new(@cache[pattern.to_s].min_length.clone, @cache[pattern.to_s].max_length.clone,
@@ -16,7 +16,7 @@ def StringPattern.analyze(pattern, silent: false)
1616
min_length, symbol_type = pattern.to_s.scan(/^!?(\d+):(.+)/)[0]
1717
max_length = min_length
1818
if min_length.nil?
19-
puts "pattern argument not valid on StringPattern.generate: #{pattern.inspect}" unless silent
19+
StringPattern.log_message("pattern argument not valid on StringPattern.generate: #{pattern.inspect}") unless silent
2020
return pattern.to_s
2121
end
2222
end

lib/string/pattern/email.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# frozen_string_literal: true
2+
3+
class StringPattern
4+
# Validates email format using the same rules as pattern type @:
5+
# - Forbids consecutive/adjacent invalid sequences (.. __ -- etc.)
6+
# - Local part: [a-z0-9]+([\+\._\-][a-z0-9])*
7+
# - Domain part: [0-9a-z]+([\.-][a-z0-9])*
8+
def self.valid_email?(string)
9+
return false if string.nil? || !string.is_a?(String)
10+
return false if string.index("@").to_i <= 0
11+
12+
wrong = %w(.. __ -- ._ _. .- -. _- -_ @. @_ @- .@ _@ -@ @@)
13+
return false if Regexp.union(*wrong) === string
14+
15+
local = string[0..(string.index("@") - 1)]
16+
domain = string[(string.index("@") + 1)..-1]
17+
local_ok = local.scan(/([a-z0-9]+([\+\._\-][a-z0-9]|)*)/i).join == local
18+
domain_ok = domain.scan(/([0-9a-z]+([\.-][a-z0-9]|)*)/i).join == domain
19+
local_ok && domain_ok
20+
end
21+
end

lib/string/pattern/generate.rb

Lines changed: 24 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ class StringPattern
6464
# the generated string
6565
###############################################
6666
def StringPattern.generate(pattern, expected_errors: [], **synonyms)
67+
seed_given = synonyms.key?(:seed)
68+
saved_rng = seed_given ? srand(synonyms[:seed]) : nil
6769
tries = 0
6870
begin
6971
good_result = true
@@ -95,7 +97,7 @@ def StringPattern.generate(pattern, expected_errors: [], **synonyms)
9597
string << pat
9698
end
9799
else
98-
puts "StringPattern.generate: it seems you supplied wrong array of patterns: #{pattern.inspect}, expected_errors: #{expected_errors.inspect}"
100+
StringPattern.log_message("StringPattern.generate: it seems you supplied wrong array of patterns: #{pattern.inspect}, expected_errors: #{expected_errors.inspect}")
99101
return ""
100102
end
101103
}
@@ -120,7 +122,7 @@ def StringPattern.generate(pattern, expected_errors: [], **synonyms)
120122
}
121123
unless excluded_data.size == 0
122124
if (required_chars.flatten & excluded_data.flatten).size > 0
123-
puts "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}"
125+
StringPattern.log_message("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}")
124126
return ""
125127
end
126128
end
@@ -130,7 +132,7 @@ def StringPattern.generate(pattern, expected_errors: [], **synonyms)
130132
elsif pattern.kind_of?(Regexp)
131133
return generate(pattern.to_sp, expected_errors: expected_errors)
132134
else
133-
puts "pattern argument not valid on StringPattern.generate: #{pattern.inspect}, expected_errors: #{expected_errors.inspect}"
135+
StringPattern.log_message("pattern argument not valid on StringPattern.generate: #{pattern.inspect}, expected_errors: #{expected_errors.inspect}")
134136
return pattern.to_s
135137
end
136138

@@ -169,20 +171,20 @@ def StringPattern.generate(pattern, expected_errors: [], **synonyms)
169171

170172
unless deny_pattern
171173
if required_data.size == 0 and expected_errors_left.include?(:required_data)
172-
puts "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}"
174+
StringPattern.log_message("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}")
173175
return ""
174176
end
175177

176178
if excluded_data.size == 0 and expected_errors_left.include?(:excluded_data)
177-
puts "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}"
179+
StringPattern.log_message("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}")
178180
return ""
179181
end
180182

181183
if expected_errors_left.include?(:string_set_not_allowed)
182184
string_set_not_allowed = all_characters_set - string_set
183185

184186
if string_set_not_allowed.size == 0
185-
puts "all characters are allowed so it won't be possible to generate a wrong string. StringPattern.generate: #{pattern.inspect}, expected_errors: #{expected_errors.inspect}"
187+
StringPattern.log_message("all characters are allowed so it won't be possible to generate a wrong string. StringPattern.generate: #{pattern.inspect}, expected_errors: #{expected_errors.inspect}")
186188
return ""
187189
end
188190
end
@@ -205,7 +207,7 @@ def StringPattern.generate(pattern, expected_errors: [], **synonyms)
205207
expected_errors_left.delete(:length)
206208
expected_errors_left.delete(:min_length)
207209
else
208-
puts "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}"
210+
StringPattern.log_message("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}")
209211
return ""
210212
end
211213
elsif expected_errors_left.include?(:max_length) or expected_errors_left.include?(:length)
@@ -264,7 +266,7 @@ def StringPattern.generate(pattern, expected_errors: [], **synonyms)
264266
end
265267
if ((0...string.length).find_all { |i| string[i, 1] == rd_to_set }).size == 0
266268
if positions_to_set.size == 0
267-
puts "pattern not valid on StringPattern.generate, not possible to generate a valid string: #{pattern.inspect}, expected_errors: #{expected_errors.inspect}"
269+
StringPattern.log_message("pattern not valid on StringPattern.generate, not possible to generate a valid string: #{pattern.inspect}, expected_errors: #{expected_errors.inspect}")
268270
return ""
269271
else
270272
k = positions_to_set.sample
@@ -289,7 +291,7 @@ def StringPattern.generate(pattern, expected_errors: [], **synonyms)
289291
string_set_not_allowed = all_characters_set - string_set if string_set_not_allowed.size == 0
290292

291293
if string_set_not_allowed.size == 0
292-
puts "Not possible to generate a non valid string on StringPattern.generate: #{pattern.inspect}, expected_errors: #{expected_errors.inspect}"
294+
StringPattern.log_message("Not possible to generate a non valid string on StringPattern.generate: #{pattern.inspect}, expected_errors: #{expected_errors.inspect}")
293295
return ""
294296
end
295297
(rand(string.size) + 1).times {
@@ -502,24 +504,11 @@ def StringPattern.generate(pattern, expected_errors: [], **synonyms)
502504
expected_errors_left.delete(:string_set_not_allowed)
503505
end
504506

505-
error_regular_expression = false
507+
error_regular_expression = !StringPattern.valid_email?(string)
506508

507509
if deny_pattern and expected_errors.include?(:length)
508510
good_result = true #it is already with wrong length
509511
else
510-
# I'm doing this because many times the regular expression checking hangs with these characters
511-
wrong = %w(.. __ -- ._ _. .- -. _- -_ @. @_ @- .@ _@ -@ @@)
512-
if !(Regexp.union(*wrong) === string) #don't include any or the wrong strings
513-
if string.index("@").to_i > 0 and
514-
string[0..(string.index("@") - 1)].scan(/([a-z0-9]+([\+\._\-][a-z0-9]|)*)/i).join == string[0..(string.index("@") - 1)] and
515-
string[(string.index("@") + 1)..-1].scan(/([0-9a-z]+([\.-][a-z0-9]|)*)/i).join == string[string[(string.index("@") + 1)..-1]]
516-
error_regular_expression = false
517-
else
518-
error_regular_expression = true
519-
end
520-
else
521-
error_regular_expression = true
522-
end
523512

524513
if expected_errors.size == 0
525514
if error_regular_expression
@@ -540,7 +529,9 @@ def StringPattern.generate(pattern, expected_errors: [], **synonyms)
540529
end
541530
end until good_result or tries > 100
542531
unless good_result
543-
puts "Not possible to generate an email on StringPattern.generate: #{pattern.inspect}, expected_errors: #{expected_errors.inspect}"
532+
msg = "Not possible to generate an email on StringPattern.generate: #{pattern.inspect}, expected_errors: #{expected_errors.inspect}"
533+
raise StringPattern::GenerationImpossibleError, msg if @raise_on_error
534+
StringPattern.log_message(msg)
544535
return ""
545536
end
546537
end
@@ -569,7 +560,9 @@ def StringPattern.generate(pattern, expected_errors: [], **synonyms)
569560
end
570561
end
571562
if @block_list_enabled
572-
if @block_list.is_a?(Array)
563+
if @block_list.respond_to?(:call)
564+
good_result = false if @block_list.call(string)
565+
elsif @block_list.is_a?(Array)
573566
@block_list.each do |bl|
574567
if string.match?(/#{bl}/i)
575568
good_result = false
@@ -580,10 +573,14 @@ def StringPattern.generate(pattern, expected_errors: [], **synonyms)
580573
end
581574
end until good_result or tries > 10000
582575
unless good_result
583-
puts "Not possible to generate the string on StringPattern.generate: #{pattern.inspect}, expected_errors: #{expected_errors.inspect}"
584-
puts "Take 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"
576+
msg = "Not possible to generate the string on StringPattern.generate: #{pattern.inspect}, expected_errors: #{expected_errors.inspect}"
577+
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+
raise StringPattern::GenerationImpossibleError, msg if @raise_on_error
579+
StringPattern.log_message(msg)
585580
return ""
586581
end
587582
return string
583+
ensure
584+
srand(saved_rng) if saved_rng
588585
end
589586
end

lib/string/pattern/validate.rb

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def StringPattern.validate(text: "", pattern: "", expected_errors: [], not_expec
5454
max_length = patt.max_length.clone
5555
symbol_type = patt.symbol_type.clone
5656
else
57-
puts "String pattern class not supported (#{pat.class} for #{pat})"
57+
StringPattern.log_message("String pattern class not supported (#{pat.class} for #{pat})")
5858
return false
5959
end
6060

@@ -133,7 +133,7 @@ def StringPattern.validate(text: "", pattern: "", expected_errors: [], not_expec
133133
required_chars << rd if rd.size == 1
134134
}
135135
if (required_chars.flatten & excluded_data.flatten).size > 0
136-
puts "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}"
136+
StringPattern.log_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}")
137137
return ""
138138
end
139139
end
@@ -183,20 +183,7 @@ def StringPattern.validate(text: "", pattern: "", expected_errors: [], not_expec
183183
end
184184
}
185185
else #symbol_type=="@"
186-
string = text_to_validate
187-
wrong = %w(.. __ -- ._ _. .- -. _- -_ @. @_ @- .@ _@ -@ @@)
188-
if !(Regexp.union(*wrong) === string) #don't include any or the wrong strings
189-
if string.index("@").to_i > 0 and
190-
string[0..(string.index("@") - 1)].scan(/([a-z0-9]+([\+\._\-][a-z0-9]|)*)/i).join == string[0..(string.index("@") - 1)] and
191-
string[(string.index("@") + 1)..-1].scan(/([0-9a-z]+([\.-][a-z0-9]|)*)/i).join == string[string[(string.index("@") + 1)..-1]]
192-
error_regular_expression = false
193-
else
194-
error_regular_expression = true
195-
end
196-
else
197-
error_regular_expression = true
198-
end
199-
186+
error_regular_expression = !StringPattern.valid_email?(text_to_validate)
200187
if error_regular_expression
201188
detected_errors.push(:value)
202189
end

0 commit comments

Comments
 (0)