use std::collections::HashMap;
use super::flow::{ParamDecl, ParamKind};
use crate::i18n::{Strings, fill};
pub type ParamValues = HashMap<String, String>;
pub fn parse_assignment(arg: &str) -> Result<(String, String), String> {
let Some((name, value)) = arg.split_once('=') else {
return Err(format!(
"expected NAME=VALUE, got `{arg}` (no `=`; e.g. --param CASES_DIR=./cases)"
));
};
let name = name.trim();
if name.is_empty() {
return Err(format!("`{arg}` has no parameter name before the `=`"));
}
Ok((name.to_string(), value.to_string()))
}
pub fn undeclared<'a>(decls: &[&ParamDecl], chosen: &'a ParamValues) -> Vec<&'a str> {
let mut names: Vec<&str> = chosen
.keys()
.filter(|name| !decls.iter().any(|d| &&d.name == name))
.map(String::as_str)
.collect();
names.sort_unstable();
names
}
pub fn value_for(decl: &ParamDecl, chosen: &ParamValues, s: &Strings) -> Result<String, String> {
let value = match chosen.get(&decl.name) {
Some(v) => v.clone(),
None => match &decl.default {
Some(d) => d.clone(),
None => return Err(fill(s.param_required, &[&decl.prompt(), &decl.name])),
},
};
check(decl, &value, s)?;
Ok(value)
}
pub fn effective(decls: &[&ParamDecl], chosen: &ParamValues) -> ParamValues {
decls
.iter()
.filter_map(|d| {
let v = chosen.get(&d.name).or(d.default.as_ref())?;
Some((d.name.clone(), v.clone()))
})
.collect()
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ParamRow {
pub name: String,
pub prompt: String,
pub kind: ParamKind,
pub value: String,
pub problem: Option<String>,
}
pub fn rows(decls: &[&ParamDecl], chosen: &ParamValues, s: &Strings) -> Vec<ParamRow> {
decls
.iter()
.map(|p| {
let value = chosen
.get(&p.name)
.cloned()
.or_else(|| p.default.clone())
.unwrap_or_default();
ParamRow {
name: p.name.clone(),
prompt: p.prompt(),
kind: p.kind.clone(),
problem: check(p, &value, s).err().or_else(|| {
(value.trim().is_empty() && p.default.is_none())
.then(|| s.param_row_required.to_string())
}),
value,
}
})
.collect()
}
pub fn check(decl: &ParamDecl, value: &str, s: &Strings) -> Result<(), String> {
let trimmed = value.trim();
if trimmed.contains("{{") {
return Ok(());
}
match &decl.kind {
ParamKind::Choice(options) if !options.is_empty() => {
if !options.iter().any(|o| o == trimmed) {
return Err(fill(
s.param_not_a_choice,
&[&decl.prompt(), trimmed, &options.join(", ")],
));
}
}
ParamKind::Number => {
if trimmed.is_empty() || trimmed.parse::<f64>().is_err() {
return Err(fill(s.param_not_a_number, &[&decl.prompt(), trimmed]));
}
}
_ => {}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::report::parser::parse_flow;
fn decl(src: &str) -> ParamDecl {
let flow = parse_flow(&format!("{src}\n")).expect("parses");
match &flow.nodes[0] {
super::super::flow::FlowNode::Param(p) => p.clone(),
other => panic!("expected Param, got {other:?}"),
}
}
#[test]
fn an_assignment_keeps_everything_after_the_first_equals() {
assert_eq!(
parse_assignment("CASES_DIR=./batch-07"),
Ok(("CASES_DIR".to_string(), "./batch-07".to_string()))
);
assert_eq!(
parse_assignment("URL=https://x.test/?a=1&b=2"),
Ok(("URL".to_string(), "https://x.test/?a=1&b=2".to_string()))
);
assert_eq!(
parse_assignment(" CASES = /tmp/a b "),
Ok(("CASES".to_string(), " /tmp/a b ".to_string()))
);
assert_eq!(
parse_assignment("CASES="),
Ok(("CASES".to_string(), String::new()))
);
assert!(parse_assignment("CASES_DIR").is_err(), "no `=`");
assert!(parse_assignment("=/tmp/a").is_err(), "no name");
}
#[test]
fn undeclared_names_are_listed_and_declared_ones_are_not() {
let a = decl("PARAM FOLDER CASES = \"./x\"");
let b = decl("PARAM TEXT TICKET");
let decls = [&a, &b];
let mut chosen = ParamValues::new();
chosen.insert("CASES".into(), "./y".into());
assert!(undeclared(&decls, &chosen).is_empty());
chosen.insert("CASE_DIR".into(), "./y".into());
chosen.insert("AAA".into(), "1".into());
assert_eq!(undeclared(&decls, &chosen), vec!["AAA", "CASE_DIR"]);
}
#[test]
fn a_required_parameter_stops_the_run_rather_than_running_empty() {
let s = Strings::english();
let d = decl("PARAM TEXT TICKET_REF");
let err = value_for(&d, &ParamValues::new(), s).unwrap_err();
assert!(err.contains("Ticket ref"), "{err}");
let mut chosen = ParamValues::new();
chosen.insert("TICKET_REF".into(), "T-1".into());
assert_eq!(value_for(&d, &chosen, s).unwrap(), "T-1");
}
#[test]
fn a_chosen_value_is_held_to_the_same_rules_as_a_default() {
let s = Strings::english();
let d = decl("PARAM CHOICE(\"v4.2\", \"v4.3\") VERSION = \"v4.3\"");
let mut chosen = ParamValues::new();
chosen.insert("VERSION".into(), "v9".into());
let err = value_for(&d, &chosen, s).unwrap_err();
assert!(err.contains("v9") && err.contains("v4.2, v4.3"), "{err}");
let n = decl("PARAM NUMBER TRIES = \"3\"");
chosen.insert("TRIES".into(), "lots".into());
assert!(value_for(&n, &chosen, s).is_err());
chosen.insert("TRIES".into(), "5".into());
assert_eq!(value_for(&n, &chosen, s).unwrap(), "5");
}
#[test]
fn a_supplied_value_beats_the_declared_default() {
let s = Strings::english();
let d = decl("PARAM ENV TARGET = \"staging\"");
assert_eq!(value_for(&d, &ParamValues::new(), s).unwrap(), "staging");
let mut chosen = ParamValues::new();
chosen.insert("TARGET".into(), "prod".into());
assert_eq!(value_for(&d, &chosen, s).unwrap(), "prod");
}
}