From f1536d9c1941b5e3a40863c22299128e26d128eb Mon Sep 17 00:00:00 2001
From: MarioRuiz <MarioRuizS@gmail.com>
Date: Wed, 11 Feb 2026 14:18:51 +0000
Subject: [PATCH] Release 2.4.0: email validation, analyze, UUID, seed/sample,
 valid?, raise_on_error

- Add StringPattern.valid_email? for email format validation (aligned with @ pattern)
- Add lib/string/pattern/email.rb and include in gemspec
- Document StringPattern.analyze; add analyze_spec.rb
- Add UUID v4: StringPattern.uuid, StringPattern.valid_uuid?
- Add seed: for reproducible generation, sample for batch distinct generation
- Add StringPattern.valid? for boolean validation
- Add raise_on_error and logger for error handling
- Support block_list as Proc
- Clarify validate return values in README
- Bump version to 2.4.0

Co-authored-by: Cursor <cursoragent@cursor.com>
---
 .travis.yml                          |   4 +-
 README.md                            |  70 +++++++++++++++--
 lib/string/pattern/add_to_ruby.rb    |  11 ++-
 lib/string/pattern/analyze.rb        |  10 +--
 lib/string/pattern/email.rb          |  21 ++++++
 lib/string/pattern/generate.rb       |  51 ++++++-------
 lib/string/pattern/validate.rb       |  19 +----
 lib/string_pattern.rb                |  73 ++++++++++++++++++
 spec/string/pattern/analyze_spec.rb  | 109 +++++++++++++++++++++++++++
 spec/string/pattern/generate_spec.rb |  56 +++++++++++++-
 spec/string/pattern/validate_spec.rb |  40 ++++++++++
 string_pattern.gemspec               |   5 +-
 12 files changed, 404 insertions(+), 65 deletions(-)
 create mode 100644 lib/string/pattern/email.rb
 create mode 100644 spec/string/pattern/analyze_spec.rb

diff --git a/.travis.yml b/.travis.yml
index dcfd6f1..af2941d 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,7 +1,9 @@
 language: ruby
 rvm:
-  - 2.7
   - 3.0
+  - 3.1
+  - 3.2
+  - 3.3
   - ruby-head
 branches:
   except:
diff --git a/README.md b/README.md
index 219c935..12bae33 100644
--- a/README.md
+++ b/README.md
@@ -318,12 +318,9 @@ Examples:
 
 If you need to validate if a specific text is fulfilling the pattern you can use the validate method.
 
-If a string pattern supplied and no other parameters supplied the output will be an array with the errors detected. 
+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.
 
-
-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
-
-In case an array of patterns supplied it will return only true or false
+When an array of patterns is supplied, the method returns only `true` or `false`.
 
 Examples:
 
