use crate::parser::ast::{
Action, Condition, ForeachBlock, GivenStep, ReportFormat, Scenario, ScenarioBodyItem,
ScenarioSpan, SettingSpan, Span, StateCondition, Statement, TaskArg, TaskBodyItem, TaskCall,
TaskDef, TestCase, TestCaseSpan, TestSuite, TestSuiteSettings, ThenStep, Value, WhenStep,
};
use crate::parser::helpers::{
substitute_string, substitute_variables_in_given_step, substitute_variables_in_test_case,
substitute_variables_in_then_step, substitute_variables_in_when_step,
};
use pest::iterators::{Pair, Pairs};
use pest::Parser;
use pest_derive::Parser;
use std::collections::HashMap;
#[derive(Parser, Debug)]
#[grammar = "parser/choreo.pest"]
pub struct ChoreoParser;
pub fn parse(source: &str) -> Result<TestSuite, pest::error::Error<Rule>> {
let pairs = ChoreoParser::parse(Rule::grammar, source)?.next().unwrap();
let mut statements = vec![];
for statement_pair in pairs.into_inner() {
if statement_pair.as_rule() == Rule::EOI {
break;
}
let pair = statement_pair.into_inner().next().unwrap();
statements.push(match pair.as_rule() {
Rule::actors_def => build_actors_def(pair),
Rule::settings_def => build_settings_def(pair),
Rule::env_def => build_env_def(pair),
Rule::var_def => build_var_def(pair),
Rule::feature_def => build_feature_def(pair),
Rule::scenario_def => build_scenario(pair),
Rule::background_def => build_background_def(pair),
Rule::task_def => build_task_def(pair),
_ => unimplemented!("Parser rule not handled: {:?}", pair.as_rule()),
});
}
Ok(TestSuite { statements })
}
fn build_background_def(pair: Pair<Rule>) -> Statement {
let steps = build_given_steps(pair.into_inner());
Statement::BackgroundDef(steps)
}
fn build_actors_def(pair: Pair<Rule>) -> Statement {
let identifiers: Vec<String> = pair
.into_inner()
.filter(|p| p.as_rule() == Rule::identifier)
.map(|p| p.as_str().to_string())
.collect();
Statement::ActorDef(identifiers)
}
fn build_settings_def(pair: Pair<Rule>) -> Statement {
let span = pair.as_span();
let mut settings = TestSuiteSettings::default();
let mut setting_spans = SettingSpan {
timeout_seconds_span: None,
report_path_span: None,
report_format_span: None,
shell_path_span: None,
stop_on_failure_span: None,
expected_failures_span: None,
};
settings.span = Some(Span {
start: span.start(),
end: span.end(),
line: span.start_pos().line_col().0,
column: span.start_pos().line_col().1,
});
for setting_pair in pair.into_inner() {
let setting_span = setting_pair.as_span();
let mut inner = setting_pair.into_inner();
let key = inner.next().unwrap().as_str();
let value_pair = inner.next().unwrap();
let span_info = Span {
start: setting_span.start(),
end: setting_span.end(),
line: setting_span.start_pos().line_col().0,
column: setting_span.start_pos().line_col().1,
};
match key {
"timeout_seconds" => {
setting_spans.timeout_seconds_span = Some(span_info);
if let Value::Number(n) = build_value(value_pair) {
settings.timeout_seconds = n as u64;
} else {
panic!("'timeout_seconds' setting must be a number");
}
}
"report_path" => {
setting_spans.report_path_span = Some(span_info);
if let Value::String(s) = build_value(value_pair) {
if s.trim().is_empty() {
panic!(
"'report_path' setting cannot be an empty or whitespace-only string."
);
}
settings.report_path = s;
} else {
panic!("'report_path' setting must be a string");
}
}
"report_format" => {
setting_spans.report_format_span = Some(span_info);
if let Value::String(s) = build_value(value_pair) {
settings.report_format = match s.as_str() {
"json" => ReportFormat::Json,
"junit" => ReportFormat::Junit,
_ => panic!("Invalid 'report_format': must be 'json' or 'junit'"),
};
} else {
panic!("'report_format' setting must be a string");
}
}
"stop_on_failure" => {
setting_spans.stop_on_failure_span = Some(span_info);
if let Value::Bool(b) = build_value(value_pair) {
println!("Value: {}", b);
settings.stop_on_failure = Some(b).is_some();
} else {
panic!("'stop_on_failure' setting must be a boolean (true/false)");
}
}
"shell_path" => {
setting_spans.shell_path_span = Some(span_info);
if let Value::String(s) = build_value(value_pair) {
if s.trim().is_empty() {
panic!(
"'shell_path' setting cannot be an empty or whitespace-only string."
);
}
settings.shell_path = Some(s);
} else {
panic!("'shell_path' setting must be a string");
}
}
"expected_failures" => {
setting_spans.expected_failures_span = Some(span_info);
if let Value::Number(n) = build_value(value_pair) {
settings.expected_failures = n as usize;
} else {
panic!("'expected_failures' setting must be a number");
}
}
_ => { }
}
}
settings.setting_spans = Some(setting_spans);
Statement::SettingsDef(settings)
}
fn build_var_def(pair: Pair<Rule>) -> Statement {
let mut inner = pair.into_inner();
let key = inner.next().unwrap().as_str().to_string();
let value_pair = inner.next().unwrap();
let value = build_value(value_pair);
Statement::VarDef(key, value)
}
fn build_feature_def(pair: Pair<Rule>) -> Statement {
let name = pair
.into_inner()
.next()
.unwrap()
.into_inner()
.next()
.unwrap()
.as_str()
.to_string();
Statement::FeatureDef(name)
}
fn build_env_def(pair: Pair<Rule>) -> Statement {
let identifiers: Vec<String> = pair.into_inner().map(|p| p.as_str().to_string()).collect();
Statement::EnvDef(identifiers)
}
fn build_task_def(pair: Pair<Rule>) -> Statement {
let span = pair.as_span();
let mut inner = pair.into_inner();
let name = inner.next().unwrap().as_str().to_string();
let mut parameters = Vec::new();
let mut body = Vec::new();
for item in inner {
match item.as_rule() {
Rule::task_param_list => {
parameters = item.into_inner().map(|p| p.as_str().to_string()).collect();
}
Rule::task_body_item => {
let inner_item = item.into_inner().next().unwrap();
match inner_item.as_rule() {
Rule::action => {
let specific_action = inner_item.into_inner().next().unwrap();
body.push(TaskBodyItem::Action(build_action(specific_action)));
}
Rule::condition => {
body.push(TaskBodyItem::Condition(build_condition(inner_item)));
}
_ => {}
}
}
_ => {}
}
}
Statement::TaskDef(TaskDef {
name,
parameters,
body,
span: Some(Span {
start: span.start(),
end: span.end(),
line: span.start_pos().line_col().0,
column: span.start_pos().line_col().1,
}),
})
}
pub fn build_task_call(pair: Pair<Rule>) -> TaskCall {
let mut inner = pair.into_inner();
let name = inner.next().unwrap().as_str().to_string();
let mut arguments = Vec::new();
if let Some(arg_list) = inner.next() {
if arg_list.as_rule() == Rule::task_arg_list {
for arg in arg_list.into_inner() {
arguments.push(build_task_arg(arg));
}
}
}
TaskCall { name, arguments }
}
fn build_task_arg(pair: Pair<Rule>) -> TaskArg {
let inner = pair.into_inner().next().unwrap();
match inner.as_rule() {
Rule::wait_marker => {
let duration_str = inner.as_str();
let duration = if duration_str.ends_with("ms") {
let value_str = &duration_str[..duration_str.len() - 2];
value_str.parse::<f32>().unwrap_or(0.0) / 1000.0
} else if duration_str.ends_with('s') {
let value_str = &duration_str[..duration_str.len() - 1];
value_str.parse::<f32>().unwrap_or(0.0)
} else {
0.0
};
TaskArg::Duration(duration)
}
Rule::string => {
let s = inner.into_inner().next().unwrap().as_str();
TaskArg::String(unescape_string(s))
}
Rule::number => {
let n: i32 = inner.as_str().parse().unwrap_or(0);
TaskArg::Number(n)
}
Rule::variable_ref => TaskArg::VariableRef(inner.as_str().to_string()),
Rule::identifier => {
TaskArg::VariableRef(format!("${{{}}}", inner.as_str()))
}
_ => TaskArg::String(inner.as_str().to_string()),
}
}
#[allow(dead_code)]
fn build_actions(pairs: Pairs<Rule>) -> Vec<Action> {
pairs
.map(|pair| {
let inner_action = pair.into_inner().next().unwrap();
build_action(inner_action)
})
.collect()
}
fn build_scenario(pair: Pair<Rule>) -> Statement {
let span = pair.as_span();
let mut inner = pair.into_inner();
let mut scenario = Scenario::default();
let mut scenario_spans = ScenarioSpan {
name_span: None,
tests_span: None,
after_span: None,
};
scenario.span = Some(Span {
start: span.start(),
end: span.end(),
line: span.start_pos().line_col().0,
column: span.start_pos().line_col().1,
});
if let Some(token) = inner.peek() {
if token.as_rule() == Rule::parallel_keyword {
scenario.parallel = true;
inner.next();
}
}
let name_pair = inner.next().unwrap();
scenario_spans.name_span = Some(Span {
start: name_pair.as_span().start(),
end: name_pair.as_span().end(),
line: name_pair.as_span().start_pos().line_col().0,
column: name_pair.as_span().start_pos().line_col().1,
});
scenario.name = unescape_string(name_pair.into_inner().next().unwrap().as_str());
let mut body_items = Vec::new();
for item in inner {
let item_span = item.as_span();
let span_info = Span {
start: item_span.start(),
end: item_span.end(),
line: item_span.start_pos().line_col().0,
column: item_span.start_pos().line_col().1,
};
match item.as_rule() {
Rule::scenario_body => {
for body_item in item.into_inner() {
for scenario_body_item in body_item.into_inner() {
match scenario_body_item.as_rule() {
Rule::test => {
let test_case = build_test_case(scenario_body_item);
body_items.push(ScenarioBodyItem::Test(test_case.clone()));
scenario.tests.push(test_case); }
Rule::foreach_block => {
let foreach_block = build_foreach_block(scenario_body_item);
body_items.push(ScenarioBodyItem::Foreach(foreach_block));
}
_ => {}
}
}
}
}
Rule::test => {
let test_case = build_test_case(item);
body_items.push(ScenarioBodyItem::Test(test_case.clone()));
scenario.tests.push(test_case);
}
Rule::after_block => {
scenario_spans.after_span = Some(span_info);
scenario.after = build_when_steps(item.into_inner());
}
_ => {}
}
}
scenario.body = body_items;
scenario.scenario_span = Some(scenario_spans);
Statement::Scenario(scenario)
}
pub fn build_test_case(pair: Pair<Rule>) -> TestCase {
let span = pair.as_span();
let mut inner = pair.into_inner();
let mut testcase_spans = TestCaseSpan {
name_span: None,
description_span: None,
given_span: None,
when_span: None,
then_span: None,
};
let name_pair = inner.next().unwrap();
let name = name_pair.as_str().to_string();
testcase_spans.name_span = Some(Span {
start: name_pair.as_span().start(),
end: name_pair.as_span().end(),
line: name_pair.as_span().start_pos().line_col().0,
column: name_pair.as_span().start_pos().line_col().1,
});
let description_pair = inner.next().unwrap();
let description_span = description_pair.as_span();
let description = description_pair
.into_inner()
.next()
.unwrap()
.as_str()
.to_string();
testcase_spans.description_span = Some(Span {
start: description_span.start(),
end: description_span.end(),
line: description_span.start_pos().line_col().0,
column: description_span.start_pos().line_col().1,
});
let given_block = inner.next().expect("Missing given block in test case");
let given_span = given_block.as_span();
testcase_spans.given_span = Some(Span {
start: given_span.start(),
end: given_span.end(),
line: given_span.start_pos().line_col().0,
column: given_span.start_pos().line_col().1,
});
let when_block = inner.next().expect("Missing when block in test case");
let when_span = when_block.as_span();
testcase_spans.when_span = Some(Span {
start: when_span.start(),
end: when_span.end(),
line: when_span.start_pos().line_col().0,
column: when_span.start_pos().line_col().1,
});
let then_block = inner.next().expect("Missing then block in test case");
let then_span = then_block.as_span();
testcase_spans.then_span = Some(Span {
start: then_span.start(),
end: then_span.end(),
line: then_span.start_pos().line_col().0,
column: then_span.start_pos().line_col().1,
});
TestCase {
name,
description,
given: build_given_steps(given_block.into_inner()),
when: build_when_steps(when_block.into_inner()),
then: build_then_steps(then_block.into_inner()),
span: Some(Span {
start: span.start(),
end: span.end(),
line: span.start_pos().line_col().0,
column: span.start_pos().line_col().1,
}),
testcase_spans: Some(testcase_spans),
}
}
fn build_foreach_block(pair: Pair<Rule>) -> ForeachBlock {
let mut inner = pair.into_inner();
let loop_variable = inner.next().unwrap().as_str().to_string();
let array_variable = inner.next().unwrap().as_str().to_string();
let tests: Vec<TestCase> = inner.map(|test_pair| build_test_case(test_pair)).collect();
ForeachBlock {
loop_variable,
array_variable,
tests,
}
}
pub fn expand_scenario_foreach_blocks(
s: &Scenario,
env_vars: &HashMap<String, String>,
) -> Scenario {
let mut expanded_body = Vec::new();
for item in &s.body {
match item {
ScenarioBodyItem::Test(test) => {
expanded_body.push(ScenarioBodyItem::Test(test.clone()));
}
ScenarioBodyItem::Foreach(foreach_block) => {
let array_var_name = &foreach_block.array_variable;
let array_value_opt = env_vars.get(array_var_name);
let array_value_str = match array_value_opt {
Some(v) => v,
None => {
expanded_body.push(ScenarioBodyItem::Foreach(foreach_block.clone()));
continue;
}
};
let items_json: Vec<serde_json::Value> =
match serde_json::from_str::<serde_json::Value>(array_value_str) {
Ok(serde_json::Value::Array(arr)) => arr,
Ok(single) => vec![single],
Err(_) => {
array_value_str
.split(',')
.map(|s| serde_json::Value::String(s.trim().to_string()))
.filter(|v| match v {
serde_json::Value::String(s) => !s.is_empty(),
_ => true,
})
.collect()
}
};
for element in items_json {
let item_value_str = match &element {
serde_json::Value::String(s) => s.clone(),
serde_json::Value::Number(n) => n.to_string(),
serde_json::Value::Bool(b) => b.to_string(),
serde_json::Value::Null => "null".to_string(),
other => serde_json::to_string(other).unwrap_or_else(|_| other.to_string()),
};
let mut loop_env = env_vars.clone();
loop_env.insert(foreach_block.loop_variable.clone(), item_value_str.clone());
loop_env.insert(
format!("${{{}}}", &foreach_block.loop_variable),
item_value_str.clone(),
);
for test in &foreach_block.tests {
let mut expanded_test = test.clone();
expanded_test.name = substitute_string(&test.name, &loop_env);
expanded_test.description = substitute_string(&test.description, &loop_env);
expanded_test.given = test
.given
.iter()
.map(|step| substitute_variables_in_given_step(step, &loop_env))
.collect();
expanded_test.when = test
.when
.iter()
.map(|step| substitute_variables_in_when_step(step, &loop_env))
.collect();
expanded_test.then = test
.then
.iter()
.map(|step| substitute_variables_in_then_step(step, &loop_env))
.collect();
expanded_body.push(ScenarioBodyItem::Test(expanded_test));
}
}
}
}
}
Scenario {
name: s.name.clone(),
tests: expanded_body
.iter()
.filter_map(|item| match item {
ScenarioBodyItem::Test(test) => Some(test.clone()),
_ => None,
})
.collect(),
body: expanded_body,
after: s.after.clone(),
parallel: s.parallel,
span: s.span.clone(),
scenario_span: s.scenario_span.clone(),
}
}
pub fn _expand_foreach_blocks(
scenario: &Scenario,
variables: &HashMap<String, String>,
) -> Vec<TestCase> {
let mut new_scenario = scenario.clone();
let mut expanded_tests = Vec::new();
expanded_tests.extend(new_scenario.tests.clone());
for item in &scenario.body {
match item {
ScenarioBodyItem::Test(test_case) => {
expanded_tests.push(test_case.clone());
}
ScenarioBodyItem::Foreach(foreach_block) => {
let array_var_name = foreach_block
.array_variable
.trim_start_matches("${")
.trim_end_matches('}');
if let Some(array_json_str) = variables.get(array_var_name) {
if let Ok(array_values) = serde_json::from_str::<Vec<String>>(array_json_str) {
for value in array_values {
for test_template in &foreach_block.tests {
let mut loop_vars = variables.clone();
loop_vars
.insert(foreach_block.loop_variable.clone(), value.clone());
let substituted_test =
substitute_variables_in_test_case(test_template, &loop_vars);
expanded_tests.push(substituted_test);
}
}
}
}
}
}
}
new_scenario.tests = expanded_tests;
new_scenario.tests
}
pub fn expand_foreach_blocks(
scenario: &Scenario,
variables: &HashMap<String, String>,
) -> Vec<TestCase> {
let mut expanded = scenario.tests.clone();
for item in &scenario.body {
match item {
ScenarioBodyItem::Test(t) => {
if !expanded.iter().any(|e| e.name == t.name) {
expanded.push(t.clone());
}
}
ScenarioBodyItem::Foreach(foreach_block) => {
let array_var_name = foreach_block
.array_variable
.trim_start_matches("${")
.trim_end_matches('}');
let Some(raw_json) = variables.get(array_var_name) else {
eprintln!(
"[foreach] Variable '{}' not found for loop '{}'",
array_var_name, foreach_block.loop_variable
);
continue;
};
let Ok(items) = serde_json::from_str::<Vec<serde_json::Value>>(raw_json) else {
eprintln!(
"[foreach] Variable '{}' is not a JSON array for loop '{}'",
array_var_name, foreach_block.loop_variable
);
continue;
};
for item_value in items {
let mut loop_scope = variables.clone();
let loop_var = foreach_block.loop_variable.clone();
loop_scope.insert(
loop_var.clone(),
item_value
.as_str()
.map(|s| s.to_string())
.unwrap_or_else(|| item_value.to_string()),
);
if let serde_json::Value::Object(obj) = &item_value {
for (k, v) in obj {
loop_scope.insert(
format!("{}.{}", loop_var, k),
v.as_str()
.map(|s| s.to_string())
.unwrap_or_else(|| v.to_string()),
);
}
}
for template in &foreach_block.tests {
let materialised = substitute_variables_in_test_case(template, &loop_scope);
let final_name = if expanded.iter().any(|t| t.name == materialised.name) {
format!("{} ({})", materialised.name, loop_scope[&loop_var])
} else {
materialised.name.clone()
};
let mut adjusted = materialised.clone();
adjusted.name = final_name;
expanded.push(adjusted);
}
}
}
}
}
expanded
}
pub fn build_given_steps(pairs: Pairs<Rule>) -> Vec<GivenStep> {
pairs
.map(|pair| {
match pair.as_rule() {
Rule::action => {
let specific_action = pair.into_inner().next().unwrap();
GivenStep::Action(build_action(specific_action))
}
Rule::condition => GivenStep::Condition(build_condition(pair)),
Rule::task_call => GivenStep::TaskCall(build_task_call(pair)),
_ => unreachable!("Unexpected rule in given block: {:?}", pair.as_rule()),
}
})
.collect()
}
pub fn build_when_steps(pairs: Pairs<Rule>) -> Vec<WhenStep> {
pairs
.map(|pair| match pair.as_rule() {
Rule::action => {
let specific_action = pair.into_inner().next().unwrap();
WhenStep::Action(build_action(specific_action))
}
Rule::task_call => WhenStep::TaskCall(build_task_call(pair)),
_ => unreachable!("Unexpected rule in when block: {:?}", pair.as_rule()),
})
.collect()
}
pub fn build_then_steps(pairs: Pairs<Rule>) -> Vec<ThenStep> {
pairs
.map(|pair| match pair.as_rule() {
Rule::condition => ThenStep::Condition(build_condition(pair)),
Rule::task_call => ThenStep::TaskCall(build_task_call(pair)),
_ => unreachable!("Unexpected rule in then block: {:?}", pair.as_rule()),
})
.collect()
}
pub fn build_condition_from_specific(inner_cond: Pair<Rule>) -> Condition {
match inner_cond.as_rule() {
Rule::wait_condition => {
let mut inner = inner_cond.into_inner();
let op = inner.next().unwrap().as_str().to_string();
let wait_marker_str = inner.next().unwrap().as_str();
let wait = if wait_marker_str.ends_with("ms") {
let value_str = &wait_marker_str[..wait_marker_str.len() - 2];
value_str.parse::<f32>().unwrap() / 1000.0
} else if wait_marker_str.ends_with('s') {
let value_str = &wait_marker_str[..wait_marker_str.len() - 1];
value_str.parse::<f32>().unwrap()
} else {
0.0
};
Condition::Wait { op, wait }
}
Rule::terminal_condition => {
let mut inner = inner_cond.into_inner();
let terminal_cond = inner.next().unwrap();
build_condition_from_specific(terminal_cond)
}
Rule::output_contains_condition => {
let mut inner = inner_cond.into_inner();
let actor = "Terminal".to_string(); let text = inner
.next()
.unwrap()
.into_inner()
.next()
.unwrap()
.as_str()
.to_string();
Condition::OutputContains { actor, text }
}
Rule::output_not_contains_condition => {
let mut inner = inner_cond.into_inner();
let actor = "Terminal".to_string();
let text = inner
.next()
.unwrap()
.into_inner()
.next()
.unwrap()
.as_str()
.to_string();
Condition::OutputNotContains { actor, text }
}
Rule::state_condition => {
let mut inner = inner_cond.into_inner();
if let Some(first_token) = inner.next() {
match first_token.as_rule() {
Rule::state_can_start => Condition::State(StateCondition::CanStart),
Rule::state_has_succeeded => {
let mut parts = first_token.into_inner();
let test_name = parts
.next()
.expect("Missing test name after has_succeeded")
.as_str()
.to_string();
Condition::State(StateCondition::HasSucceeded(test_name))
}
Rule::identifier => {
let test_name = first_token.as_str().to_string();
Condition::State(StateCondition::HasSucceeded(test_name))
}
other => panic!("Unexpected rule in state_condition: {:?}", other),
}
} else {
panic!("Empty state_condition tokens");
}
}
Rule::output_matches_condition => {
let mut inner = inner_cond.into_inner();
let regex = inner
.next()
.unwrap()
.into_inner()
.next()
.unwrap()
.as_str()
.to_string();
let capture_as = inner.next().map(|p| p.as_str().to_string());
Condition::OutputMatches {
actor: "Terminal".to_string(),
regex,
capture_as,
}
}
Rule::last_command_succeeded_cond => Condition::LastCommandSucceeded,
Rule::last_command_failed_cond => Condition::LastCommandFailed,
Rule::last_command_exit_code_is_cond => {
let mut inner = inner_cond.into_inner();
let code_str = inner.next().unwrap().as_str();
let code: i32 = code_str.parse().unwrap();
Condition::LastCommandExitCodeIs(code)
}
Rule::output_is_valid_json_condition => Condition::OutputIsValidJson,
Rule::json_output_has_path_condition => {
let mut inner = inner_cond.into_inner();
let path = inner
.next()
.unwrap()
.into_inner()
.next()
.unwrap()
.as_str()
.to_string();
Condition::JsonOutputHasPath { path }
}
Rule::json_output_at_equals_condition => {
let mut inner = inner_cond.into_inner();
let path = inner.next().unwrap().as_str().to_string();
let value = build_value(inner.next().unwrap());
Condition::JsonOutputAtEquals { path, value }
}
Rule::json_output_at_includes_condition => {
let mut inner = inner_cond.into_inner();
let path = inner.next().unwrap().as_str().to_string();
let value = build_value(inner.next().unwrap());
Condition::JsonOutputAtIncludes { path, value }
}
Rule::json_output_at_has_item_count_condition => {
let mut inner = inner_cond.into_inner();
let path = inner.next().unwrap().as_str().to_string();
let count_str = inner.next().unwrap().as_str();
let count: i32 = count_str.parse().unwrap();
Condition::JsonOutputAtHasItemCount { path, count }
}
Rule::file_is_empty_condition => {
let mut inner = inner_cond.into_inner();
let path = unescape_string(inner.next().unwrap().into_inner().next().unwrap().as_str());
Condition::FileIsEmpty { path }
}
Rule::file_is_not_empty_condition => {
let mut inner = inner_cond.into_inner();
let path = unescape_string(inner.next().unwrap().into_inner().next().unwrap().as_str());
Condition::FileIsNotEmpty { path }
}
Rule::filesystem_condition => {
let mut inner = inner_cond.into_inner();
let next_pair = inner.next().unwrap();
match next_pair.as_rule() {
Rule::file_is_empty_condition => build_condition_from_specific(next_pair),
Rule::file_is_not_empty_condition => build_condition_from_specific(next_pair),
_ => {
let keyword = next_pair.as_str();
let path = inner
.next()
.unwrap()
.into_inner()
.next()
.unwrap()
.as_str()
.to_string();
match keyword {
"file_exists" => Condition::FileExists { path },
"file_does_not_exist" => Condition::FileDoesNotExist { path },
"dir_exists" => Condition::DirExists { path },
"dir_does_not_exist" => Condition::DirDoesNotExist { path },
"file_contains" => {
let content = inner
.next()
.unwrap()
.into_inner()
.next()
.unwrap()
.as_str()
.to_string();
Condition::FileContains { path, content }
}
_ => unreachable!("Unsupported filesystem condition keyword: {}", keyword),
}
}
}
}
Rule::stdout_is_empty_condition => Condition::StdoutIsEmpty,
Rule::stderr_is_empty_condition => Condition::StderrIsEmpty,
Rule::stderr_contains_condition => {
let text = unescape_string(
inner_cond
.into_inner()
.next()
.unwrap()
.into_inner()
.next()
.unwrap()
.as_str(),
);
Condition::StderrContains(text)
}
Rule::output_starts_with_condition => {
let text = unescape_string(
inner_cond
.into_inner()
.next()
.unwrap()
.into_inner()
.next()
.unwrap()
.as_str(),
);
Condition::OutputStartsWith(text)
}
Rule::output_ends_with_condition => {
let text = unescape_string(
inner_cond
.into_inner()
.next()
.unwrap()
.into_inner()
.next()
.unwrap()
.as_str(),
);
Condition::OutputEndsWith(text)
}
Rule::output_equals_condition => {
let text = unescape_string(
inner_cond
.into_inner()
.next()
.unwrap()
.into_inner()
.next()
.unwrap()
.as_str(),
);
Condition::OutputEquals(text)
}
Rule::web_condition => {
let inner = inner_cond.into_inner().next().unwrap();
build_condition_from_specific(inner)
}
Rule::response_status_is_condition => {
let status = inner_cond
.into_inner()
.next()
.unwrap()
.as_str()
.parse()
.unwrap();
Condition::ResponseStatusIs(status)
}
Rule::response_status_is_success_condition => Condition::ResponseStatusIsSuccess,
Rule::response_status_is_error_condition => Condition::ResponseStatusIsError,
Rule::response_status_is_in_condition => {
let statuses: Vec<u16> = inner_cond
.into_inner()
.filter(|p| p.as_rule() == Rule::number)
.map(|p| p.as_str().parse().unwrap())
.collect();
Condition::ResponseStatusIsIn(statuses)
}
Rule::response_time_is_below_condition => {
let mut inner = inner_cond.into_inner();
let duration_marker_str = inner.next().unwrap().as_str();
let duration = if duration_marker_str.ends_with("ms") {
let value_str = &duration_marker_str[..duration_marker_str.len() - 2];
value_str.parse::<f32>().unwrap() / 1000.0
} else if duration_marker_str.ends_with('s') {
let value_str = &duration_marker_str[..duration_marker_str.len() - 1];
value_str.parse::<f32>().unwrap()
} else {
0.0
};
Condition::ResponseTimeIsBelow { duration }
}
Rule::response_body_contains_condition => {
let value = inner_cond
.into_inner()
.next()
.unwrap()
.into_inner()
.next()
.unwrap()
.as_str()
.to_string();
Condition::ResponseBodyContains { value }
}
Rule::response_body_matches_condition => {
let mut inner = inner_cond.into_inner();
let regex_str = inner.next().unwrap().into_inner().next().unwrap().as_str();
let regex = unescape_string(regex_str);
let capture_as = inner.next().map(|p| p.as_str().to_string());
Condition::ResponseBodyMatches { regex, capture_as }
}
Rule::response_body_equals_json => {
let mut inner = inner_cond.into_inner();
let expected = inner.next().unwrap().as_str().trim_matches('"').to_string();
let mut ignored = Vec::new();
if let Some(ignored_pair) = inner.next() {
let mut ignored_inner = ignored_pair.into_inner();
if let Some(first) = ignored_inner.next() {
ignored.push(unescape_string(first.as_str()));
for s in ignored_inner {
ignored.push(unescape_string(s.as_str()));
}
}
}
Condition::ResponseBodyEqualsJson { expected, ignored }
}
Rule::json_value_is_string_condition => {
let path = inner_cond
.into_inner()
.next()
.unwrap()
.into_inner()
.next()
.unwrap()
.as_str()
.to_string();
Condition::JsonValueIsString { path }
}
Rule::json_value_is_number_condition => {
let path = inner_cond
.into_inner()
.next()
.unwrap()
.into_inner()
.next()
.unwrap()
.as_str()
.to_string();
Condition::JsonValueIsNumber { path }
}
Rule::json_value_is_array_condition => {
let path = inner_cond
.into_inner()
.next()
.unwrap()
.into_inner()
.next()
.unwrap()
.as_str()
.to_string();
Condition::JsonValueIsArray { path }
}
Rule::json_value_is_object_condition => {
let path = inner_cond
.into_inner()
.next()
.unwrap()
.into_inner()
.next()
.unwrap()
.as_str()
.to_string();
Condition::JsonValueIsObject { path }
}
Rule::json_value_has_size_condition => {
let mut inner = inner_cond.into_inner();
let path = inner
.next()
.unwrap()
.into_inner()
.next()
.unwrap()
.as_str()
.to_string();
let size_str = inner.next().unwrap().as_str();
let size: usize = size_str.parse().unwrap();
Condition::JsonValueHasSize { path, size }
}
Rule::json_body_has_path_condition => {
let path = inner_cond
.into_inner()
.next()
.unwrap()
.into_inner()
.next()
.unwrap()
.as_str()
.to_string();
Condition::JsonBodyHasPath { path }
}
Rule::json_path_equals_condition => {
let mut inner = inner_cond.into_inner();
let path = inner
.next()
.unwrap()
.into_inner()
.next()
.unwrap()
.as_str()
.to_string();
let expected_value = build_value(inner.next().unwrap());
Condition::JsonPathEquals {
path,
expected_value,
}
}
Rule::json_path_capture_condition => {
let mut inner = inner_cond.into_inner();
let path = inner
.next()
.unwrap()
.into_inner()
.next()
.unwrap()
.as_str()
.to_string();
let capture_as = inner.next().map(|p| p.as_str().to_string()).unwrap();
Condition::JsonPathCapture { path, capture_as }
}
Rule::system_condition => {
let inner = inner_cond.into_inner().next().unwrap();
build_condition_from_specific(inner)
}
Rule::service_is_running_condition => {
let name = unescape_string(
inner_cond
.into_inner()
.next()
.unwrap()
.into_inner()
.next()
.unwrap()
.as_str(),
);
Condition::ServiceIsRunning { name }
}
Rule::service_is_stopped_condition => {
let name = unescape_string(
inner_cond
.into_inner()
.next()
.unwrap()
.into_inner()
.next()
.unwrap()
.as_str(),
);
Condition::ServiceIsStopped { name }
}
Rule::service_is_installed_condition => {
let name = unescape_string(
inner_cond
.into_inner()
.next()
.unwrap()
.into_inner()
.next()
.unwrap()
.as_str(),
);
Condition::ServiceIsInstalled { name }
}
Rule::port_is_listening_condition => {
let port: u16 = inner_cond
.into_inner()
.next()
.unwrap()
.as_str()
.parse()
.unwrap();
Condition::PortIsListening { port }
}
Rule::port_is_closed_condition => {
let port: u16 = inner_cond
.into_inner()
.next()
.unwrap()
.as_str()
.parse()
.unwrap();
Condition::PortIsClosed { port }
}
_ => unreachable!("Unhandled condition: {:?}", inner_cond.as_rule()),
}
}
pub fn build_condition(pair: Pair<Rule>) -> Condition {
let inner_cond = pair.into_inner().next().unwrap();
build_condition_from_specific(inner_cond)
}
#[allow(dead_code)]
fn build_conditions(pairs: Pairs<Rule>) -> Vec<Condition> {
pairs.map(build_condition).collect()
}
pub fn build_action(inner_action: Pair<Rule>) -> Action {
match inner_action.as_rule() {
Rule::run_action => {
let mut inner = inner_action.into_inner();
let command = inner.next().unwrap().into_inner().next().unwrap().as_str();
let command = unescape_string(command);
Action::Run {
actor: "Terminal".to_string(),
command,
}
}
Rule::set_cwd_action => {
let mut inner = inner_action.into_inner();
let path = inner.next().unwrap().into_inner().next().unwrap().as_str();
let path = unescape_string(path);
Action::SetCwd { path }
}
Rule::system_action => {
let mut inner = inner_action.into_inner();
let action_type_pair = inner.next().unwrap(); let specific = action_type_pair
.into_inner()
.next()
.expect("Missing specific system action");
match specific.as_rule() {
Rule::system_log => {
let mut action_inner = specific.into_inner();
let message_pair = action_inner.next().unwrap();
let message =
unescape_string(message_pair.into_inner().next().unwrap().as_str());
Action::Log { message }
}
Rule::system_pause => {
let mut action_inner = specific.into_inner();
let duration_marker = action_inner.next().unwrap().as_str();
let duration = parse_duration(duration_marker);
Action::Pause { duration }
}
Rule::system_timestamp => {
let mut action_inner = specific.into_inner();
let var_pair = action_inner.next().unwrap();
let var_name = if var_pair.as_rule() == Rule::string {
unescape_string(var_pair.into_inner().next().unwrap().as_str())
} else {
var_pair.as_str().to_string()
};
Action::Timestamp { variable: var_name }
}
Rule::system_uuid => {
let mut action_inner = specific.into_inner();
let var_pair = action_inner.next().unwrap();
let var_name = var_pair.as_str().to_string();
Action::Uuid { variable: var_name }
}
_ => unreachable!("Unhandled system_action type: {:?}", specific.as_str()),
}
}
Rule::filesystem_action => {
let mut inner = inner_action.into_inner();
let keyword = inner.next().unwrap().as_str();
let path = inner
.next()
.unwrap()
.into_inner()
.next()
.map_or(String::new(), |p| p.as_str().to_string());
match keyword {
"create_dir" => Action::CreateDir { path },
"delete_file" => Action::DeleteFile { path },
"delete_dir" => Action::DeleteDir { path },
"create_file" => {
let content = if let Some(content_pair) = inner.next() {
content_pair
.into_inner()
.next()
.map_or(String::new(), |p| p.as_str().to_string())
} else {
String::new()
};
Action::CreateFile { path, content }
}
"read_file" => {
let variable = inner.next().map(|p| p.as_str().to_string());
Action::ReadFile { path, variable }
}
_ => unreachable!(),
}
}
Rule::web_action => {
let mut inner = inner_action.into_inner();
let action_type = inner.next().unwrap();
let action_type_str = action_type.as_str();
let mut action_inner = action_type.into_inner();
let method = action_type_str.split_whitespace().next().unwrap_or("");
match method {
"set_header" => {
let key = action_inner
.next()
.unwrap()
.into_inner()
.next()
.unwrap()
.as_str()
.to_string();
let value = action_inner
.next()
.unwrap()
.into_inner()
.next()
.unwrap()
.as_str()
.to_string();
Action::HttpSetHeader { key, value }
}
"clear_header" => {
let key = action_inner
.next()
.unwrap()
.into_inner()
.next()
.unwrap()
.as_str()
.to_string();
Action::HttpClearHeader { key }
}
"clear_headers" => Action::HttpClearHeaders,
"set_cookie" => {
let key = action_inner
.next()
.unwrap()
.into_inner()
.next()
.unwrap()
.as_str()
.to_string();
let value = action_inner
.next()
.unwrap()
.into_inner()
.next()
.unwrap()
.as_str()
.to_string();
Action::HttpSetCookie { key, value }
}
"clear_cookie" => {
let key = action_inner
.next()
.unwrap()
.into_inner()
.next()
.unwrap()
.as_str()
.to_string();
Action::HttpClearCookie { key }
}
"clear_cookies" => Action::HttpClearCookies,
"http_get" => {
let url = action_inner
.next()
.unwrap()
.into_inner()
.next()
.unwrap()
.as_str()
.to_string();
Action::HttpGet { url }
}
"http_post" => {
let url = action_inner
.next()
.unwrap()
.into_inner()
.next()
.unwrap()
.as_str()
.to_string();
let body = unescape_string(
action_inner
.next()
.unwrap()
.into_inner()
.next()
.unwrap()
.as_str(),
);
Action::HttpPost { url, body }
}
"http_put" => {
let url = action_inner
.next()
.unwrap()
.into_inner()
.next()
.unwrap()
.as_str()
.to_string();
let body = unescape_string(
action_inner
.next()
.unwrap()
.into_inner()
.next()
.unwrap()
.as_str(),
);
Action::HttpPut { url, body }
}
"http_patch" => {
let url = action_inner
.next()
.unwrap()
.into_inner()
.next()
.unwrap()
.as_str()
.to_string();
let body = unescape_string(
action_inner
.next()
.unwrap()
.into_inner()
.next()
.unwrap()
.as_str(),
);
Action::HttpPatch { url, body }
}
"http_delete" => {
let url = action_inner
.next()
.unwrap()
.into_inner()
.next()
.unwrap()
.as_str()
.to_string();
Action::HttpDelete { url }
}
_ => panic!("Unknown action method: {}", method),
}
}
_ => unreachable!("Unhandled action: {:?}", inner_action.as_rule()),
}
}
fn build_value(pair: Pair<Rule>) -> Value {
let inner_pair = match pair.clone().into_inner().next() {
Some(p) => p,
None => pair.clone(),
};
match inner_pair.as_rule() {
Rule::string => {
let inner = inner_pair.into_inner().next().unwrap();
Value::String(unescape_string(inner.as_str()))
}
Rule::number => Value::Number(inner_pair.as_str().parse().unwrap()),
Rule::identifier => {
let var_name = pair.as_str();
Value::String(format!("${{{}}}", var_name))
}
Rule::binary_op => Value::Bool(inner_pair.as_str().parse().unwrap()),
Rule::array => {
let elements: Vec<Value> = inner_pair
.into_inner()
.map(|element_pair| build_value(element_pair))
.collect();
Value::Array(elements)
}
Rule::object => {
let mut map: HashMap<String, Value> = HashMap::new();
for entry in inner_pair.into_inner() {
let mut kv = entry.into_inner();
let key_pair = kv.next().unwrap();
let value_pair = kv.next().unwrap();
let key = if key_pair.as_rule() == Rule::string {
unescape_string(key_pair.into_inner().next().unwrap().as_str())
} else {
key_pair.as_str().to_string()
};
let val = build_value(value_pair);
map.insert(key, val);
}
Value::Object(map)
}
_ => {
if pair.as_rule() == Rule::binary_op {
Value::Bool(pair.as_str().parse().unwrap())
} else {
unreachable!("Unexpected value rule: {:?}", pair.as_rule())
}
}
}
}
fn parse_duration(duration_str: &str) -> f32 {
if duration_str.ends_with("ms") {
let num_part = &duration_str[..duration_str.len() - 2];
num_part.parse::<f32>().unwrap_or(0.0) / 1000.0
} else if duration_str.ends_with("s") {
let num_part = &duration_str[..duration_str.len() - 1];
num_part.parse::<f32>().unwrap_or(0.0)
} else {
0.0
}
}
pub fn unescape_string(s: &str) -> String {
s.replace("\\\"", "\"")
.replace("\\'", "'")
.replace("\\n", "\n")
.replace("\\t", "\t")
.replace("\\r", "\r")
.replace("\\\\", "\\")
}