Class: Synthra::Types::Regex

Inherits:
Base
  • Object
show all
Defined in:
lib/synthra/types/regex.rb

Overview

Regex type generator.

Generates a string that MATCHES a supplied regular-expression pattern. The generator is small and dependency-free; it understands the common subset of regex syntax used in fake-data schemas:

  • character classes: \d \w \s \D \W \S and [a-z] [A-Z0-9] [^abc]
  • quantifiers: n n,m n, + * ?
  • literals: any other character is emitted verbatim
  • anchors: ^ and $ are stripped (treated as match boundaries)
  • groups: (...) are flattened (their contents are generated)
  • alternation: a|b picks one branch at random (seeded)

Anything it cannot understand degrades gracefully to a reasonable string rather than crashing or returning "".

Constant Summary collapse

DEFAULT_REPEAT_MAX =

cap for unbounded quantifiers (+ and *)

8

Instance Method Summary collapse

Instance Method Details

#build_from_pattern(source, rng) ⇒ String (private)

Generate a string matching +source+.

Parameters:

  • source (String)

    regex source (no surrounding slashes)

  • rng (Generator::RNG)

    seeded RNG

Returns:

  • (String)


51
52
53
54
55
56
# File 'lib/synthra/types/regex.rb', line 51

def build_from_pattern(source, rng)
  source = strip_anchors(source)
  source = pick_alternation(source, rng)
  tokens = tokenize(source)
  tokens.map { |tok| render_token(tok, rng) }.join
end

#escape_class(esc) ⇒ Object (private)

Expand an escaped class character (\d \w \s and negations) to a char list.



151
152
153
154
155
156
157
158
159
160
161
# File 'lib/synthra/types/regex.rb', line 151

def escape_class(esc)
  case esc
  when "d" then ("0".."9").to_a
  when "w" then ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a + ["_"]
  when "s" then [" ", "\t"]
  when "D" then ("a".."z").to_a + ("A".."Z").to_a
  when "W" then [" ", "-", ".", "!"]
  when "S" then ("a".."z").to_a + ("0".."9").to_a
  else [esc] # escaped literal, e.g. \. \- \/
  end
end

#generate_edge(rng, context, args) ⇒ Object



36
37
38
# File 'lib/synthra/types/regex.rb', line 36

def generate_edge(rng, context, args)
  rng.sample(["", "0", "a"])
end

#generate_invalid(rng, context, args) ⇒ Object



40
41
42
# File 'lib/synthra/types/regex.rb', line 40

def generate_invalid(rng, context, args)
  rng.sample([nil, 123, [], {}])
end

#generate_random(rng, context, args) ⇒ Object



23
24
25
26
27
28
29
30
31
32
33
34
# File 'lib/synthra/types/regex.rb', line 23

def generate_random(rng, context, args)
  pattern = args[:pattern]
  return "" unless pattern

  source = pattern.is_a?(Regexp) ? pattern.source : pattern.to_s
  return "" if source.empty?

  build_from_pattern(source, rng)
rescue StandardError
  # Pattern too complex / malformed — never crash, never return "".
  "generated_string"
end

#pick_alternation(source, rng) ⇒ Object (private)

If the (top-level) pattern uses alternation, pick one branch deterministically. Only splits on bars that are not inside a group or char class.



65
66
67
68
69
70
# File 'lib/synthra/types/regex.rb', line 65

def pick_alternation(source, rng)
  branches = split_top_level_alternation(source)
  return source if branches.length <= 1

  rng.sample(branches)
end

#printable_charsObject (private)



243
244
245
# File 'lib/synthra/types/regex.rb', line 243

def printable_chars
  @printable_chars ||= ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
end

#read_atom(source, i) ⇒ Object (private)

Read a single atom starting at +i+. Returns [atom_descriptor, next_index]. atom_descriptor is one of: { kind: :class, chars: [...] } - a character set to pick from { kind: :literal, char: "x" } - a literal character



131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/synthra/types/regex.rb', line 131

def read_atom(source, i)
  ch = source[i]
  case ch
  when "\\"
    esc = source[i + 1]
    [{ kind: :class, chars: escape_class(esc) }, i + 2]
  when "["
    chars, next_i = read_char_class(source, i + 1)
    [{ kind: :class, chars: chars }, next_i]
  when "."
    [{ kind: :class, chars: printable_chars }, i + 1]
  when "(", ")"
    # Flatten groups: skip the paren, the contents are read as normal atoms.
    [{ kind: :empty }, i + 1]
  else
    [{ kind: :literal, char: ch }, i + 1]
  end
