///|
/// A parsed and compiled cucumber expression, ready for matching.
pub(all) struct Expression {
priv source : String
priv regex : @regexp.Regexp
priv param_types : Array[ParamType]
priv transformers : Array[Transformer]
/// For each parameter, the number of capture groups its regex pattern uses.
/// This is needed because {string} uses multiple inner capture groups.
priv group_counts : Array[Int]
}
///|
/// A successful match result with extracted parameters.
pub(all) struct Match {
params : Array[Param]
} derive(Show, Eq)
///|
/// An extracted parameter value.
pub(all) struct Param {
value : ParamValue
type_ : ParamType
raw : String
} derive(Show, Eq)
///|
/// Parse a cucumber expression with the default parameter type registry.
pub fn Expression::parse(
expression : String,
) -> Expression raise ExpressionError {
Expression::parse_with_registry(expression, ParamTypeRegistry::default())
}
///|
/// Parse a cucumber expression with a custom parameter type registry.
pub fn Expression::parse_with_registry(
expression : String,
registry : ParamTypeRegistry,
) -> Expression raise ExpressionError {
let ast = parse_expression(expression)
let param_types = collect_param_types(ast, registry)
let transformers = collect_transformers(ast, registry)
let group_counts = collect_group_counts(ast, registry)
let regex_str = compile(ast, registry)
let regex = @regexp.compile(regex_str.view()) catch {
_ =>
raise ExpressionError::ValidationError(
position=0,
message="Failed to compile regex: " + regex_str,
)
}
{ source: expression, regex, param_types, transformers, group_counts }
}
///|
/// Get the original expression source string.
pub fn Expression::source(self : Expression) -> String {
self.source
}
///|
/// Match this expression against a text string.
/// Returns Some(Match) with extracted parameters, or None if no match.
pub fn Expression::match_(self : Expression, text : String) -> Match? {
let result = self.regex.match_(text.view())
guard result is Some(mr) else { return None }
let params : Array[Param] = []
let mut group_idx = 1 // group 0 is full match
for i, type_ in self.param_types {
let num_groups = self.group_counts[i]
let raw_value = match type_ {
ParamType::String_ => {
// {string} has multiple capture groups. The outer group (group_idx)
// captures the full match including quotes. Inner groups capture content.
// We use the outer group and strip quotes.
let outer = mr.get(group_idx)
match outer {
Some(sv) => {
let s = sv.to_string()
if (s.has_prefix("\"") && s.has_suffix("\"")) ||
(s.has_prefix("'") && s.has_suffix("'")) {
s.view(start_offset=1, end_offset=s.length() - 1).to_string()
} else {
s
}
}
None => ""
}
}
_ =>
match mr.get(group_idx) {
Some(sv) => sv.to_string()
None => ""
}
}
// Apply transformer to get typed value
let transformed = self.transformers[i].call([raw_value]) catch {
_ => ParamValue::AnonymousVal(raw_value)
}
params.push({ value: transformed, type_, raw: raw_value })
group_idx = group_idx + num_groups
}
Some({ params, })
}
///|
/// Collect parameter types from AST nodes in order.
fn collect_param_types(
node : Node,
registry : ParamTypeRegistry,
) -> Array[ParamType] {
let types : Array[ParamType] = []
fn walk(n : Node) {
match n {
ParameterNode(name) =>
match registry.get(name) {
Some(entry) => types.push(entry.type_)
None => () // error already caught during compile
}
ExpressionNode(children) | OptionalNode(children) =>
for child in children {
walk(child)
}
AlternationNode(arms) =>
for arm in arms {
for node in arm {
walk(node)
}
}
TextNode(_) => ()
}
}
walk(node)
types
}
///|
/// Collect transformers from AST nodes in order.
fn collect_transformers(
node : Node,
registry : ParamTypeRegistry,
) -> Array[Transformer] {
let transformers : Array[Transformer] = []
fn walk(n : Node) {
match n {
ParameterNode(name) =>
match registry.get(name) {
Some(entry) => transformers.push(entry.transformer)
None => ()
}
ExpressionNode(children) | OptionalNode(children) =>
for child in children {
walk(child)
}
AlternationNode(arms) =>
for arm in arms {
for node in arm {
walk(node)
}
}
TextNode(_) => ()
}
}
walk(node)
transformers
}
///|
/// Count the number of capture groups each parameter's regex pattern produces.
fn collect_group_counts(
node : Node,
registry : ParamTypeRegistry,
) -> Array[Int] {
let counts : Array[Int] = []
fn walk(n : Node) {
match n {
ParameterNode(name) =>
match registry.get(name) {
Some(entry) => counts.push(count_capture_groups(entry.patterns))
None => counts.push(1)
}
ExpressionNode(children) | OptionalNode(children) =>
for child in children {
walk(child)
}
AlternationNode(arms) =>
for arm in arms {
for node in arm {
walk(node)
}
}
TextNode(_) => ()
}
}
walk(node)
counts
}
///|
/// Count the total number of capture groups produced by the compiled form
/// of a parameter's patterns. The compiler wraps patterns in one outer
/// capturing group: `((?:pat1)|(?:pat2))`, so we start with 1 for the outer
/// group, then add any inner capturing groups from the patterns themselves.
fn count_capture_groups(patterns : Array[RegexPattern]) -> Int {
let mut count = 1 // the outer capturing group added by the compiler
for pat in patterns {
count = count + count_groups_in_pattern(pat.to_string())
}
count
}
///|
/// Count capturing groups in a single regex pattern string.
/// A capturing group is `(` not followed by `?`.
fn count_groups_in_pattern(pattern : String) -> Int {
let chars = pattern.to_array()
let len = chars.length()
let mut count = 0
let mut i = 0
while i < len {
if chars[i] == '\\' {
// skip escaped character
i = i + 2
} else if chars[i] == '(' {
if i + 1 < len && chars[i + 1] == '?' {
// non-capturing group (?:...) — don't count
()
} else {
count = count + 1
}
i = i + 1
} else {
i = i + 1
}
}
count
}