@@ -443,6 +440,69 @@ StringPattern.block_list_enabled = true
 "2-20:Tn".gen #>AAñ34Ef99éNOP
 ```
 
+#### StringPattern.analyze
+
+To inspect how a pattern is parsed without generating or validating:
+
+```ruby
+p = StringPattern.analyze("10-20:LN/x/")
+# => #<Struct min_length=10, max_length=20, symbol_type="LN/x/", required_data=..., string_set=..., unique=false>
+p.min_length   # => 10
+p.max_length   # => 20
+p.symbol_type  # => "LN/x/"
+```
+
+Useful for debugging or building tools on top of the pattern DSL. Invalid patterns return the pattern string; use `silent: true` to avoid logging.
+
+#### Error handling and logging
+
+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:
+
+- Set `StringPattern.logger = Logger.new($stderr)` to send messages to a logger instead of `puts`.
+- Set `StringPattern.raise_on_error = true` to raise `StringPattern::GenerationImpossibleError` or `StringPattern::InvalidPatternError` instead of returning `""`.
+
+#### Reproducible generation (seed)
+
+Pass `seed:` to get the same string for the same pattern in tests:
+
+```ruby
+"10:N".gen(seed: 42)  # => same result every time
+```
+
+#### Batch generation (sample)
+
+Generate up to `n` distinct strings without mutating the global dont_repeat cache:
+
+```ruby
+StringPattern.sample("4:N", 10)  # => array of 10 distinct 4-digit strings
+```
+
+#### Boolean validation (valid?)
+
+Check if text matches a pattern without building the full error list:
+
+```ruby
+StringPattern.valid?(text: "user@domain.com", pattern: "14-40:@")  # => true
+```
+
+#### UUID
+
+Generate a random UUID v4 or validate one:
+
+```ruby
+StringPattern.uuid                    # => "550e8400-e29b-41d4-a716-446655440000"
+StringPattern.valid_uuid?(some_str)   # => true or false
+```
+
+#### block_list as Proc
+
+You can set `block_list` to a Proc for custom blocking:
+
+```ruby
+StringPattern.block_list = ->(s) { s.include?("forbidden") }
+StringPattern.block_list_enabled = true
+```
+
 
 ## Contributing
 
diff --git a/lib/string/pattern/add_to_ruby.rb b/lib/string/pattern/add_to_ruby.rb
index 511b1c8..adcc06b 100644
--- a/lib/string/pattern/add_to_ruby.rb
+++ b/lib/string/pattern/add_to_ruby.rb
@@ -106,7 +106,7 @@ def to_sp
         elsif token == :literal and text.size == 2
           text = text[1]
         else
-          puts "Report token not controlled: type: #{type}, token: #{token}, text: '#{text}' [#{ts}..#{te}]"
+          StringPattern.log_message("Report token not controlled: type: #{type}, token: #{token}, text: '#{text}' [#{ts}..#{te}]")
         end
       end
 
@@ -165,7 +165,7 @@ def to_sp
               set_negate = false
             else
               pats += "]"
-            end    
+            end
 
           end
         elsif type == :group
@@ -190,7 +190,6 @@ def to_sp
             patg << pats
             pats = ""
           elsif patg.empty?
-            # for the case the first element was not added to patg and was on pata fex: (a+|b|c)
             patg << pata.pop
           end
         end
@@ -299,11 +298,11 @@ def to_sp
     end
     if pats != ""
       if pata.empty?
-        if pats[0] == "[" and pats[-1] == "]" #fex: /[12ab]/
+        if pats[0] == "[" and pats[-1] == "]"
           pata = ["1:#{pats}"]
         end
       else
-        pata[-1] += pats[1] #fex: /allo/
+        pata[-1] += pats[1]
       end
     end
     if pata.size == 1 and pata[0].kind_of?(String)
@@ -325,7 +324,7 @@ def generate(pattern, expected_errors: [], **synonyms)
     if pattern.is_a?(String) || pattern.is_a?(Array) || pattern.is_a?(Symbol) || pattern.is_a?(Regexp)
       StringPattern.generate(pattern, expected_errors: expected_errors, **synonyms)
     else
-      puts " Kernel generate method: class not recognized:#{pattern.class}"
+      StringPattern.log_message(" Kernel generate method: class not recognized:#{pattern.class}")
     end
   end
 
diff --git a/lib/string/pattern/analyze.rb b/lib/string/pattern/analyze.rb
index df65494..3873a9a 100644
--- a/lib/string/pattern/analyze.rb
+++ b/lib/string/pattern/analyze.rb
@@ -1,8 +1,8 @@
 class StringPattern
-  ###############################################
-  # Analyze the pattern supplied and returns an object of Pattern structure including:
-  # min_length, max_length, symbol_type, required_data, excluded_data, data_provided, string_set, all_characters_set
-  ###############################################
+  # Analyzes a pattern string and returns a Pattern struct.
+  # @param pattern [String, Symbol] Pattern in format "length:type" or "min-max:type" (e.g. "10:N", "5-15:L")
+  # @param silent [Boolean] If true, invalid patterns do not log a message.
+  # @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.
   def StringPattern.analyze(pattern, silent: false)
     #unless @cache[pattern.to_s].nil?
     #  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)
       min_length, symbol_type = pattern.to_s.scan(/^!?(\d+):(.+)/)[0]
       max_length = min_length
       if min_length.nil?
-        puts "pattern argument not valid on StringPattern.generate: #{pattern.inspect}" unless silent
+        StringPattern.log_message("pattern argument not valid on StringPattern.generate: #{pattern.inspect}") unless silent
         return pattern.to_s
       end
     end
diff --git a/lib/string/pattern/email.rb b/lib/string/pattern/email.rb
new file mode 100644
index 0000000..a79fc34
--- /dev/null
+++ b/lib/string/pattern/email.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class StringPattern
+  # Validates email format using the same rules as pattern type @:
+  # - Forbids consecutive/adjacent invalid sequences (.. __ -- etc.)
+  # - Local part: [a-z0-9]+([\+\._\-][a-z0-9])*
+  # - Domain part: [0-9a-z]+([\.-][a-z0-9])*
+  def self.valid_email?(string)
+    return false if string.nil? || !string.is_a?(String)
+    return false if string.index("@").to_i <= 0
+
+    wrong = %w(.. __ -- ._ _. .- -. _- -_ @. @_ @- .@ _@ -@ @@)
+    return false if Regexp.union(*wrong) === string
+
+    local = string[0..(string.index("@") - 1)]
+    domain = string[(string.index("@") + 1)..-1]
+    local_ok = local.scan(/([a-z0-9]+([\+\._\-][a-z0-9]|)*)/i).join == local
+    domain_ok = domain.scan(/([0-9a-z]+([\.-][a-z0-9]|)*)/i).join == domain
+    local_ok && domain_ok
+  end
+end
diff --git a/lib/string/pattern/generate.rb b/lib/string/pattern/generate.rb
index eb7b108..37ce1b6 100644
--- a/lib/string/pattern/generate.rb
+++ b/lib/string/pattern/generate.rb
@@ -64,6 +64,8 @@ class StringPattern
   #     the generated string
   ###############################################
   def StringPattern.generate(pattern, expected_errors: [], **synonyms)
+    seed_given = synonyms.key?(:seed)
+    saved_rng = seed_given ? srand(synonyms[:seed]) : nil
     tries = 0
     begin
       good_result = true
@@ -95,7 +97,7 @@ def StringPattern.generate(pattern, expected_errors: [], **synonyms)
               string << pat
             end
           else
-            puts "StringPattern.generate: it seems you supplied wrong array of patterns: #{pattern.inspect}, expected_errors: #{expected_errors.inspect}"
+            StringPattern.log_message("StringPattern.generate: it seems you supplied wrong array of patterns: #{pattern.inspect}, expected_errors: #{expected_errors.inspect}")
             return ""
           end
         }
@@ -120,7 +122,7 @@ def StringPattern.generate(pattern, expected_errors: [], **synonyms)
           }
           unless excluded_data.size == 0
             if (required_chars.flatten & excluded_data.flatten).size > 0
-              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}"
+              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}")
               return ""
             end
           end
@@ -130,7 +132,7 @@ def StringPattern.generate(pattern, expected_errors: [], **synonyms)
       elsif pattern.kind_of?(Regexp)
         return generate(pattern.to_sp, expected_errors: expected_errors)
       else
-        puts "pattern argument not valid on StringPattern.generate: #{pattern.inspect}, expected_errors: #{expected_errors.inspect}"
+        StringPattern.log_message("pattern argument not valid on StringPattern.generate: #{pattern.inspect}, expected_errors: #{expected_errors.inspect}")
         return pattern.to_s
       end
 
@@ -169,12 +171,12 @@ def StringPattern.generate(pattern, expected_errors: [], **synonyms)
 
       unless deny_pattern
         if required_data.size == 0 and expected_errors_left.include?(:required_data)
-          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}"
+          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}")
           return ""
         end
 
         if excluded_data.size == 0 and expected_errors_left.include?(:excluded_data)
-          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}"
+          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}")
           return ""
         end
 
@@ -182,7 +184,7 @@ def StringPattern.generate(pattern, expected_errors: [], **synonyms)
           string_set_not_allowed = all_characters_set - string_set
 
           if string_set_not_allowed.size == 0
-            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}"
+            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}")
             return ""
           end
         end
@@ -205,7 +207,7 @@ def StringPattern.generate(pattern, expected_errors: [], **synonyms)
             expected_errors_left.delete(:length)
             expected_errors_left.delete(:min_length)
           else
-            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}"
+            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}")
             return ""
           end
         elsif expected_errors_left.include?(:max_length) or expected_errors_left.include?(:length)
@@ -264,7 +266,7 @@ def StringPattern.generate(pattern, expected_errors: [], **synonyms)
             end
             if ((0...string.length).find_all { |i| string[i, 1] == rd_to_set }).size == 0
               if positions_to_set.size == 0
-                puts "pattern not valid on StringPattern.generate, not possible to generate a valid string: #{pattern.inspect}, expected_errors: #{expected_errors.inspect}"
+                StringPattern.log_message("pattern not valid on StringPattern.generate, not possible to generate a valid string: #{pattern.inspect}, expected_errors: #{expected_errors.inspect}")
                 return ""
               else
                 k = positions_to_set.sample
@@ -289,7 +291,7 @@ def StringPattern.generate(pattern, expected_errors: [], **synonyms)
           string_set_not_allowed = all_characters_set - string_set if string_set_not_allowed.size == 0
 
           if string_set_not_allowed.size == 0
-            puts "Not possible to generate a non valid string on StringPattern.generate: #{pattern.inspect}, expected_errors: #{expected_errors.inspect}"
+            StringPattern.log_message("Not possible to generate a non valid string on StringPattern.generate: #{pattern.inspect}, expected_errors: #{expected_errors.inspect}")
             return ""
           end
           (rand(string.size) + 1).times {
@@ -502,24 +504,11 @@ def StringPattern.generate(pattern, expected_errors: [], **synonyms)
             expected_errors_left.delete(:string_set_not_allowed)
           end
 
-          error_regular_expression = false
+          error_regular_expression = !StringPattern.valid_email?(string)
 
           if deny_pattern and expected_errors.include?(:length)
             good_result = true #it is already with wrong length
           else
-            # I'm doing this because many times the regular expression checking hangs with these characters
-            wrong = %w(.. __ -- ._ _. .- -. _- -_ @. @_ @- .@ _@ -@ @@)
-            if !(Regexp.union(*wrong) === string) #don't include any or the wrong strings
-              if string.index("@").to_i > 0 and
-                 string[0..(string.index("@") - 1)].scan(/([a-z0-9]+([\+\._\-][a-z0-9]|)*)/i).join == string[0..(string.index("@") - 1)] and
-                 string[(string.index("@") + 1)..-1].scan(/([0-9a-z]+([\.-][a-z0-9]|)*)/i).join == string[string[(string.index("@") + 1)..-1]]
-                error_regular_expression = false
-              else
-                error_regular_expression = true
-              end
-            else
-              error_regular_expression = true
-            end
 
             if expected_errors.size == 0
               if error_regular_expression
@@ -540,7 +529,9 @@ def StringPattern.generate(pattern, expected_errors: [], **synonyms)
           end
         end until good_result or tries > 100
         unless good_result
-          puts "Not possible to generate an email on StringPattern.generate: #{pattern.inspect}, expected_errors: #{expected_errors.inspect}"
+          msg = "Not possible to generate an email on StringPattern.generate: #{pattern.inspect}, expected_errors: #{expected_errors.inspect}"
+          raise StringPattern::GenerationImpossibleError, msg if @raise_on_error
+          StringPattern.log_message(msg)
           return ""
         end
       end
@@ -569,7 +560,9 @@ def StringPattern.generate(pattern, expected_errors: [], **synonyms)
         end
       end
       if @block_list_enabled
-        if @block_list.is_a?(Array)
+        if @block_list.respond_to?(:call)
+          good_result = false if @block_list.call(string)
+        elsif @block_list.is_a?(Array)
           @block_list.each do |bl|
             if string.match?(/#{bl}/i)
               good_result = false
@@ -580,10 +573,14 @@ def StringPattern.generate(pattern, expected_errors: [], **synonyms)
       end
     end until good_result or tries > 10000
     unless good_result
-      puts "Not possible to generate the string on StringPattern.generate: #{pattern.inspect}, expected_errors: #{expected_errors.inspect}"
-      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"
+      msg = "Not possible to generate the string on StringPattern.generate: #{pattern.inspect}, expected_errors: #{expected_errors.inspect}"
+      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"
+      raise StringPattern::GenerationImpossibleError, msg if @raise_on_error
+      StringPattern.log_message(msg)
       return ""
     end
     return string
+  ensure
+    srand(saved_rng) if saved_rng
   end
 end
diff --git a/lib/string/pattern/validate.rb b/lib/string/pattern/validate.rb
index 325d898..7b74eac 100644
--- a/lib/string/pattern/validate.rb
+++ b/lib/string/pattern/validate.rb
@@ -54,7 +54,7 @@ def StringPattern.validate(text: "", pattern: "", expected_errors: [], not_expec
           max_length = patt.max_length.clone
           symbol_type = patt.symbol_type.clone
         else
-          puts "String pattern class not supported (#{pat.class} for #{pat})"
+          StringPattern.log_message("String pattern class not supported (#{pat.class} for #{pat})")
           return false
         end
 
@@ -133,7 +133,7 @@ def StringPattern.validate(text: "", pattern: "", expected_errors: [], not_expec
         required_chars << rd if rd.size == 1
       }
       if (required_chars.flatten & excluded_data.flatten).size > 0
-        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}"
+        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}")
         return ""
       end
     end
@@ -183,20 +183,7 @@ def StringPattern.validate(text: "", pattern: "", expected_errors: [], not_expec
           end
         }
       else #symbol_type=="@"
-        string = text_to_validate
-        wrong = %w(.. __ -- ._ _. .- -. _- -_ @. @_ @- .@ _@ -@ @@)
-        if !(Regexp.union(*wrong) === string) #don't include any or the wrong strings
-          if string.index("@").to_i > 0 and
-             string[0..(string.index("@") - 1)].scan(/([a-z0-9]+([\+\._\-][a-z0-9]|)*)/i).join == string[0..(string.index("@") - 1)] and
-             string[(string.index("@") + 1)..-1].scan(/([0-9a-z]+([\.-][a-z0-9]|)*)/i).join == string[string[(string.index("@") + 1)..-1]]
-            error_regular_expression = false
-          else
-            error_regular_expression = true
-          end
-        else
-          error_regular_expression = true
-        end
-
+        error_regular_expression = !StringPattern.valid_email?(text_to_validate)
         if error_regular_expression
           detected_errors.push(:value)
         end
diff --git a/lib/string_pattern.rb b/lib/string_pattern.rb
index 188a9e7..45f4c71 100644
--- a/lib/string_pattern.rb
+++ b/lib/string_pattern.rb
@@ -1,6 +1,7 @@
 SP_ADD_TO_RUBY = true if !defined?(SP_ADD_TO_RUBY)
 require_relative "string/pattern/add_to_ruby" if SP_ADD_TO_RUBY
 require_relative "string/pattern/analyze"
+require_relative "string/pattern/email"
 require_relative "string/pattern/generate"
 require_relative "string/pattern/validate"
 
@@ -29,8 +30,22 @@
 #             Array of words to be avoided from resultant strings.
 # block_list_enabled: (TrueFalse, default: false)
 #             If true block_list will be take in consideration
+#
+# @example Generate a string
+#   StringPattern.generate("10:N")  # => "3448910834"
+# @example Validate and get errors
+#   StringPattern.validate(text: "ab", pattern: "6:N")  # => [:min_length, :length]
 class StringPattern
+  # Raised when an invalid pattern is used and {raise_on_error} is true.
+  InvalidPatternError = Class.new(StandardError)
+  # Raised when generation is impossible (e.g. exhausted by dont_repeat) and {raise_on_error} is true.
+  GenerationImpossibleError = Class.new(StandardError)
+
   class << self
+    # @return [String, nil] When set, warning messages are sent here instead of +puts+.
+    attr_accessor :logger
+    # @return [Boolean] When true, invalid patterns or impossible generation raise instead of returning "".
+    attr_accessor :raise_on_error
     attr_accessor :national_chars, :optimistic, :dont_repeat, :cache, :cache_values, :default_infinite, :word_separator, :block_list, :block_list_enabled
   end
   @national_chars = (("a".."z").to_a + ("A".."Z").to_a).join
@@ -42,6 +57,8 @@ class << self
   @word_separator = "_"
   @block_list_enabled = false
   @block_list = []
+  @logger = nil
+  @raise_on_error = false
   NUMBER_SET = ("0".."9").to_a
   SPECIAL_SET = [" ", "~", "!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "-", "_", "+", "=", "{", "}", "[", "]", "'", ";", ":", "?", ">", "<", "`", "|", "/", '"']
   ALPHA_SET_LOWER = ("a".."z").to_a
