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#""[^"\\]*(?:\\.[^"\\]*)*"|'[^'\\]*(?:\\.[^'\\]*)*'"#;
68const EMPH_RE: &str =
75 r"\*\*\*([^*]+)\*\*\*|___([^_]+)___|\*\*([^*]+)\*\*|__([^_]+)__|\*([^*]+)\*|_([^_]+)_";
76
77fn emph_transform() -> ParseFn {
81 Rc::new(|groups: &[&str]| {
82 let inner = groups.iter().copied().find(|g| !g.is_empty()).unwrap_or("");
83 Value::String(inner.to_string())
84 })
85}
86
87impl ParameterTypeRegistry {
88 pub fn new() -> ParameterTypeRegistry {
91 ParameterTypeRegistry {
92 types: vec![
93 ParameterTypeDef {
94 name: "int".to_string(),
95 regexp_source: INT_RE.to_string(),
96 transform: Transform::Int,
97 },
98 ParameterTypeDef {
99 name: "word".to_string(),
100 regexp_source: WORD_RE.to_string(),
101 transform: Transform::Word,
102 },
103 ParameterTypeDef {
104 name: "string".to_string(),
105 regexp_source: STRING_RE.to_string(),
106 transform: Transform::QuotedString,
107 },
108 ParameterTypeDef {
109 name: "emph".to_string(),
110 regexp_source: EMPH_RE.to_string(),
111 transform: Transform::Custom(emph_transform()),
112 },
113 ],
114 }
115 }
116
117 pub fn define(&mut self, name: &str, regexp_source: &str, parse: ParseFn) {
120 self.types.push(ParameterTypeDef {
121 name: name.to_string(),
122 regexp_source: regexp_source.to_string(),
123 transform: Transform::Custom(parse),
124 });
125 }
126
127 fn lookup(&self, name: &str) -> Option<&ParameterTypeDef> {
128 self.types.iter().find(|t| t.name == name)
129 }
130}
131
132impl Default for ParameterTypeRegistry {
133 fn default() -> Self {
134 Self::new()
135 }
136}
137
138#[derive(Clone, Debug, PartialEq, Eq)]
140pub struct ExpressionError {
141 pub message: String,
142}
143
144#[derive(Clone)]
146pub struct CompiledExpression {
147 source: String,
148 regexp_source: String,
149 anchored: Regex,
150 params: Vec<ParamRef>,
151}
152
153#[derive(Clone)]
154struct ParamRef {
155 group_name: String,
156 type_name: String,
157 transform: Transform,
158 inner_groups: Vec<usize>,
162}
163
164impl CompiledExpression {
165 pub fn compile(
168 source: &str,
169 types: &ParameterTypeRegistry,
170 ) -> Result<CompiledExpression, ExpressionError> {
171 let parsed = Expression::parse(source).map_err(|e| ExpressionError {
172 message: format!("failed to parse cucumber expression: {e}"),
173 })?;
174
175 let mut regex_str = String::from("^");
176 let mut params = Vec::new();
177 for se in &parsed.0 {
178 match se {
179 SingleExpression::Text(input) => regex_str.push_str(&escape_text(input.fragment())),
180 SingleExpression::Whitespaces(input) => {
181 regex_str.push_str(®ex::escape(input.fragment()))
182 }
183 SingleExpression::Parameter(p) => {
184 let name = *p.input.fragment();
185 let def = types.lookup(name).ok_or_else(|| ExpressionError {
186 message: format!("Undefined parameter type {{{name}}}"),
187 })?;
188 let group = format!("__p{}", params.len());
189 regex_str.push_str(&format!("(?P<{group}>{})", def.regexp_source));
190 params.push(ParamRef {
191 group_name: group,
192 type_name: name.to_string(),
193 transform: def.transform.clone(),
194 inner_groups: Vec::new(), });
196 }
197 SingleExpression::Optional(opt) => {
198 regex_str.push_str("(?:");
199 regex_str.push_str(&escape_text(opt.0.fragment()));
200 regex_str.push_str(")?");
201 }
202 SingleExpression::Alternation(alt) => regex_str.push_str(&alternation_regex(alt)),
203 }
204 }
205 regex_str.push('$');
206
207 let anchored = Regex::new(®ex_str).map_err(|e| ExpressionError {
208 message: format!("failed to compile expression regex: {e}"),
209 })?;
210
211 let names: Vec<Option<&str>> = anchored.capture_names().collect();
217 let param_positions: Vec<usize> = params
218 .iter()
219 .map(|p| {
220 names
221 .iter()
222 .position(|n| *n == Some(p.group_name.as_str()))
223 .expect("named parameter group exists")
224 })
225 .collect();
226 for (n, param) in params.iter_mut().enumerate() {
227 let start = param_positions[n] + 1;
228 let end = param_positions.get(n + 1).copied().unwrap_or(names.len());
229 param.inner_groups = (start..end).collect();
230 }
231
232 Ok(CompiledExpression {
233 source: source.to_string(),
234 regexp_source: regex_str,
235 anchored,
236 params,
237 })
238 }
239
240 pub fn source(&self) -> &str {
242 &self.source
243 }
244
245 pub fn regexp_source(&self) -> &str {
247 &self.regexp_source
248 }
249
250 pub fn match_whole(&self, text: &str) -> Option<Vec<Argument>> {
253 let caps = self.anchored.captures(text)?;
254 let mut args = Vec::with_capacity(self.params.len());
255 for p in &self.params {
256 match caps.name(&p.group_name) {
257 Some(m) => {
258 let value = match &p.transform {
259 Transform::Custom(f) if !p.inner_groups.is_empty() => {
263 let groups: Vec<&str> = p
264 .inner_groups
265 .iter()
266 .map(|&i| caps.get(i).map_or("", |g| g.as_str()))
267 .collect();
268 f(&groups)
269 }
270 other => apply_transform(other, m.as_str()),
271 };
272 args.push(Argument {
273 value,
274 parameter_type_name: p.type_name.clone(),
275 group: Some((m.start(), m.end())),
276 })
277 }
278 None => args.push(Argument {
279 value: Value::Null,
280 parameter_type_name: p.type_name.clone(),
281 group: None,
282 }),
283 }
284 }
285 Some(args)
286 }
287}
288
289fn apply_transform(transform: &Transform, text: &str) -> Value {
290 match transform {
291 Transform::Int => text.parse::<i64>().map_or(Value::Null, Value::Int),
292 Transform::Word => Value::String(text.to_string()),
293 Transform::QuotedString => Value::String(dequote(text)),
294 Transform::Custom(f) => f(&[text]),
295 }
296}
297
298fn dequote(s: &str) -> String {
300 let chars: Vec<char> = s.chars().collect();
301 if chars.len() < 2 {
302 return s.to_string();
303 }
304 let inner = &chars[1..chars.len() - 1];
305 let mut out = String::new();
306 let mut i = 0;
307 while i < inner.len() {
308 if inner[i] == '\\' && i + 1 < inner.len() {
309 out.push(inner[i + 1]);
310 i += 2;
311 } else {
312 out.push(inner[i]);
313 i += 1;
314 }
315 }
316 out
317}
318
319fn escape_text(raw: &str) -> String {
322 let mut unescaped = String::new();
323 let mut chars = raw.chars();
324 while let Some(c) = chars.next() {
325 if c == '\\' {
326 if let Some(n) = chars.next() {
327 unescaped.push(n);
328 }
329 } else {
330 unescaped.push(c);
331 }
332 }
333 regex::escape(&unescaped)
334}
335
336fn alternation_regex(
337 alt: &cucumber_expressions::ast::Alternation<cucumber_expressions::ast::Spanned<'_>>,
338) -> String {
339 use cucumber_expressions::ast::Alternative;
340 let mut branches = Vec::new();
341 for single in alt.0.iter() {
342 let mut branch = String::new();
343 for alternative in single {
344 match alternative {
345 Alternative::Text(t) => branch.push_str(&escape_text(t.fragment())),
346 Alternative::Optional(o) => {
347 branch.push_str("(?:");
348 branch.push_str(&escape_text(o.0.fragment()));
349 branch.push_str(")?");
350 }
351 }
352 }
353 branches.push(branch);
354 }
355 format!("(?:{})", branches.join("|"))
356}
357
358pub fn parameter_type_names(source: &str) -> Vec<String> {
361 let Ok(parsed) = Expression::parse(source) else {
362 return Vec::new();
363 };
364 parsed
365 .0
366 .iter()
367 .filter_map(|se| match se {
368 SingleExpression::Parameter(p) => Some((*p.input.fragment()).to_string()),
369 _ => None,
370 })
371 .collect()
372}