1use std::fmt;
18use std::str::FromStr;
19
20use super::names::PortName;
21use crate::Error;
22use crate::placeholders::{self, Piece};
23
24#[derive(Debug, Clone, PartialEq, Eq, Hash)]
26#[non_exhaustive]
27pub enum Reference {
28 MachineName,
30 OwnAddress {
32 network: String,
34 },
35 Address {
37 machine: String,
39 network: String,
41 },
42 Port {
44 machine: String,
46 port: PortName,
48 field: PortField,
50 },
51 CloudOutput(String),
53 Version,
55 Os(OsField),
57 Var(String),
59 Setting {
61 name: String,
63 field: SettingField,
65 },
66 Item(String),
68 ItemPath(String),
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
74#[non_exhaustive]
75pub enum PortField {
76 Port,
78 Published,
80 Url,
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
86#[non_exhaustive]
87pub enum OsField {
88 Family,
90 Version,
92 Codename,
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
98#[non_exhaustive]
99pub enum SettingField {
100 Value,
102 Path,
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
108#[non_exhaustive]
109pub enum Place {
110 Message,
112 Template,
114 Entry,
116 EntryItem,
118}
119
120impl Reference {
121 #[must_use]
123 pub fn is_run_time(&self) -> bool {
124 matches!(
125 self,
126 Reference::Port {
127 field: PortField::Published | PortField::Url,
128 ..
129 }
130 )
131 }
132
133 pub fn allowed_in(&self, place: Place) -> Result<(), &'static str> {
139 let entry = matches!(place, Place::Entry | Place::EntryItem);
140 match self {
141 Reference::MachineName | Reference::OwnAddress { .. } if place == Place::Message => Err("the message names machines by `machines.<m>`"),
142 Reference::Port { .. } if self.is_run_time() && place != Place::Message => {
143 Err("a published port and a URL are known only once the lab runs; a file gets `port`")
144 }
145 Reference::CloudOutput(_) if place != Place::Message => Err("a cloud output is the message's only"),
146 Reference::Version | Reference::Os(_) | Reference::Var(_) | Reference::Setting { .. } if !entry => Err("only a catalogue entry has it"),
147 Reference::Item(_) | Reference::ItemPath(_) if place != Place::EntryItem => Err("only a step repeated with `for_each` has an `item`"),
148 _ => Ok(()),
149 }
150 }
151}
152
153impl FromStr for Reference {
154 type Err = Error;
155
156 fn from_str(path: &str) -> Result<Reference, Error> {
162 let unknown = |why| Error::Placeholder { path: path.to_owned(), why };
163 let parts: Vec<&str> = path.split('.').collect();
164 match parts.as_slice() {
165 ["machine", "name"] => Ok(Reference::MachineName),
166 ["machine", "addresses", n] => Ok(Reference::OwnAddress { network: (*n).to_owned() }),
167 ["machine", ..] => Err(unknown("this machine's paths are `machine.name` and `machine.addresses.<network>`")),
168 ["machines", m, rest @ ..] => {
169 machine(m, rest).ok_or_else(|| unknown("a machine's paths are `ports.<name>.port`, `.published`, `.url` and `addresses.<network>`"))
170 }
171 ["cloud", "outputs", name] => Ok(Reference::CloudOutput((*name).to_owned())),
172 ["version"] => Ok(Reference::Version),
173 ["os", "family"] => Ok(Reference::Os(OsField::Family)),
174 ["os", "version"] => Ok(Reference::Os(OsField::Version)),
175 ["os", "codename"] => Ok(Reference::Os(OsField::Codename)),
176 ["os", ..] => Err(unknown("the OS's paths are `os.family`, `os.version` and `os.codename`")),
177 ["vars", x] => Ok(Reference::Var((*x).to_owned())),
178 ["with", s] => Ok(Reference::Setting {
179 name: (*s).to_owned(),
180 field: SettingField::Value,
181 }),
182 ["with", s, "path"] => Ok(Reference::Setting {
183 name: (*s).to_owned(),
184 field: SettingField::Path,
185 }),
186 ["item", field] => Ok(Reference::Item((*field).to_owned())),
187 ["item", field, "path"] => Ok(Reference::ItemPath((*field).to_owned())),
188 _ => Err(unknown(
189 "the paths start with `machine.`, `machines.<m>.`, `cloud.outputs.`, `version`, `os.`, `vars.`, `with.` or `item.`",
190 )),
191 }
192 }
193}
194
195fn machine(m: &str, rest: &[&str]) -> Option<Reference> {
197 let machine = m.to_owned();
198 match rest {
199 ["addresses", n] => Some(Reference::Address {
200 machine,
201 network: (*n).to_owned(),
202 }),
203 ["ports", p, field] => {
204 let field = match *field {
205 "port" => PortField::Port,
206 "published" => PortField::Published,
207 "url" => PortField::Url,
208 _ => return None,
209 };
210 Some(Reference::Port {
211 machine,
212 port: p.parse().ok()?,
213 field,
214 })
215 }
216 _ => None,
217 }
218}
219
220impl fmt::Display for Reference {
221 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
222 match self {
223 Reference::MachineName => f.write_str("machine.name"),
224 Reference::OwnAddress { network } => write!(f, "machine.addresses.{network}"),
225 Reference::Address { machine, network } => write!(f, "machines.{machine}.addresses.{network}"),
226 Reference::Port { machine, port, field } => {
227 let field = match field {
228 PortField::Port => "port",
229 PortField::Published => "published",
230 PortField::Url => "url",
231 };
232 write!(f, "machines.{machine}.ports.{port}.{field}")
233 }
234 Reference::CloudOutput(name) => write!(f, "cloud.outputs.{name}"),
235 Reference::Version => f.write_str("version"),
236 Reference::Os(OsField::Family) => f.write_str("os.family"),
237 Reference::Os(OsField::Version) => f.write_str("os.version"),
238 Reference::Os(OsField::Codename) => f.write_str("os.codename"),
239 Reference::Var(x) => write!(f, "vars.{x}"),
240 Reference::Setting {
241 name,
242 field: SettingField::Value,
243 } => write!(f, "with.{name}"),
244 Reference::Setting {
245 name,
246 field: SettingField::Path,
247 } => write!(f, "with.{name}.path"),
248 Reference::Item(field) => write!(f, "item.{field}"),
249 Reference::ItemPath(field) => write!(f, "item.{field}.path"),
250 }
251 }
252}
253
254#[derive(Debug, Clone, PartialEq, Eq)]
256#[non_exhaustive]
257pub enum Part {
258 Text(String),
260 Placeholder(Reference),
262}
263
264pub fn read(text: &str, place: Place) -> Result<Vec<Part>, Vec<Error>> {
270 let mut parts = Vec::new();
271 let mut wrong = Vec::new();
272 for piece in placeholders::pieces(text) {
273 match piece {
274 Piece::Text(t) => parts.push(Part::Text(t.to_owned())),
275 Piece::Placeholder(inner) => match reference(inner, place) {
276 Ok(r) => parts.push(Part::Placeholder(r)),
277 Err(e) => wrong.push(e),
278 },
279 Piece::Unclosed(_) => wrong.push(Error::Unclosed),
280 }
281 }
282 if wrong.is_empty() { Ok(parts) } else { Err(wrong) }
283}
284
285pub fn join<E>(parts: Vec<Part>, mut value: impl FnMut(&Reference) -> Result<String, E>) -> Result<String, E> {
291 let mut out = String::new();
292 for part in parts {
293 match part {
294 Part::Text(t) => out.push_str(&t),
295 Part::Placeholder(r) => out.push_str(&value(&r)?),
296 }
297 }
298 Ok(out)
299}
300
301#[must_use]
304pub fn placeholders(text: &str, place: Place) -> Vec<Result<Reference, Error>> {
305 placeholders::pieces(text)
306 .into_iter()
307 .filter_map(|piece| match piece {
308 Piece::Text(_) => None,
309 Piece::Placeholder(inner) => Some(reference(inner, place)),
310 Piece::Unclosed(_) => Some(Err(Error::Unclosed)),
311 })
312 .collect()
313}
314
315fn reference(inner: &str, place: Place) -> Result<Reference, Error> {
317 let r: Reference = inner.trim().parse()?;
318 match r.allowed_in(place) {
319 Ok(()) => Ok(r),
320 Err(why) => Err(Error::NotHere { reference: r, why }),
321 }
322}
323
324#[cfg(test)]
325mod tests;