@@ -63,4 +80,60 @@ def self.national_chars=(par)
     @national_chars = par
   end
 
+  def self.log_message(message)
+    if @logger
+      @logger.warn(message)
+    else
+      puts message
+    end
+  end
+
+  # Returns true if +text+ matches +pattern+; false otherwise. Uses validate under the hood.
+  # @param text [String] (synonyms: text_to_validate, validate)
+  # @param pattern [String, Symbol, Array]
+  # @return [Boolean]
+  def self.valid?(text: nil, pattern: nil, **synonyms)
+    text = synonyms[:text_to_validate] if text.nil? && synonyms.key?(:text_to_validate)
+    text = synonyms[:validate] if text.nil? && synonyms.key?(:validate)
+    return false if text.nil? || pattern.nil?
+    result = validate(text: text, pattern: pattern, **synonyms)
+    pattern.is_a?(Array) ? result == true : result.is_a?(Array) && result.empty?
+  end
+
+  # Generates up to +n+ distinct strings for +pattern+. Uses a temporary dont_repeat state.
+  # @param pattern [String, Symbol, Array, Regexp]
+  # @param n [Integer]
+  # @return [Array<String>]
+  def self.sample(pattern, n)
+    return [] if n <= 0
+    old_dont = @dont_repeat
+    old_cache = @cache_values.dup
+    @dont_repeat = true
+    @cache_values = {}
+    results = []
+    n.times do
+      s = generate(pattern)
+      break if s.nil? || s.empty?
+      results << s
+    end
+    results
+  ensure
+    @dont_repeat = old_dont
+    @cache_values = old_cache
+  end
+
+  # Generates a random UUID v4 (e.g. "550e8400-e29b-41d4-a716-446655440000").
+  # @return [String]
+  def self.uuid
+    require "securerandom"
+    SecureRandom.uuid
+  end
+
+  # Returns true if +str+ is a valid UUID v4 format (8-4-4-4-12 hex with version and variant bits).
+  # @param str [String]
+  # @return [Boolean]
+  def self.valid_uuid?(str)
+    return false unless str.is_a?(String)
+    str.match?(/\A[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\z/i)
+  end
 end
diff --git a/spec/string/pattern/analyze_spec.rb b/spec/string/pattern/analyze_spec.rb
new file mode 100644
index 0000000..c495e50
--- /dev/null
+++ b/spec/string/pattern/analyze_spec.rb
@@ -0,0 +1,109 @@
+require "string_pattern"
+
+RSpec.describe StringPattern, ".analyze" do
+  describe "single pattern" do
+    it "returns a Pattern struct for simple length:symbol_type" do
+      p = StringPattern.analyze("10:N")
+      expect(p).to be_a(Struct)
+      expect(p.min_length).to eq(10)
+      expect(p.max_length).to eq(10)
+      expect(p.symbol_type).to eq("N")
+      expect(p.unique).to eq(false)
+    end
+
+    it "returns pattern with range min-max" do
+      p = StringPattern.analyze("5-15:L")
+      expect(p.min_length).to eq(5)
+      expect(p.max_length).to eq(15)
+      expect(p.symbol_type).to include("L")
+    end
+  end
+
+  describe "symbol types" do
+    it "parses N/n for numbers" do
+      p = StringPattern.analyze("3:N")
+      expect(p.symbol_type.downcase).to include("n")
+      expect(p.string_set).to include(*StringPattern::NUMBER_SET)
+    end
+
+    it "parses L for letters" do
+      p = StringPattern.analyze("4:L")
+      expect(p.symbol_type).to include("L")
+      expect(p.string_set).to include("a", "A")
+    end
+
+    it "parses x for lowercase" do
+      p = StringPattern.analyze("2:x")
+      expect(p.symbol_type).to include("x")
+    end
+
+    it "parses X for uppercase" do
+      p = StringPattern.analyze("2:X")
+      expect(p.symbol_type).to include("X")
+    end
+
+    it "parses @ for email" do
+      p = StringPattern.analyze("20:@")
+      expect(p.symbol_type).to eq("@")
+    end
+  end
+
+  describe "required and excluded data" do
+    it "parses required characters /x/" do
+      p = StringPattern.analyze("6:XN/x/")
+      expect(p.required_data).not_to be_empty
+    end
+
+    it "parses excluded characters [%0%]" do
+      p = StringPattern.analyze("10:N[%0%]")
+      expect(p.excluded_data).not_to be_empty
+      expect(p.excluded_data.flatten).to include("0")
+    end
+
+    it "parses data_provided [abc]" do
+      p = StringPattern.analyze("5:[abc]")
+      expect(p.data_provided).to include("a", "b", "c")
+    end
+  end
+
+  describe "unique flag" do
+    it "sets unique true when pattern ends with &" do
+      p = StringPattern.analyze("6-10:L&")
+      expect(p.unique).to eq(true)
+      expect(p.symbol_type).to eq("L")
+    end
+
+    it "sets unique false when no &" do
+      p = StringPattern.analyze("6:L")
+      expect(p.unique).to eq(false)
+    end
+  end
+
+  describe "deny pattern !" do
+    it "includes ! in symbol_type when pattern starts with !" do
+      p = StringPattern.analyze("!10:N")
+      expect(p.symbol_type).to start_with("!")
+    end
+  end
+
+  describe "silent" do
+    it "returns pattern string for invalid pattern and does not put when silent: true" do
+      expect { StringPattern.analyze(":N", silent: true) }.not_to output.to_stdout
+      expect(StringPattern.analyze(":N", silent: true)).to eq(":N")
+    end
+
+    it "prints message for invalid pattern when silent: false" do
+      expect { StringPattern.analyze(":N", silent: false) }.to output(/pattern argument not valid/).to_stdout
+      expect(StringPattern.analyze(":N", silent: false)).to eq(":N")
+    end
+  end
+
+  describe "caching" do
+    it "returns cached result for same pattern" do
+      p1 = StringPattern.analyze("7:Tn")
+      p2 = StringPattern.analyze("7:Tn")
+      expect(p1.min_length).to eq(p2.min_length)
+      expect(p1.symbol_type).to eq(p2.symbol_type)
+    end
+  end
+end
diff --git a/spec/string/pattern/generate_spec.rb b/spec/string/pattern/generate_spec.rb
index a7ec19e..51ca075 100644
--- a/spec/string/pattern/generate_spec.rb
+++ b/spec/string/pattern/generate_spec.rb
@@ -226,6 +226,11 @@
       it 'returns wrong email when expected_errors :string_set_not_allowed' do
         expect("30:@".gen(errors: :string_set_not_allowed)).not_to match(/(\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z)/i)
       end
+      it "generated email passes validation (regression)" do
+        email = "25-35:@".gen
+        expect(email).not_to eq("")
+        expect("25-35:@".validate(email)).to eq([])
+      end
     end
 
     describe "expected errors" do
@@ -287,7 +292,7 @@
         expect('0-2:N'.gen(errors: :min_length)).to eq ''
         expect { '0-2:N:*'.gen(errors: :min_length) }.to output(/min_length is 0 so it won't be possible to generate a wrong string smaller than 0 characters/).to_stdout
       end
-      
+
     end
 
     describe "array of patterns" do
@@ -491,12 +496,59 @@
       it "doesn't return any of block list" do
         StringPattern.block_list_enabled = true
         StringPattern.block_list = ('a'..'x').to_a
-        5.times do 
+        5.times do
           expect("2:L".gen).to match(/^[yz]{2}$/i)
         end
       end
+      it "supports block_list as Proc" do
+        StringPattern.block_list_enabled = true
+        StringPattern.block_list = ->(s) { s.include?("bad") }
+        5.times do
+          val = "5:L".gen
+          expect(val).not_to include("bad")
+        end
+        StringPattern.block_list = []
+        StringPattern.block_list_enabled = false
+      end
+    end
+
+    describe "seed (reproducible generation)" do
+      it "produces the same string for the same seed" do
+        a = "10:N".gen(seed: 42)
+        b = "10:N".gen(seed: 42)
+        expect(a).to eq(b)
+      end
+      it "produces different strings for different seeds" do
+        a = "10:N".gen(seed: 1)
+        b = "10:N".gen(seed: 2)
+        expect(a).not_to eq(b)
+      end
     end
 
+    describe "sample" do
+      it "returns n distinct strings when possible" do
+        results = StringPattern.sample("2:N", 5)
+        expect(results.size).to eq(5)
+        expect(results.uniq.size).to eq(5)
+      end
+      it "returns array of strings matching pattern" do
+        results = StringPattern.sample("3:N", 3)
+        results.each { |s| expect(s).to match(/^[0-9]{3}$/) }
+      end
+    end
 
+    describe "uuid and valid_uuid?" do
+      it "generates a valid UUID v4" do
+        u = StringPattern.uuid
+        expect(u).to match(/\A[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\z/i)
+      end
+      it "valid_uuid? returns true for valid UUID" do
+        expect(StringPattern.valid_uuid?("550e8400-e29b-41d4-a716-446655440000")).to eq(true)
+      end
+      it "valid_uuid? returns false for invalid string" do
+        expect(StringPattern.valid_uuid?("not-a-uuid")).to eq(false)
+        expect(StringPattern.valid_uuid?("550e8400-e29b-41d4-a716-44665544000")).to eq(false)
+      end
+    end
   end
 end
diff --git a/spec/string/pattern/validate_spec.rb b/spec/string/pattern/validate_spec.rb
index c3f2a79..8d67cce 100644
--- a/spec/string/pattern/validate_spec.rb
+++ b/spec/string/pattern/validate_spec.rb
@@ -5,6 +5,9 @@
     it "returns empty when good validation" do
       expect("6:N".validate("333444")).to eq([])
     end
+    it "returns false when text is nil" do
+      expect(StringPattern.validate(text: nil, pattern: "6:N")).to eq(false)
+    end
     it "returns true when good validation for array of patterns" do
       expect(["3:n", "3:x"].validate("333xxx")).to eq(true)
     end
@@ -16,6 +19,27 @@
       expect { ['1:N',3].validate('3')}.to output(/String pattern class not supported/).to_stdout
     end
   end
+
+  describe "valid?" do
+    it "returns true when text matches pattern" do
+      expect(StringPattern.valid?(text: "333444", pattern: "6:N")).to eq(true)
+    end
+    it "returns false when text does not match" do
+      expect(StringPattern.valid?(text: "33", pattern: "6:N")).to eq(false)
+    end
+    it "returns true for array pattern when valid" do
+      expect(StringPattern.valid?(text: "333xxx", pattern: ["3:n", "3:x"])).to eq(true)
+    end
+  end
+
+  describe "edge cases" do
+    it "handles block_list_enabled without block_list" do
+      StringPattern.block_list_enabled = false
+      StringPattern.block_list = []
+      expect("6:N".validate("333444")).to eq([])
+    end
+  end
+
   describe "length" do
     it "returns :min_length when wrong :min_length" do
       expect("6:N".validate("33344")).to include(:min_length)
@@ -66,6 +90,22 @@
     end
   end
 
+  describe "email validation (regression)" do
+    it "returns empty array for valid email within length" do
+      expect("14-40:@".validate("user@domain.com")).to eq([])
+      expect("10-40:@".validate("test.user+tag@example.co.uk")).to eq([])
+    end
+    it "returns :value for invalid email missing local part" do
+      expect("1-40:@".validate("@domain.com")).to include(:value)
+    end
+    it "returns :value for invalid email with consecutive dots" do
+      expect("1-40:@".validate("user@domain..com")).to include(:value)
+    end
+    it "returns :value for invalid email with double at-sign" do
+      expect("1-40:@".validate("user@@domain.com")).to include(:value)
+    end
+  end
+
   describe "expected_errors" do
     it "admits alias :errors" do
       expect("6:N".validate("335344", errors: [:value])).to eq(false)
diff --git a/string_pattern.gemspec b/string_pattern.gemspec
index c11d6ce..6438eec 100644
--- a/string_pattern.gemspec
+++ b/string_pattern.gemspec
@@ -1,12 +1,12 @@
 Gem::Specification.new do |s|
   s.name        = 'string_pattern'
-  s.version     = '2.3.0'
+  s.version     = '2.4.0'
   s.summary     = "Generate easily random strings following a simple pattern or regular expression. '10-20:Xn/x/'.generate #>qBstvc6JN8ra. Also generate words in English or Spanish. Perfect to be used in test data factories."
   s.description = "Easily generate strings supplying a very simple pattern. '10-20:Xn/x/'.generate #>qBstvc6JN8ra. Generate random strings using a regular expression (Regexp): /[a-z0-9]{2,5}\w+/.gen . Also generate words in English or Spanish. Perfect to be used in test data factories. Also, validate if a text fulfills a specific pattern or even generate a string following a pattern and returning the wrong length, value... for testing your applications."
   s.authors     = ["Mario Ruiz"]
   s.email       = 'marioruizs@gmail.com'
   s.files       = ["lib/string_pattern.rb","lib/string/pattern/add_to_ruby.rb", "lib/string/pattern/analyze.rb",
-                   "lib/string/pattern/generate.rb", "lib/string/pattern/validate.rb",
+                   "lib/string/pattern/email.rb", "lib/string/pattern/generate.rb", "lib/string/pattern/validate.rb",
                    "LICENSE","README.md",".yardopts",
                   'data/english/adjs.json', 'data/english/nouns.json', 'data/spanish/palabras0.json',
                   'data/spanish/palabras1.json','data/spanish/palabras2.json','data/spanish/palabras3.json',
@@ -18,4 +18,3 @@ Gem::Specification.new do |s|
   s.license       = 'MIT'
   s.add_runtime_dependency 'regexp_parser', '~> 2.5', '>= 2.5.0'
 end
-
