use std::fmt;
use std::str::FromStr;
use super::names::PortName;
use crate::Error;
use crate::placeholders::{self, Piece};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Reference {
MachineName,
OwnAddress {
network: String,
},
Address {
machine: String,
network: String,
},
Port {
machine: String,
port: PortName,
field: PortField,
},
CloudOutput(String),
Version,
Os(OsField),
Var(String),
Setting {
name: String,
field: SettingField,
},
Item(String),
ItemPath(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum PortField {
Port,
Published,
Url,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum OsField {
Family,
Version,
Codename,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum SettingField {
Value,
Path,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Place {
Message,
Template,
Entry,
EntryItem,
}
impl Reference {
#[must_use]
pub fn is_run_time(&self) -> bool {
matches!(
self,
Reference::Port {
field: PortField::Published | PortField::Url,
..
}
)
}
pub fn allowed_in(&self, place: Place) -> Result<(), &'static str> {
let entry = matches!(place, Place::Entry | Place::EntryItem);
match self {
Reference::MachineName | Reference::OwnAddress { .. } if place == Place::Message => Err("the message names machines by `machines.<m>`"),
Reference::Port { .. } if self.is_run_time() && place != Place::Message => {
Err("a published port and a URL are known only once the lab runs; a file gets `port`")
}
Reference::CloudOutput(_) if place != Place::Message => Err("a cloud output is the message's only"),
Reference::Version | Reference::Os(_) | Reference::Var(_) | Reference::Setting { .. } if !entry => Err("only a catalogue entry has it"),
Reference::Item(_) | Reference::ItemPath(_) if place != Place::EntryItem => Err("only a step repeated with `for_each` has an `item`"),
_ => Ok(()),
}
}
}
impl FromStr for Reference {
type Err = Error;
fn from_str(path: &str) -> Result<Reference, Error> {
let unknown = |why| Error::Placeholder { path: path.to_owned(), why };
let parts: Vec<&str> = path.split('.').collect();
match parts.as_slice() {
["machine", "name"] => Ok(Reference::MachineName),
["machine", "addresses", n] => Ok(Reference::OwnAddress { network: (*n).to_owned() }),
["machine", ..] => Err(unknown("this machine's paths are `machine.name` and `machine.addresses.<network>`")),
["machines", m, rest @ ..] => {
machine(m, rest).ok_or_else(|| unknown("a machine's paths are `ports.<name>.port`, `.published`, `.url` and `addresses.<network>`"))
}
["cloud", "outputs", name] => Ok(Reference::CloudOutput((*name).to_owned())),
["version"] => Ok(Reference::Version),
["os", "family"] => Ok(Reference::Os(OsField::Family)),
["os", "version"] => Ok(Reference::Os(OsField::Version)),
["os", "codename"] => Ok(Reference::Os(OsField::Codename)),
["os", ..] => Err(unknown("the OS's paths are `os.family`, `os.version` and `os.codename`")),
["vars", x] => Ok(Reference::Var((*x).to_owned())),
["with", s] => Ok(Reference::Setting {
name: (*s).to_owned(),
field: SettingField::Value,
}),
["with", s, "path"] => Ok(Reference::Setting {
name: (*s).to_owned(),
field: SettingField::Path,
}),
["item", field] => Ok(Reference::Item((*field).to_owned())),
["item", field, "path"] => Ok(Reference::ItemPath((*field).to_owned())),
_ => Err(unknown(
"the paths start with `machine.`, `machines.<m>.`, `cloud.outputs.`, `version`, `os.`, `vars.`, `with.` or `item.`",
)),
}
}
}
fn machine(m: &str, rest: &[&str]) -> Option<Reference> {
let machine = m.to_owned();
match rest {
["addresses", n] => Some(Reference::Address {
machine,
network: (*n).to_owned(),
}),
["ports", p, field] => {
let field = match *field {
"port" => PortField::Port,
"published" => PortField::Published,
"url" => PortField::Url,
_ => return None,
};
Some(Reference::Port {
machine,
port: p.parse().ok()?,
field,
})
}
_ => None,
}
}
impl fmt::Display for Reference {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Reference::MachineName => f.write_str("machine.name"),
Reference::OwnAddress { network } => write!(f, "machine.addresses.{network}"),
Reference::Address { machine, network } => write!(f, "machines.{machine}.addresses.{network}"),
Reference::Port { machine, port, field } => {
let field = match field {
PortField::Port => "port",
PortField::Published => "published",
PortField::Url => "url",
};
write!(f, "machines.{machine}.ports.{port}.{field}")
}
Reference::CloudOutput(name) => write!(f, "cloud.outputs.{name}"),
Reference::Version => f.write_str("version"),
Reference::Os(OsField::Family) => f.write_str("os.family"),
Reference::Os(OsField::Version) => f.write_str("os.version"),
Reference::Os(OsField::Codename) => f.write_str("os.codename"),
Reference::Var(x) => write!(f, "vars.{x}"),
Reference::Setting {
name,
field: SettingField::Value,
} => write!(f, "with.{name}"),
Reference::Setting {
name,
field: SettingField::Path,
} => write!(f, "with.{name}.path"),
Reference::Item(field) => write!(f, "item.{field}"),
Reference::ItemPath(field) => write!(f, "item.{field}.path"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Part {
Text(String),
Placeholder(Reference),
}
pub fn read(text: &str, place: Place) -> Result<Vec<Part>, Vec<Error>> {
let mut parts = Vec::new();
let mut wrong = Vec::new();
for piece in placeholders::pieces(text) {
match piece {
Piece::Text(t) => parts.push(Part::Text(t.to_owned())),
Piece::Placeholder(inner) => match reference(inner, place) {
Ok(r) => parts.push(Part::Placeholder(r)),
Err(e) => wrong.push(e),
},
Piece::Unclosed(_) => wrong.push(Error::Unclosed),
}
}
if wrong.is_empty() { Ok(parts) } else { Err(wrong) }
}
pub fn join<E>(parts: Vec<Part>, mut value: impl FnMut(&Reference) -> Result<String, E>) -> Result<String, E> {
let mut out = String::new();
for part in parts {
match part {
Part::Text(t) => out.push_str(&t),
Part::Placeholder(r) => out.push_str(&value(&r)?),
}
}
Ok(out)
}
#[must_use]
pub fn placeholders(text: &str, place: Place) -> Vec<Result<Reference, Error>> {
placeholders::pieces(text)
.into_iter()
.filter_map(|piece| match piece {
Piece::Text(_) => None,
Piece::Placeholder(inner) => Some(reference(inner, place)),
Piece::Unclosed(_) => Some(Err(Error::Unclosed)),
})
.collect()
}
fn reference(inner: &str, place: Place) -> Result<Reference, Error> {
let r: Reference = inner.trim().parse()?;
match r.allowed_in(place) {
Ok(()) => Ok(r),
Err(why) => Err(Error::NotHere { reference: r, why }),
}
}
#[cfg(test)]
mod tests;