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