isoloom_spec/syntax/check/
messages.rs1use super::super::Spec;
9use super::super::placeholder::{self, Place, PortField, Reference};
10use super::super::problem::{Code, FieldPath, Problem, Problems};
11use crate::Error;
12
13pub(super) fn check(spec: &Spec, out: &mut Problems) {
15 let Some(message) = &spec.message else { return };
16 let at = FieldPath::new("message");
17 for found in placeholder_problems(spec, None, message, Place::Message) {
18 let code = if found.unclosed { Code::InvalidValue } else { Code::UnknownReference };
19 let p = Problem::new(code, at.clone(), found.reason);
20 out.push(if found.unclosed { p } else { p.fix(example(spec)) });
21 }
22}
23
24#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct Unresolved {
27 pub reason: String,
29 pub unclosed: bool,
31}
32
33#[must_use]
36pub fn placeholder_problems(spec: &Spec, machine: Option<&str>, text: &str, place: Place) -> Vec<Unresolved> {
37 placeholder::placeholders(text, place)
38 .into_iter()
39 .filter_map(|found| match found {
40 Ok(r) => resolve(spec, machine, &r).err().map(|why| Unresolved {
41 reason: format!("`{{{{ {r} }}}}`: {why}"),
42 unclosed: false,
43 }),
44 Err(e) => Some(Unresolved {
45 unclosed: matches!(e, Error::Unclosed),
46 reason: e.to_string(),
47 }),
48 })
49 .collect()
50}
51
52fn resolve(spec: &Spec, template_of: Option<&str>, r: &Reference) -> Result<(), String> {
55 let on = |m: &str, n: &str| -> Result<(), String> {
56 let found = spec.machines.get(m).ok_or_else(|| format!("no machine named `{m}`"))?;
57 if found.networks.contains_key(n) {
58 Ok(())
59 } else {
60 Err(format!("`{m}` isn't on a network named `{n}`"))
61 }
62 };
63 match r {
64 Reference::OwnAddress { network } => on(template_of.unwrap_or_default(), network),
65 Reference::Address { machine, network } => on(machine, network),
66 Reference::Port { machine, port, field } => {
67 let m = spec.machines.get(machine).ok_or_else(|| format!("no machine named `{machine}`"))?;
68 let p = m
69 .ports
70 .get(port)
71 .ok_or_else(|| format!("`{machine}` has no port named `{port}` (ports are named, never numbered)"))?;
72 if *field == PortField::Url && !p.protocol.is_http() {
73 return Err(format!("`{port}` isn't an `http` or `https` port, so it has no URL"));
74 }
75 Ok(())
76 }
77 Reference::CloudOutput(name) => match &spec.cloud {
78 Some(c) if c.outputs.contains_key(name) => Ok(()),
79 Some(_) => Err(format!("`cloud.outputs` has no `{name}`")),
80 None => Err("the spec has no `cloud:`".into()),
81 },
82 _ => Ok(()),
83 }
84}
85
86fn example(spec: &Spec) -> String {
88 let named = spec
89 .machines
90 .iter()
91 .find_map(|(m, machine)| machine.ports.keys().next().map(|p| format!("`{{{{ machines.{m}.ports.{p}.published }}}}`")));
92 named.unwrap_or_else(|| "`{{ machines.<machine>.ports.<name>.published }}` or `{{ machines.<machine>.addresses.<network> }}`".into())
93}