Skip to main content

isoloom_spec/syntax/check/
messages.rs

1//! The placeholders of the message and of an app's templates, read with the spec's one grammar
2//! ([`placeholder`](super::super::placeholder)) and resolved against the spec at validation: a
3//! typo or an index (`ports.0`) fails here, not at run time. The message may use
4//! `machines.<m>.ports.<name>.port`, `.published` and `.url` (an `http` or `https` port's),
5//! `machines.<m>.addresses.<network>` and `cloud.outputs.<name>`; a template the machine's own
6//! `machine.name` and `machine.addresses.<network>`, and no run-time value.
7
8use super::super::Spec;
9use super::super::placeholder::{self, Place, PortField, Reference};
10use super::super::problem::{Code, FieldPath, Problem, Problems};
11use crate::Error;
12
13/// Every placeholder of the message.
14pub(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/// A placeholder that names nothing the spec has, or can't be read.
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct Unresolved {
27    /// Why, naming the placeholder.
28    pub reason: String,
29    /// Whether it is a `{{` without its `}}` (else a path naming nothing there).
30    pub unclosed: bool,
31}
32
33/// The placeholders of `text`, written at `place`, that name nothing the spec has, in order:
34/// `machine` is the machine a template is for. Empty when each resolves.
35#[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
52/// Whether `r` names something of the spec (`template_of`: the machine a template is for),
53/// else why not. An entry's own paths are the catalogue's to resolve.
54fn 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
86/// A placeholder the spec could write, as the fix.
87fn 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}