1use crate::value::Value;
18use cucumber_expressions::Expression;
19use cucumber_expressions::ast::SingleExpression;
20use regex::Regex;
21use std::rc::Rc;
22
23pub type ParseFn = Rc<dyn Fn(&[&str]) -> Value>;
29
30#[derive(Clone, Debug, PartialEq)]
32pub struct Argument {
33 pub value: Value,
35 pub parameter_type_name: String,
37 pub group: Option<(usize, usize)>,
40}
41
42#[derive(Clone)]
44pub struct ParameterTypeRegistry {
45 types: Vec<ParameterTypeDef>,
46}
47
48#[derive(Clone)]
49struct ParameterTypeDef {
50 name: String,
51 regexp_source: String,
52 transform: Transform,
53}
54
55#[derive(Clone)]
56enum Transform {
57 Int,
58 Word,
59 QuotedString,
60 Custom(ParseFn),
61}
62
63const INT_RE: &str = r"(?:-?\d+)|(?:\d+)";
66const WORD_RE: &str = r"[^\s]+";
67const STRING_RE: &str = r#""[^"\\]*(?:\\.[^"\\]*)*"|'[^'\\]*(?:\\.[^'\\]*)*'"#;
68
69impl ParameterTypeRegistry {
70 pub fn new() -> ParameterTypeRegistry {
72 ParameterTypeRegistry {
73 types: vec![
74 ParameterTypeDef {
75 name: "int".to_string(),
76 regexp_source: INT_RE.to_string(),
77 transform: Transform::Int,
78 },
79 ParameterTypeDef {
80 name: "word".to_string(),
81 regexp_source: WORD_RE.to_string(),
82 transform: Transform::Word,
83 },
84 ParameterTypeDef {
85 name: "string".to_string(),
86 regexp_source: STRING_RE.to_string(),
87 transform: Transform::QuotedString,
88 },
89 ],
90 }
91 }
92
93 pub fn define(&mut self, name: &str, regexp_source: &str, parse: ParseFn) {
96 self.types.push(ParameterTypeDef {
97 name: name.to_string(),
98 regexp_source: regexp_source.to_string(),
99 transform: Transform::Custom(parse),
100 });
101 }
102
103 fn lookup(&self, name: &str) -> Option<&ParameterTypeDef> {
104 self.types.iter().find(|t| t.name == name)
105 }
106}
107
108impl Default for ParameterTypeRegistry {
109 fn default() -> Self {
110 Self::new()
111 }
112}
113
114#[derive(Clone, Debug, PartialEq, Eq)]
116pub struct ExpressionError {
117 pub message: String,
118}
119
120#[derive(Clone)]
122pub struct CompiledExpression {
123 source: String,
124 regexp_source: String,
125 anchored: Regex,
126 params: Vec<ParamRef>,
127}
128
129#[derive(Clone)]
130struct ParamRef {
131 group_name: String,
132 type_name: String,
133 transform: Transform,
134 inner_groups: Vec<usize>,
138}
139
140impl CompiledExpression {
141 pub fn compile(
144 source: &str,
145 types: &ParameterTypeRegistry,
146 ) -> Result<CompiledExpression, ExpressionError> {
147 let parsed = Expression::parse(source).map_err(|e| ExpressionError {
148 message: format!("failed to parse cucumber expression: {e}"),
149 })?;
150
151 let mut regex_str = String::from("^");
152 let mut params = Vec::new();
153 for se in &parsed.0 {
154 match se {
155 SingleExpression::Text(input) => regex_str.push_str(&escape_text(input.fragment())),
156 SingleExpression::Whitespaces(input) => {
157 regex_str.push_str(®ex::escape(input.fragment()))
158 }
159 SingleExpression::Parameter(p) => {
160 let name = *p.input.fragment();
161 let def = types.lookup(name).ok_or_else(|| ExpressionError {
162 message: format!("Undefined parameter type {{{name}}}"),
163 })?;
164 let group = format!("__p{}", params.len());
165 regex_str.push_str(&format!("(?P<{group}>{})", def.regexp_source));
166 params.push(ParamRef {
167 group_name: group,
168 type_name: name.to_string(),
169 transform: def.transform.clone(),
170 inner_groups: Vec::new(), });
172 }
173 SingleExpression::Optional(opt) => {
174 regex_str.push_str("(?:");
175 regex_str.push_str(&escape_text(opt.0.fragment()));
176 regex_str.push_str(")?");
177 }
178 SingleExpression::Alternation(alt) => regex_str.push_str(&alternation_regex(alt)),
179 }
180 }
181 regex_str.push('$');
182
183 let anchored = Regex::new(®ex_str).map_err(|e| ExpressionError {
184 message: format!("failed to compile expression regex: {e}"),
185 })?;
186
187 let names: Vec<Option<&str>> = anchored.capture_names().collect();
193 let param_positions: Vec<usize> = params
194 .iter()
195 .map(|p| {
196 names
197 .iter()
198 .position(|n| *n == Some(p.group_name.as_str()))
199 .expect("named parameter group exists")
200 })
201 .collect();
202 for (n, param) in params.iter_mut().enumerate() {
203 let start = param_positions[n] + 1;
204 let end = param_positions.get(n + 1).copied().unwrap_or(names.len());
205 param.inner_groups = (start..end).collect();
206 }
207
208 Ok(CompiledExpression {
209 source: source.to_string(),
210 regexp_source: regex_str,
211 anchored,
212 params,
213 })
214 }
215
216 pub fn source(&self) -> &str {
218 &self.source
219 }
220
221 pub fn regexp_source(&self) -> &str {
223 &self.regexp_source
224 }
225
226 pub fn match_whole(&self, text: &str) -> Option<Vec<Argument>> {
229 let caps = self.anchored.captures(text)?;
230 let mut args = Vec::with_capacity(self.params.len());
231 for p in &self.params {
232 match caps.name(&p.group_name) {
233 Some(m) => {
234 let value = match &p.transform {
235 Transform::Custom(f) if !p.inner_groups.is_empty() => {
239 let groups: Vec<&str> = p
240 .inner_groups
241 .iter()
242 .map(|&i| caps.get(i).map_or("", |g| g.as_str()))
243 .collect();
244 f(&groups)
245 }
246 other => apply_transform(other, m.as_str()),
247 };
248 args.push(Argument {
249 value,
250 parameter_type_name: p.type_name.clone(),
251 group: Some((m.start(), m.end())),
252 })
253 }
254 None => args.push(Argument {
255 value: Value::Null,
256 parameter_type_name: p.type_name.clone(),
257 group: None,
258 }),
259 }
260 }
261 Some(args)
262 }
263}
264
265fn apply_transform(transform: &Transform, text: &str) -> Value {
266 match transform {
267 Transform::Int => text.parse::<i64>().map_or(Value::Null, Value::Int),
268 Transform::Word => Value::String(text.to_string()),
269 Transform::QuotedString => Value::String(dequote(text)),
270 Transform::Custom(f) => f(&[text]),
271 }
272}
273
274fn dequote(s: &str) -> String {
276 let chars: Vec<char> = s.chars().collect();
277 if chars.len() < 2 {
278 return s.to_string();
279 }
280 let inner = &chars[1..chars.len() - 1];
281 let mut out = String::new();
282 let mut i = 0;
283 while i < inner.len() {
284 if inner[i] == '\\' && i + 1 < inner.len() {
285 out.push(inner[i + 1]);
286 i += 2;
287 } else {
288 out.push(inner[i]);
289 i += 1;
290 }
291 }
292 out
293}
294
295fn escape_text(raw: &str) -> String {
298 let mut unescaped = String::new();
299 let mut chars = raw.chars();
300 while let Some(c) = chars.next() {
301 if c == '\\' {
302 if let Some(n) = chars.next() {
303 unescaped.push(n);
304 }
305 } else {
306 unescaped.push(c);
307 }
308 }
309 regex::escape(&unescaped)
310}
311
312fn alternation_regex(
313 alt: &cucumber_expressions::ast::Alternation<cucumber_expressions::ast::Spanned<'_>>,
314) -> String {
315 use cucumber_expressions::ast::Alternative;
316 let mut branches = Vec::new();
317 for single in alt.0.iter() {
318 let mut branch = String::new();
319 for alternative in single {
320 match alternative {
321 Alternative::Text(t) => branch.push_str(&escape_text(t.fragment())),
322 Alternative::Optional(o) => {
323 branch.push_str("(?:");
324 branch.push_str(&escape_text(o.0.fragment()));
325 branch.push_str(")?");
326 }
327 }
328 }
329 branches.push(branch);
330 }
331 format!("(?:{})", branches.join("|"))
332}
333
334pub fn parameter_type_names(source: &str) -> Vec<String> {
337 let Ok(parsed) = Expression::parse(source) else {
338 return Vec::new();
339 };
340 parsed
341 .0
342 .iter()
343 .filter_map(|se| match se {
344 SingleExpression::Parameter(p) => Some((*p.input.fragment()).to_string()),
345 _ => None,
346 })
347 .collect()
348}