use std::fmt;
use std::path::{Path, PathBuf};
use super::{Issue, Plan, PlanError, Scenario, ScenarioError};
#[derive(Debug)]
pub struct ResolveError {
pub file: PathBuf,
pub cause: ResolveCause,
}
#[derive(Debug)]
pub enum ResolveCause {
Parse {
error: Box<toml::de::Error>,
text: String,
},
Scenario(ScenarioError),
Merged(Box<PlanError>),
Invalid(Vec<Issue>),
Unread(String),
Cycle,
}
impl ResolveError {
#[must_use]
pub const fn unread(file: PathBuf, reason: String) -> Self {
Self {
file,
cause: ResolveCause::Unread(reason),
}
}
fn parse(file: PathBuf, error: toml::de::Error, text: String) -> Self {
Self {
file,
cause: ResolveCause::Parse {
error: Box::new(error),
text,
},
}
}
fn scenario(file: PathBuf, error: ScenarioError, text: String) -> Self {
match error {
ScenarioError::Parse(error) => Self::parse(file, error, text),
other => Self {
file,
cause: ResolveCause::Scenario(other),
},
}
}
}
impl fmt::Display for ResolveError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let file = self.file.display();
match &self.cause {
ResolveCause::Parse { error, .. } => write!(f, "{file}: {error}"),
ResolveCause::Scenario(error) => write!(f, "{file}: {error}"),
ResolveCause::Merged(error) => write!(f, "{error}"),
ResolveCause::Invalid(issues) => {
let listing: Vec<String> = issues.iter().map(ToString::to_string).collect();
write!(f, "{file}: {}", listing.join("; "))
}
ResolveCause::Unread(reason) => f.write_str(reason),
ResolveCause::Cycle => write!(f, "{file}: scenario base chain forms a cycle"),
}
}
}
impl std::error::Error for ResolveError {}
pub fn resolve_plan(
start: PathBuf,
text: String,
read: &mut dyn FnMut(&Path) -> Result<String, String>,
locate: &mut dyn FnMut(&Path, &str) -> Result<PathBuf, String>,
) -> Result<Plan, ResolveError> {
let mut visited = vec![start.clone()];
let mut current = start;
let mut text = text;
let mut overlays = Vec::new();
loop {
match Scenario::from_toml_str(&text) {
Ok(Some(scenario)) => {
let next = locate(¤t, scenario.base()).map_err(|reason| {
ResolveError::unread(beside(¤t, scenario.base()), reason)
})?;
if visited.contains(&next) {
return Err(ResolveError {
file: next,
cause: ResolveCause::Cycle,
});
}
visited.push(next.clone());
overlays.push((current, scenario));
current = next;
text = read(¤t)
.map_err(|reason| ResolveError::unread(current.clone(), reason))?;
}
Ok(None) => break,
Err(error) => return Err(ResolveError::scenario(current, error, text)),
}
}
if overlays.is_empty() {
return toml::from_str(&text).map_err(|error| ResolveError::parse(current, error, text));
}
merged(current, text, overlays)
}
fn merged(
current: PathBuf,
text: String,
overlays: Vec<(PathBuf, Scenario)>,
) -> Result<Plan, ResolveError> {
let mut table: toml::Table =
toml::from_str(&text).map_err(|error| ResolveError::parse(current, error, text))?;
let opened = overlays[0].0.clone();
for (path, scenario) in overlays.into_iter().rev() {
table = scenario.apply(table).map_err(|issues| ResolveError {
file: path,
cause: ResolveCause::Invalid(issues),
})?;
}
Plan::from_toml_table(table).map_err(|error| ResolveError {
file: opened,
cause: ResolveCause::Merged(Box::new(error)),
})
}
fn beside(referrer: &Path, base: &str) -> PathBuf {
referrer.parent().unwrap_or(Path::new("")).join(base)
}