///|
/// Simple counter for generating unique IDs.
priv struct IdCounter {
mut pickle_count : Int
mut step_count : Int
}
///|
fn IdCounter::new() -> IdCounter {
{ pickle_count: 0, step_count: 0 }
}
///|
fn IdCounter::next_pickle_id(self : IdCounter) -> String {
let id = "pickle-" + self.pickle_count.to_string()
self.pickle_count += 1
id
}
///|
fn IdCounter::next_step_id(self : IdCounter) -> String {
let id = "step-" + self.step_count.to_string()
self.step_count += 1
id
}
///|
/// Replace all occurrences of `old` with `new_val` in `s`.
/// Named differently from the one in outline.mbt to avoid collision.
fn string_replace_compiler(
s : String,
old : String,
new_val : String,
) -> String {
let buf = StringBuilder::new()
let s_len = s.length()
let old_len = old.length()
if old_len == 0 {
return s
}
let mut i = 0
while i < s_len {
if i + old_len <= s_len {
let mut matches = true
for j = 0; j < old_len; j = j + 1 {
if s[i + j] != old[j] {
matches = false
break
}
}
if matches {
buf.write_string(new_val)
i = i + old_len
continue
}
}
buf.write_char(s[i].to_int().unsafe_to_char())
i = i + 1
}
buf.to_string()
}
///|
/// Map a gherkin KeywordType to a PickleStepType option.
fn map_keyword_type(
kt : @gherkin.KeywordType,
) -> @cucumber_messages.PickleStepType? {
match kt {
Context => Some(@cucumber_messages.PickleStepType::Context)
Action => Some(@cucumber_messages.PickleStepType::Action)
Outcome => Some(@cucumber_messages.PickleStepType::Outcome)
Conjunction => None
Unknown => Some(@cucumber_messages.PickleStepType::Unknown)
}
}
///|
/// Convert gherkin tags to pickle tags.
fn compile_tags(
tags : Array[@gherkin.Tag],
) -> Array[@cucumber_messages.PickleTag] {
let result : Array[@cucumber_messages.PickleTag] = []
for tag in tags {
result.push({ name: tag.name, astNodeId: tag.id })
}
result
}
///|
/// Compile a single gherkin step into a pickle step, applying placeholder
/// substitution if headers/values are provided (for scenario outlines).
fn compile_step(
step : @gherkin.Step,
ids : IdCounter,
last_type : @cucumber_messages.PickleStepType?,
headers : Array[String],
values : Array[String],
) -> (@cucumber_messages.PickleStep, @cucumber_messages.PickleStepType?) {
let mapped = map_keyword_type(step.keyword_type)
let effective_type = match mapped {
Some(t) => Some(t)
None => last_type
}
let mut text = step.text
for i = 0; i < headers.length(); i = i + 1 {
text = string_replace_compiler(text, "<" + headers[i] + ">", values[i])
}
let argument : @cucumber_messages.PickleStepArgument? = match step.argument {
Some(@gherkin.StepArgument::DocString(ds)) => {
let mut content = ds.content
for i = 0; i < headers.length(); i = i + 1 {
content = string_replace_compiler(
content,
"<" + headers[i] + ">",
values[i],
)
}
Some({
docString: Some({ content, mediaType: ds.media_type }),
dataTable: None,
})
}
Some(@gherkin.StepArgument::DataTable(dt)) => {
let rows : Array[@cucumber_messages.PickleTableRow] = []
for row in dt.rows {
let cells : Array[@cucumber_messages.PickleTableCell] = []
for cell in row.cells {
let mut value = cell.value
for i = 0; i < headers.length(); i = i + 1 {
value = string_replace_compiler(
value,
"<" + headers[i] + ">",
values[i],
)
}
cells.push({ value, })
}
rows.push({ cells, })
}
Some({ docString: None, dataTable: Some({ rows, }) })
}
None => None
}
let pickle_step : @cucumber_messages.PickleStep = {
id: ids.next_step_id(),
text,
astNodeIds: [step.id],
type_: effective_type,
argument,
}
(pickle_step, effective_type)
}
///|
/// Compile steps from an array of gherkin steps, tracking the last keyword type
/// for Conjunction resolution.
fn compile_steps(
steps : Array[@gherkin.Step],
ids : IdCounter,
last_type : @cucumber_messages.PickleStepType?,
headers : Array[String],
values : Array[String],
) -> (Array[@cucumber_messages.PickleStep], @cucumber_messages.PickleStepType?) {
let result : Array[@cucumber_messages.PickleStep] = []
let mut current_type = last_type
for step in steps {
let (ps, new_type) = compile_step(step, ids, current_type, headers, values)
result.push(ps)
current_type = new_type
}
(result, current_type)
}
///|
/// Compile a regular scenario (not an outline) into a single pickle.
fn compile_scenario(
scenario : @gherkin.Scenario,
uri : String,
language : String,
feature_tags : Array[@gherkin.Tag],
background_steps : Array[@gherkin.Step],
ids : IdCounter,
) -> @cucumber_messages.Pickle? {
// Empty scenario with no background steps produces no pickle
if scenario.steps.is_empty() && background_steps.is_empty() {
return None
}
let all_tags : Array[@gherkin.Tag] = []
all_tags.append(feature_tags)
all_tags.append(scenario.tags)
let empty_headers : Array[String] = []
let empty_values : Array[String] = []
let (bg_steps, last_type) = compile_steps(
background_steps,
ids,
None,
empty_headers,
empty_values,
)
let (sc_steps, _) = compile_steps(
scenario.steps,
ids,
last_type,
empty_headers,
empty_values,
)
let all_steps : Array[@cucumber_messages.PickleStep] = []
all_steps.append(bg_steps)
all_steps.append(sc_steps)
let pickle : @cucumber_messages.Pickle = {
id: ids.next_pickle_id(),
uri,
name: scenario.name,
language,
steps: all_steps,
tags: compile_tags(all_tags),
astNodeIds: [scenario.id],
location: None,
}
Some(pickle)
}
///|
/// Compile a scenario outline with examples into multiple pickles.
fn compile_outline(
scenario : @gherkin.Scenario,
uri : String,
language : String,
feature_tags : Array[@gherkin.Tag],
background_steps : Array[@gherkin.Step],
ids : IdCounter,
) -> Array[@cucumber_messages.Pickle] {
let pickles : Array[@cucumber_messages.Pickle] = []
for examples in scenario.examples {
let all_tags : Array[@gherkin.Tag] = []
all_tags.append(feature_tags)
all_tags.append(scenario.tags)
all_tags.append(examples.tags)
let headers : Array[String] = match examples.table_header {
Some(header) => header.cells.map(fn(c) { c.value })
None => []
}
for row in examples.table_body {
let values = row.cells.map(fn(c) { c.value })
// Build scenario name with parameter suffix
let params : Array[String] = []
for i = 0; i < headers.length(); i = i + 1 {
params.push(headers[i] + "=" + values[i])
}
let name = scenario.name + " (" + params.join(", ") + ")"
let (bg_steps, last_type) = compile_steps(
background_steps,
ids,
None,
headers,
values,
)
let (sc_steps, _) = compile_steps(
scenario.steps,
ids,
last_type,
headers,
values,
)
let all_steps : Array[@cucumber_messages.PickleStep] = []
all_steps.append(bg_steps)
all_steps.append(sc_steps)
let pickle : @cucumber_messages.Pickle = {
id: ids.next_pickle_id(),
uri,
name,
language,
steps: all_steps,
tags: compile_tags(all_tags),
astNodeIds: [scenario.id, row.id],
location: None,
}
pickles.push(pickle)
}
}
pickles
}
///|
/// Process a scenario — dispatch to regular or outline compilation.
fn process_scenario(
scenario : @gherkin.Scenario,
uri : String,
language : String,
feature_tags : Array[@gherkin.Tag],
background_steps : Array[@gherkin.Step],
ids : IdCounter,
pickles : Array[@cucumber_messages.Pickle],
) -> Unit {
if scenario.examples.is_empty() {
match
compile_scenario(
scenario, uri, language, feature_tags, background_steps, ids,
) {
Some(p) => pickles.push(p)
None => ()
}
} else {
let outline_pickles = compile_outline(
scenario, uri, language, feature_tags, background_steps, ids,
)
pickles.append(outline_pickles)
}
}
///|
/// Compile a single feature into pickles.
fn compile_feature(
uri : String,
feature : @gherkin.Feature,
ids : IdCounter,
) -> Array[@cucumber_messages.Pickle] {
let pickles : Array[@cucumber_messages.Pickle] = []
// Collect feature-level background
let feature_bg_steps : Array[@gherkin.Step] = []
for child in feature.children {
match child {
Background(bg) => {
feature_bg_steps.clear()
feature_bg_steps.append(bg.steps)
}
Scenario(scenario) =>
process_scenario(
scenario,
uri,
feature.language,
feature.tags,
feature_bg_steps,
ids,
pickles,
)
Rule(rule) => {
// Rule-scoped background = feature bg + rule bg
let rule_bg_steps : Array[@gherkin.Step] = []
rule_bg_steps.append(feature_bg_steps)
// Merge tags: feature + rule
let rule_tags : Array[@gherkin.Tag] = []
rule_tags.append(feature.tags)
rule_tags.append(rule.tags)
for rule_child in rule.children {
match rule_child {
Background(bg) => {
// Reset rule bg to feature bg + this rule background
rule_bg_steps.clear()
rule_bg_steps.append(feature_bg_steps)
rule_bg_steps.append(bg.steps)
}
Scenario(scenario) =>
process_scenario(
scenario,
uri,
feature.language,
rule_tags,
rule_bg_steps,
ids,
pickles,
)
}
}
}
}
}
pickles
}
///|
/// Compile all cached features into Pickle instances.
/// This is the main entry point for the pickle compiler.
pub fn compile_pickles(
cache : FeatureCache,
) -> Array[@cucumber_messages.Pickle] {
let ids = IdCounter::new()
let all_pickles : Array[@cucumber_messages.Pickle] = []
for entry in cache.features() {
let (uri, feature) = entry
let pickles = compile_feature(uri, feature, ids)
all_pickles.append(pickles)
}
all_pickles
}