string_pattern 2.3.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.
@@ -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
@@ -54,7 +59,7 @@ class StringPattern
54
59
  max_length = patt.max_length.clone
55
60
  symbol_type = patt.symbol_type.clone
56
61
  else
57
- puts "String pattern class not supported (#{pat.class} for #{pat})"
62
+ StringPattern.log_message("String pattern class not supported (#{pat.class} for #{pat})")
58
63
  return false
59
64
  end
60
65
 
@@ -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
- 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}"
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
@@ -183,38 +190,106 @@ class StringPattern
183
190
  end
184
191
  }
185
192
  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
-
193
+ error_regular_expression = !StringPattern.valid_email?(text_to_validate)
200
194
  if error_regular_expression
201
195
  detected_errors.push(:value)
202
196
  end
203
197
  end
204
198
  end
205
199
 
206
- if expected_errors.size == 0 and not_expected_errors.size == 0
207
- return detected_errors.uniq
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
208
208
  else
209
- if expected_errors & detected_errors == expected_errors
210
- if (not_expected_errors & detected_errors).size > 0
211
- return false
212
- else
213
- return true
214
- end
215
- else
216
- return false
217
- end
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 }
218
293
  end
219
294
  end
220
295
  end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class StringPattern
4
+ VERSION = "2.5.0"
5
+ end
@@ -1,6 +1,9 @@
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"
6
+ require_relative "string/pattern/email"
4
7
  require_relative "string/pattern/generate"
5
8
  require_relative "string/pattern/validate"
6
9
 
@@ -29,10 +32,47 @@ require_relative "string/pattern/validate"
29
32
  # Array of words to be avoided from resultant strings.
30
33
  # block_list_enabled: (TrueFalse, default: false)
31
34
  # If true block_list will be take in consideration
35
+ #
36
+ # @example Generate a string
37
+ # StringPattern.generate("10:N") # => "3448910834"
38
+ # @example Validate and get errors
39
+ # StringPattern.validate(text: "ab", pattern: "6:N") # => [:min_length, :length]
32
40
  class StringPattern
41
+ # Raised when an invalid pattern is used and {raise_on_error} is true.
42
+ InvalidPatternError = Class.new(StandardError)
43
+ # Raised when generation is impossible (e.g. exhausted by dont_repeat) and {raise_on_error} is true.
44
+ GenerationImpossibleError = Class.new(StandardError)
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
+
33
65
  class << self
34
- attr_accessor :national_chars, :optimistic, :dont_repeat, :cache, :cache_values, :default_infinite, :word_separator, :block_list, :block_list_enabled
66
+ # @return [String, nil] When set, warning messages are sent here instead of +puts+.
67
+ attr_accessor :logger
68
+ # @return [Boolean] When true, invalid patterns or impossible generation raise instead of returning "".
69
+ attr_accessor :raise_on_error
70
+ attr_accessor :national_chars, :optimistic, :dont_repeat, :cache, :cache_values, :default_infinite, :word_separator, :block_list, :block_list_enabled, :block_list_regexp
35
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
36
76
  @national_chars = (("a".."z").to_a + ("A".."Z").to_a).join
37
77
  @optimistic = true
38
78
  @cache = Hash.new()
@@ -42,6 +82,9 @@ class StringPattern
42
82
  @word_separator = "_"
43
83
  @block_list_enabled = false
44
84
  @block_list = []
85
+ @logger = nil
86
+ @raise_on_error = false
87
+ @block_list_regexp = false
45
88
  NUMBER_SET = ("0".."9").to_a
46
89
  SPECIAL_SET = [" ", "~", "!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "-", "_", "+", "=", "{", "}", "[", "]", "'", ";", ":", "?", ">", "<", "`", "|", "/", '"']
47
90
  ALPHA_SET_LOWER = ("a".."z").to_a
@@ -63,4 +106,113 @@ class StringPattern
63
106
  @national_chars = par
64
107
  end
65
108
 
109
+ def self.log_message(message)
110
+ if @logger
111
+ @logger.warn(message)
112
+ else
113
+ puts message
114
+ end
115
+ end
116
+
117
+ # Returns true if +text+ matches +pattern+; false otherwise. Uses validate under the hood.
118
+ # @param text [String] (synonyms: text_to_validate, validate)
119
+ # @param pattern [String, Symbol, Array]
120
+ # @return [Boolean]
121
+ def self.valid?(text: nil, pattern: nil, **synonyms)
122
+ text = synonyms[:text_to_validate] if text.nil? && synonyms.key?(:text_to_validate)
123
+ text = synonyms[:validate] if text.nil? && synonyms.key?(:validate)
124
+ return false if text.nil? || pattern.nil?
125
+ result = validate(text: text, pattern: pattern, **synonyms)
126
+ pattern.is_a?(Array) ? result == true : result.is_a?(Array) && result.empty?
127
+ end
128
+
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.
132
+ # @param pattern [String, Symbol, Array, Regexp]
133
+ # @param n [Integer]
134
+ # @return [Array<String>]
135
+ def self.sample(pattern, n, seed: nil, **options)
136
+ return [] if n <= 0
137
+
138
+ rng = StringPattern::RandomSource.new(seed.nil? ? Random.new : Random.new(seed))
139
+ old_dont = @dont_repeat
140
+ old_cache = @cache_values.dup
141
+ started = true
142
+ @dont_repeat = true
143
+ @cache_values = {}
144
+ results = []
145
+ n.times do
146
+ s = generate(pattern, _rng: rng, **options)
147
+ break if s.nil? || s.empty?
148
+ results << s
149
+ end
150
+ results
151
+ ensure
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
202
+ end
203
+
204
+ # Generates a random UUID v4 (e.g. "550e8400-e29b-41d4-a716-446655440000").
205
+ # @return [String]
206
+ def self.uuid
207
+ require "securerandom"
208
+ SecureRandom.uuid
209
+ end
210
+
211
+ # Returns true if +str+ is a valid UUID v4 format (8-4-4-4-12 hex with version and variant bits).
212
+ # @param str [String]
213
+ # @return [Boolean]
214
+ def self.valid_uuid?(str)
215
+ return false unless str.is_a?(String)
216
+ 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)
217
+ end
66
218
  end
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.3.0
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: 2022-09-20 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: 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,14 +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
66
+ - lib/string/pattern/email.rb
64
67
  - lib/string/pattern/generate.rb
65
68
  - lib/string/pattern/validate.rb
66
69
  - lib/string_pattern.rb
70
+ - lib/string_pattern/version.rb
67
71
  homepage: https://github.com/MarioRuiz/string_pattern
68
72
  licenses:
69
73
  - MIT
70
74
  metadata: {}
71
- post_install_message:
72
75
  rdoc_options: []
73
76
  require_paths:
74
77
  - lib
@@ -76,15 +79,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
76
79
  requirements:
77
80
  - - ">="
78
81
  - !ruby/object:Gem::Version
79
- version: '0'
82
+ version: '3.0'
80
83
  required_rubygems_version: !ruby/object:Gem::Requirement
81
84
  requirements:
82
85
  - - ">="
83
86
  - !ruby/object:Gem::Version
84
87
  version: '0'
85
88
  requirements: []
86
- rubygems_version: 3.2.15
87
- signing_key:
89
+ rubygems_version: 4.0.6
88
90
  specification_version: 4
89
91
  summary: 'Generate easily random strings following a simple pattern or regular expression.
90
92
  ''10-20:Xn/x/''.generate #>qBstvc6JN8ra. Also generate words in English or Spanish.