use super::super::Spec;
use super::super::placeholder::{self, Place, PortField, Reference};
use super::super::problem::{Code, FieldPath, Problem, Problems};
use crate::Error;
pub(super) fn check(spec: &Spec, out: &mut Problems) {
let Some(message) = &spec.message else { return };
let at = FieldPath::new("message");
for found in placeholder_problems(spec, None, message, Place::Message) {
let code = if found.unclosed { Code::InvalidValue } else { Code::UnknownReference };
let p = Problem::new(code, at.clone(), found.reason);
out.push(if found.unclosed { p } else { p.fix(example(spec)) });
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Unresolved {
pub reason: String,
pub unclosed: bool,
}
#[must_use]
pub fn placeholder_problems(spec: &Spec, machine: Option<&str>, text: &str, place: Place) -> Vec<Unresolved> {
placeholder::placeholders(text, place)
.into_iter()
.filter_map(|found| match found {
Ok(r) => resolve(spec, machine, &r).err().map(|why| Unresolved {
reason: format!("`{{{{ {r} }}}}`: {why}"),
unclosed: false,
}),
Err(e) => Some(Unresolved {
unclosed: matches!(e, Error::Unclosed),
reason: e.to_string(),
}),
})
.collect()
}
fn resolve(spec: &Spec, template_of: Option<&str>, r: &Reference) -> Result<(), String> {
let on = |m: &str, n: &str| -> Result<(), String> {
let found = spec.machines.get(m).ok_or_else(|| format!("no machine named `{m}`"))?;
if found.networks.contains_key(n) {
Ok(())
} else {
Err(format!("`{m}` isn't on a network named `{n}`"))
}
};
match r {
Reference::OwnAddress { network } => on(template_of.unwrap_or_default(), network),
Reference::Address { machine, network } => on(machine, network),
Reference::Port { machine, port, field } => {
let m = spec.machines.get(machine).ok_or_else(|| format!("no machine named `{machine}`"))?;
let p = m
.ports
.get(port)
.ok_or_else(|| format!("`{machine}` has no port named `{port}` (ports are named, never numbered)"))?;
if *field == PortField::Url && !p.protocol.is_http() {
return Err(format!("`{port}` isn't an `http` or `https` port, so it has no URL"));
}
Ok(())
}
Reference::CloudOutput(name) => match &spec.cloud {
Some(c) if c.outputs.contains_key(name) => Ok(()),
Some(_) => Err(format!("`cloud.outputs` has no `{name}`")),
None => Err("the spec has no `cloud:`".into()),
},
_ => Ok(()),
}
}
fn example(spec: &Spec) -> String {
let named = spec
.machines
.iter()
.find_map(|(m, machine)| machine.ports.keys().next().map(|p| format!("`{{{{ machines.{m}.ports.{p}.published }}}}`")));
named.unwrap_or_else(|| "`{{ machines.<machine>.ports.<name>.published }}` or `{{ machines.<machine>.addresses.<network> }}`".into())
}