// A port of CucumberExpressionGenerator, ParameterTypeMatcher,
// CombinatorialGeneratedExpressionFactory and GeneratedExpression from the
// reference implementation.
///|
/// Makes Cucumber Expressions from step text, for example for the snippet
/// of an undefined step. It uses the parameter types with
/// `use_for_snippets`.
pub(all) struct CucumberExpressionGenerator {
priv registry : ParamTypeRegistry
}
///|
pub fn CucumberExpressionGenerator::new(
registry : ParamTypeRegistry,
) -> CucumberExpressionGenerator {
{ registry, }
}
///|
/// A Cucumber Expression made by `CucumberExpressionGenerator`.
pub(all) struct GeneratedExpression {
/// The escaped text around the parameters: one more than the number of
/// parameters.
priv texts : Array[String]
parameter_types : Array[ParamTypeEntry]
}
///|
/// The name and type of a parameter of a generated expression. `count` is
/// the number of times that the name is used up to and including this
/// parameter.
pub(all) struct ParameterInfo {
type_ : ParamType
name : String
count : Int
} derive(Debug, Eq)
///|
/// The text of the generated expression.
pub fn GeneratedExpression::source(self : GeneratedExpression) -> String {
let buf = StringBuilder()
for i, text in self.texts {
if i > 0 {
buf.write_string("{\{self.parameter_types[i - 1].name}}")
}
buf.write_string(text)
}
buf.to_string()
}
///|
/// Parameter names for a generated function signature, for example
/// `["int", "int2"]`.
pub fn GeneratedExpression::parameter_names(
self : GeneratedExpression,
) -> Array[String] {
self
.parameter_infos()
.map(i => if i.count == 1 { i.name } else { i.name + i.count.to_string() })
}
///|
/// The name, type and count of each parameter.
pub fn GeneratedExpression::parameter_infos(
self : GeneratedExpression,
) -> Array[ParameterInfo] {
let usage : Map[String, Int] = Map([])
self.parameter_types.map(t => {
let count = usage.get(t.name).unwrap_or(0) + 1
usage[t.name] = count
{ type_: t.type_, name: t.name, count, }
})
}
///|
/// The generator makes at most this number of expressions.
let max_generated_expressions = 256
///|
/// Make the possible expressions for the text. The best expression is first.
pub fn CucumberExpressionGenerator::generate_expressions(
self : CucumberExpressionGenerator,
text : String,
) -> Array[GeneratedExpression] {
let combinations : Array[Array[ParamTypeEntry]] = []
let matchers = self.create_parameter_type_matchers(text)
let texts : Array[String] = []
let mut pos = 0
while true {
let matching = matchers.map(m => m.advance_to(pos)).filter(m => m.find())
if matching.is_empty() {
break
}
matching.sort_by(compare_matchers)
let best = matching[0]
let types : Array[ParamTypeEntry] = []
for m in matching {
if compare_matchers(m, best) == 0 &&
!types.iter().any(t => t.name == m.entry.name) {
types.push(m.entry)
}
}
types.sort_by(compare_parameter_types)
combinations.push(types)
texts.push(escape_text(text.view(start_offset=pos, end_offset=best.start)))
pos = best.start + best.length
if pos >= text.length() {
break
}
}
texts.push(escape_text(text.view(start_offset=pos)))
let expressions : Array[GeneratedExpression] = []
fn permute(depth : Int, current : Array[ParamTypeEntry]) -> Unit {
if expressions.length() >= max_generated_expressions {
return
}
if depth == combinations.length() {
expressions.push({ texts, parameter_types: current, })
return
}
for entry in combinations[depth] {
if expressions.length() >= max_generated_expressions {
return
}
permute(depth + 1, [..current, entry])
}
}
permute(0, [])
expressions
}
///|
fn CucumberExpressionGenerator::create_parameter_type_matchers(
self : CucumberExpressionGenerator,
text : String,
) -> Array[ParameterTypeMatcher] {
let matchers : Array[ParameterTypeMatcher] = []
for entry in self.registry.entries {
if !entry.use_for_snippets {
continue
}
for pattern in entry.patterns {
// A regexp that does not compile can not match.
let regexp = @regexp.compile("(\{pattern.to_string()})") catch {
_ => continue
}
matchers.push(ParameterTypeMatcher::new(entry, regexp, text, 0))
}
}
matchers
}
///|
fn escape_text(s : StringView) -> String {
s
.to_owned()
.replace_all(old="(", new="\\(")
.replace_all(old="{", new="\\{")
.replace_all(old="/", new="\\/")
}
///|
/// The first match of a parameter type regexp in the text, at or after a
/// position.
priv struct ParameterTypeMatcher {
entry : ParamTypeEntry
regexp : @regexp.Regexp
text : String
/// The start of the match, or -1 when there is no match.
start : Int
length : Int
}
///|
fn ParameterTypeMatcher::new(
entry : ParamTypeEntry,
regexp : @regexp.Regexp,
text : String,
position : Int,
) -> ParameterTypeMatcher {
let result = regexp.execute(text.view(start_offset=position))
match result.get(0) {
Some(view) if result.matched() =>
{
entry,
regexp,
text,
start: view.start_offset(),
length: view.length(),
}
_ => { entry, regexp, text, start: -1, length: 0, }
}
}
///|
fn ParameterTypeMatcher::advance_to(
self : ParameterTypeMatcher,
position : Int,
) -> ParameterTypeMatcher {
for p in position.. Bool {
if self.start < 0 || self.length == 0 {
return false
}
let end = self.start + self.length
(self.start == 0 || is_word_boundary(self.text.code_unit_at(self.start - 1))) &&
(end == self.text.length() || is_word_boundary(self.text.code_unit_at(end)))
}
///|
/// Sort matchers by position, then longest match first.
fn compare_matchers(a : ParameterTypeMatcher, b : ParameterTypeMatcher) -> Int {
if a.start != b.start {
return a.start - b.start
}
b.length - a.length
}
///|
/// True for a separator, punctuation or symbol character (Unicode general
/// categories Z, P and S).
fn is_word_boundary(c : UInt16) -> Bool {
let ch = c.to_int().unsafe_to_char()
if ch.is_ascii() {
// Z in ASCII is only the space; P and S are the ASCII punctuation.
return ch == ' ' || ch.is_ascii_punctuation()
}
word_boundary_regexp.execute(ch.to_string()).matched()
}
///|
let word_boundary_regexp : @regexp.Regexp = @regexp.compile(
"^[\\p{Z}\\p{P}\\p{S}]",
) catch {
_ => abort("the word boundary regexp does not compile")
}
///|
#deprecated
pub extend ParameterInfo with Eq::{not_equal, equal}
///|
#deprecated
pub extend ParameterInfo with @debug.Debug::{to_repr}