end

#read_brace_quantifier(source, i) ⇒ Object (private)



206
207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/synthra/types/regex.rb', line 206

def read_brace_quantifier(source, i)
  close = source.index("}", i)
  return [1, 1, i] unless close

  body = source[(i + 1)...close]
  if body.include?(",")
    lo, hi = body.split(",", 2)
    min = lo.to_i
    max = hi.empty? ? min + DEFAULT_REPEAT_MAX : hi.to_i
  else
    min = max = body.to_i
  end
  [min, max, close + 1]
end

#read_char_class(source, i) ⇒ Object (private)

Read a [...] character class starting after the opening bracket. Returns [chars, next_index].



165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/synthra/types/regex.rb', line 165

def read_char_class(source, i)
  negate = false
  if source[i] == "^"
    negate = true
    i += 1
  end

  chars = []
  while i < source.length && source[i] != "]"
    if source[i] == "\\"
      chars.concat(escape_class(source[i + 1]))
      i += 2
      next
    end

    if source[i + 1] == "-" && source[i + 2] && source[i + 2] != "]"
      chars.concat((source[i]..source[i + 2]).to_a)
      i += 3
    else
      chars << source[i]
      i += 1
    end
  end
  i += 1 # consume closing ]

  chars = printable_chars - chars if negate
  [chars, i]
end

#read_quantifier(source, i) ⇒ Object (private)

Read a quantifier at +i+. Returns [min, max, next_index]. No quantifier => [1, 1, i].



196
197
198
199
200
201
202
203
204
# File 'lib/synthra/types/regex.rb', line 196

def read_quantifier(source, i)
  case source[i]
  when "+" then [1, DEFAULT_REPEAT_MAX, i + 1]
  when "*" then [0, DEFAULT_REPEAT_MAX, i + 1]
  when "?" then [0, 1, i + 1]
  when "{" then read_brace_quantifier(source, i)
  else [1, 1, i]
  end
end

#render_atom(atom, rng) ⇒ Object (private)



229
230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/synthra/types/regex.rb', line 229

def render_atom(atom, rng)
  case atom[:kind]
  when :class
    chars = atom[:chars]
    chars.empty? ? "" : rng.sample(chars)
  when :literal
    atom[:char]
  else
    # :nocov: render_token short-circuits :empty atoms, so this arm is a defensive fallback only
    ""
    # :nocov:
  end
end

#render_token(token, rng) ⇒ Object (private)



221
222
223
224
225
226
227
# File 'lib/synthra/types/regex.rb', line 221

def render_token(token, rng)
  atom = token[:atom]
  return "" if atom[:kind] == :empty

  count = token[:min] == token[:max] ? token[:min] : rng.int(token[:min], token[:max])
  Array.new(count) { render_atom(atom, rng) }.join
end

#split_top_level_alternation(source) ⇒ Object (private)



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/synthra/types/regex.rb', line 72

def split_top_level_alternation(source)
  branches = []
  current = +""
  depth = 0
  in_class = false
  i = 0
  while i < source.length
    ch = source[i]
    case ch
    when "\\"
      current << ch << (source[i + 1] || "")
      i += 2
      next
    when "["
      in_class = true
      current << ch
    when "]"
      in_class = false
      current << ch
    when "("
      depth += 1 unless in_class
      current << ch
    when ")"
      depth -= 1 unless in_class
      current << ch
    when "|"
      if depth.zero? && !in_class
        branches << current
        current = +""
      else
        current << ch
      end
    else
      current << ch
    end
    i += 1
  end
  branches << current
  branches
end

#strip_anchors(source) ⇒ Object (private)

Strip leading ^ and trailing $ anchors (they constrain position, not content).



59
60
61
# File 'lib/synthra/types/regex.rb', line 59

def strip_anchors(source)
  source.sub(/\A\^/, "").sub(/\$\z/, "")
end

#tokenize(source) ⇒ Array<Hash> (private)

Break the pattern into (atom, quantifier) tokens.

Returns:

  • (Array<Hash>)

    each token: { atom:, min:, max: }



116
117
118
119
120
121
122
123
124
125
# File 'lib/synthra/types/regex.rb', line 116

def tokenize(source)
  tokens = []
  i = 0
  while i < source.length
    atom, i = read_atom(source, i)
    min, max, i = read_quantifier(source, i)
    tokens << { atom: atom, min: min, max: max }
  end
  tokens
end