use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use super::error::{RenderError, shape_word};
use super::template::{ArgumentValue, FillTemplate};
pub const END_OF_OPTIONS_MARKER: &str = "--";
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CommandParameterContract {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ArgvSlot {
pub fill: FillTemplate,
pub label: String,
pub admits_leading_dash: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CommandLineContract {
pub slots: Vec<ArgvSlot>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct EnvBindingContract {
pub name: String,
pub value: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct DeclaredCommandContract {
pub name: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub parameters: Vec<CommandParameterContract>,
pub lines: Vec<CommandLineContract>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub env: Vec<EnvBindingContract>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cwd: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prior_form_refusal: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RenderedCommand {
pub argv_lines: Vec<Vec<String>>,
pub env: Vec<(String, String)>,
pub cwd: Option<String>,
}
impl DeclaredCommandContract {
#[must_use]
pub fn parameter_names(&self) -> Vec<&str> {
self.parameters
.iter()
.map(|parameter| parameter.name.as_str())
.collect()
}
pub fn render(
&self,
supplied: &BTreeMap<String, ArgumentValue>,
) -> Result<RenderedCommand, RenderError> {
if let Some(construct) = &self.prior_form_refusal {
return Err(RenderError::PriorFormUnrenderable {
command: self.name.clone(),
construct: construct.clone(),
});
}
let bound = self.bind(supplied)?;
let mut argv_lines = Vec::with_capacity(self.lines.len());
for line in &self.lines {
let mut argv = Vec::with_capacity(line.slots.len());
for slot in &line.slots {
let element = resolve(&self.name, &slot.fill, &bound)?;
if !slot.admits_leading_dash && element.starts_with('-') {
return Err(RenderError::LeadingDashOperand {
command: self.name.clone(),
argument: slot.label.clone(),
element,
marker: END_OF_OPTIONS_MARKER,
});
}
argv.push(element);
}
argv_lines.push(argv);
}
Ok(RenderedCommand {
argv_lines,
env: self
.env
.iter()
.map(|binding| (binding.name.clone(), binding.value.clone()))
.collect(),
cwd: self.cwd.clone(),
})
}
fn bind(
&self,
supplied: &BTreeMap<String, ArgumentValue>,
) -> Result<BTreeMap<String, String>, RenderError> {
for name in supplied.keys() {
if !self.parameters.iter().any(|item| item.name == *name) {
return Err(RenderError::ArgumentUndeclared {
command: self.name.clone(),
parameter: name.clone(),
});
}
}
let mut bound: BTreeMap<String, String> = BTreeMap::new();
for parameter in &self.parameters {
let value = match supplied.get(¶meter.name) {
Some(ArgumentValue::Scalar(value)) => value.clone(),
Some(value @ ArgumentValue::List(_)) => {
return Err(RenderError::ArgumentTypeMismatch {
command: self.name.clone(),
parameter: parameter.name.clone(),
observed: value.describe(),
declared: shape_word(false),
supplied: shape_word(true),
});
}
None => {
let Some(default) = ¶meter.default else {
return Err(RenderError::ArgumentMissing {
command: self.name.clone(),
parameter: parameter.name.clone(),
});
};
default.clone()
}
};
bound.insert(parameter.name.clone(), value);
}
Ok(bound)
}
}
fn resolve(
command: &str,
fill: &FillTemplate,
bound: &BTreeMap<String, String>,
) -> Result<String, RenderError> {
let mut element = String::new();
for piece in &fill.pieces {
match piece {
super::template::FillPiece::Literal { text } => element.push_str(text),
super::template::FillPiece::Hole { parameter } => {
let Some(value) = bound.get(parameter.as_str()) else {
return Err(RenderError::UnboundHole {
command: command.to_owned(),
parameter: parameter.clone(),
});
};
element.push_str(value);
}
}
}
Ok(element)
}