Skip to main content

stackless_core/def/
parse.rs

1//! `stackless.toml` text → [`StackDef`].
2
3use serde::Deserialize;
4
5use super::error::DefError;
6use super::model::StackDef;
7
8impl StackDef {
9    /// Parse definition text. Syntax errors and schema mismatches are
10    /// distinct codes: an agent fixes them differently.
11    pub fn parse(text: &str) -> Result<Self, DefError> {
12        match toml::from_str::<Self>(text) {
13            Ok(def) => Ok(def),
14            Err(err) => Err(map_toml_error(err.to_string())),
15        }
16    }
17
18    /// Parse a definition snapshotted into an instance record.
19    ///
20    /// Older snapshots may still contain `[datastores.*]`. Strip that
21    /// section (fresh files still reject it via [`Self::parse`]) but
22    /// keep the datastore names and `${datastores.*.url}` interpolations
23    /// so resume can resolve URLs from journaled provision checkpoints.
24    pub fn parse_snapshot(text: &str) -> Result<Self, DefError> {
25        let mut value: toml::Value = match toml::from_str(text) {
26            Ok(value) => value,
27            Err(err) => return Err(map_toml_error(err.to_string())),
28        };
29        let legacy_datastores = value
30            .as_table()
31            .and_then(|table| table.get("datastores"))
32            .and_then(|section| section.as_table())
33            .map(|section| section.keys().cloned().collect())
34            .unwrap_or_default();
35        if let Some(table) = value.as_table_mut() {
36            table.remove("datastores");
37        }
38        match StackDef::deserialize(value) {
39            Ok(mut def) => {
40                def.legacy_datastores = legacy_datastores;
41                Ok(def)
42            }
43            Err(err) => Err(map_toml_error(err.to_string())),
44        }
45    }
46}
47
48fn map_toml_error(message: String) -> DefError {
49    // `stack.name` is a DnsName: invalid values fail at serde, not
50    // later in validate. Keep the stable `def.validate.name_invalid`
51    // code agents already key on.
52    if let Some(name) = dns_name_parse_failure(&message) {
53        return DefError::NameInvalid {
54            kind: "stack",
55            name,
56        };
57    }
58    // toml reports schema mismatches (unknown/missing fields,
59    // wrong types) through the same error type as syntax
60    // failures; a span into valid TOML with a serde message is
61    // a schema problem.
62    if message.contains("unknown field")
63        || message.contains("missing field")
64        || message.contains("invalid type")
65        || message.contains("unknown variant")
66        || message.contains("duplicate field")
67    {
68        DefError::Schema { message }
69    } else {
70        DefError::Syntax { message }
71    }
72}
73
74fn dns_name_parse_failure(message: &str) -> Option<String> {
75    // serde custom: `invalid DNS name "Bad_Name": must be DNS-safe …`
76    let rest = message.split("invalid DNS name ").nth(1)?;
77    let name = rest.strip_prefix('"')?.split('"').next()?;
78    Some(name.to_owned())
79}