Class: Synthra::Types::Regex
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
-
#build_from_pattern(source, rng) ⇒ String
private
Generate a string matching +source+.
-
#escape_class(esc) ⇒ Object
private
Expand an escaped class character (\d \w \s and negations) to a char list.
- #generate_edge(rng, context, args) ⇒ Object
- #generate_invalid(rng, context, args) ⇒ Object
- #generate_random(rng, context, args) ⇒ Object
-
#pick_alternation(source, rng) ⇒ Object
private
If the (top-level) pattern uses alternation, pick one branch deterministically.
- #printable_chars ⇒ Object private
-
#read_atom(source, i) ⇒ Object
private
Read a single atom starting at +i+.
- #read_brace_quantifier(source, i) ⇒ Object private
-
#read_char_class(source, i) ⇒ Object
private
Read a [...] character class starting after the opening bracket.
-
#read_quantifier(source, i) ⇒ Object
private
Read a quantifier at +i+.
- #render_atom(atom, rng) ⇒ Object private
- #render_token(token, rng) ⇒ Object private
- #split_top_level_alternation(source) ⇒ Object private
-
#strip_anchors(source) ⇒ Object
private
Strip leading ^ and trailing $ anchors (they constrain position, not content).
-
#tokenize(source) ⇒ Array<Hash>
private
Break the pattern into (atom, quantifier) tokens.
Instance Method Details
#build_from_pattern(source, rng) ⇒ String (private)
Generate a string matching +source+.
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_chars ⇒ Object (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.
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 |