Skip to main content

usage/spec/
config_type.rs

1//! The type a config property's values take.
2//!
3//! A small expression grammar rather than an enum of names, because the fleet's registries
4//! need composition — `list<string>`, `map<string,string>`, `option<path>`, and the
5//! occasional union like `bool|string`. Anything unrecognized parses as
6//! [`Base::Custom`] and is preserved verbatim: a spec written for a newer usage, or one
7//! naming a type only its own tool understands, keeps working rather than failing to load.
8
9use std::fmt;
10use std::str::FromStr;
11
12use serde::Serialize;
13
14use crate::error::UsageErr;
15
16/// A named, non-composite type.
17#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
18#[serde(rename_all = "snake_case")]
19pub enum Base {
20    Bool,
21    String,
22    Int,
23    Uint,
24    Float,
25    Path,
26    Url,
27    Duration,
28    /// A free-form table: keys and values the spec does not describe.
29    Object,
30    /// A name this version does not know.
31    ///
32    /// Not an error: `data_type` used to be five values and a spec that names a sixth
33    /// should load. Consumers that need a type they understand treat it as a string, which
34    /// is what a schema generator can always do.
35    Custom(String),
36}
37
38impl fmt::Display for Base {
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40        let name = match self {
41            Self::Bool => "bool",
42            Self::String => "string",
43            Self::Int => "int",
44            Self::Uint => "uint",
45            Self::Float => "float",
46            Self::Path => "path",
47            Self::Url => "url",
48            Self::Duration => "duration",
49            Self::Object => "object",
50            Self::Custom(name) => name,
51        };
52        f.write_str(name)
53    }
54}
55
56/// The characters the grammar itself uses, which therefore cannot appear in a name.
57///
58/// Without this check a typo keeps the delimiter and becomes a [`Base::Custom`] named after
59/// it — `int>` loads as a type called `int>`, and every consumer treats it as a string. The
60/// escape hatch is for names a newer usage understands, not for text that failed to parse.
61const DELIMITERS: [char; 4] = ['<', '>', ',', '|'];
62
63impl From<&str> for Base {
64    fn from(name: &str) -> Self {
65        match name {
66            "bool" | "boolean" => Self::Bool,
67            "string" | "str" => Self::String,
68            "int" | "integer" => Self::Int,
69            "uint" | "usize" => Self::Uint,
70            "float" | "number" => Self::Float,
71            "path" => Self::Path,
72            "url" => Self::Url,
73            "duration" => Self::Duration,
74            "object" | "table" => Self::Object,
75            other => Self::Custom(other.to_string()),
76        }
77    }
78}
79
80/// A config property's type, as written.
81#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
82#[serde(rename_all = "snake_case", tag = "kind", content = "of")]
83pub enum SpecConfigType {
84    Base(Base),
85    /// `list<T>` — ordered, duplicates kept.
86    List(Box<SpecConfigType>),
87    /// `set<T>` — deduplicated.
88    Set(Box<SpecConfigType>),
89    /// `map<K, V>` — the key is always a base type.
90    Map(Base, Box<SpecConfigType>),
91    /// `option<T>` — may be absent, with no default standing in.
92    Option(Box<SpecConfigType>),
93    /// `a|b` — one of several. Only what a spec can express; nothing validates which.
94    Union(Vec<SpecConfigType>),
95}
96
97impl Default for SpecConfigType {
98    fn default() -> Self {
99        Self::Base(Base::String)
100    }
101}
102
103impl SpecConfigType {
104    /// The type this collapses to for a consumer that only handles one shape.
105    ///
106    /// A union's first member, an option's inner type: what a schema generator writes when
107    /// it has to pick. Kept here rather than in each generator so they agree.
108    pub fn simplified(&self) -> &SpecConfigType {
109        match self {
110            Self::Option(inner) => inner.simplified(),
111            Self::Union(members) => members.first().map_or(self, |m| m.simplified()),
112            other => other,
113        }
114    }
115
116    /// Whether a value may be absent with nothing standing in for it.
117    pub fn is_optional(&self) -> bool {
118        matches!(self, Self::Option(_))
119    }
120}
121
122impl fmt::Display for SpecConfigType {
123    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124        match self {
125            Self::Base(base) => write!(f, "{base}"),
126            Self::List(inner) => write!(f, "list<{inner}>"),
127            Self::Set(inner) => write!(f, "set<{inner}>"),
128            Self::Map(key, value) => write!(f, "map<{key}, {value}>"),
129            Self::Option(inner) => write!(f, "option<{inner}>"),
130            Self::Union(members) => {
131                let rendered: Vec<String> = members.iter().map(|m| m.to_string()).collect();
132                f.write_str(&rendered.join("|"))
133            }
134        }
135    }
136}
137
138impl FromStr for SpecConfigType {
139    type Err = UsageErr;
140
141    fn from_str(text: &str) -> Result<Self, Self::Err> {
142        parse_union(text.trim())
143    }
144}
145
146/// A base type, refusing a name that still holds a delimiter.
147///
148/// `Base::from` cannot do this itself: it is infallible on purpose, so a name usage does not
149/// know survives a load. What it must not do is swallow a malformed *expression* as a name.
150fn base(text: &str) -> Result<Base, UsageErr> {
151    if text.is_empty() {
152        // `map<,string>`: a name is missing, not merely unrecognized.
153        return Err(invalid(text, "it is empty"));
154    }
155    if let Some(delimiter) = text.chars().find(|c| DELIMITERS.contains(c)) {
156        return Err(invalid(
157            text,
158            &format!("a stray `{delimiter}` — check the brackets"),
159        ));
160    }
161    Ok(Base::from(text))
162}
163
164fn invalid(text: &str, why: &str) -> UsageErr {
165    UsageErr::InvalidInput(
166        format!("`{text}` is not a config type: {why}"),
167        (0, 0).into(),
168        miette::NamedSource::new("", String::new()),
169    )
170}
171
172/// `a|b|c`, splitting only on bars outside angle brackets.
173fn parse_union(text: &str) -> Result<SpecConfigType, UsageErr> {
174    let members = split_top_level(text, '|');
175    match members.as_slice() {
176        [] => Err(invalid(text, "it is empty")),
177        [one] => parse_single(one),
178        many => Ok(SpecConfigType::Union(
179            many.iter()
180                .map(|m| parse_single(m))
181                .collect::<Result<Vec<_>, _>>()?,
182        )),
183    }
184}
185
186fn parse_single(text: &str) -> Result<SpecConfigType, UsageErr> {
187    let text = text.trim();
188    if text.is_empty() {
189        return Err(invalid(text, "it is empty"));
190    }
191    let Some(open) = text.find('<') else {
192        return Ok(SpecConfigType::Base(base(text)?));
193    };
194    if !text.ends_with('>') {
195        return Err(invalid(text, "a `<` without a closing `>`"));
196    }
197    let name = text[..open].trim();
198    let inner = &text[open + 1..text.len() - 1];
199    match name {
200        "list" | "array" => Ok(SpecConfigType::List(Box::new(parse_union(inner)?))),
201        "set" => Ok(SpecConfigType::Set(Box::new(parse_union(inner)?))),
202        "option" | "optional" => Ok(SpecConfigType::Option(Box::new(parse_union(inner)?))),
203        "map" | "table" => {
204            let parts = split_top_level(inner, ',');
205            match parts.as_slice() {
206                // `map<string>` is a map from strings to that type, which is what every
207                // registry in the fleet means by a one-argument map.
208                [value] => Ok(SpecConfigType::Map(
209                    Base::String,
210                    Box::new(parse_union(value)?),
211                )),
212                [key, value] => Ok(SpecConfigType::Map(
213                    base(key.trim())?,
214                    Box::new(parse_union(value)?),
215                )),
216                _ => Err(invalid(text, "a map takes a key and a value")),
217            }
218        }
219        other => Err(invalid(
220            text,
221            &format!("`{other}` does not take a type argument"),
222        )),
223    }
224}
225
226/// Split on `sep`, ignoring separators nested inside `<…>`.
227fn split_top_level(text: &str, sep: char) -> Vec<&str> {
228    let mut parts = Vec::new();
229    let mut depth = 0usize;
230    let mut start = 0usize;
231    for (i, c) in text.char_indices() {
232        match c {
233            '<' => depth += 1,
234            '>' => depth = depth.saturating_sub(1),
235            c if c == sep && depth == 0 => {
236                parts.push(text[start..i].trim());
237                start = i + c.len_utf8();
238            }
239            _ => {}
240        }
241    }
242    let last = text[start..].trim();
243    // An empty tail only counts when a separator put it there: `"bool"` is one part, while
244    // `"bool|"` is two, the second of which is empty and fails to parse. Dropping empties
245    // here instead made `bool|`, `bool||string` and `map<string,>` load as something else.
246    if !last.is_empty() || !parts.is_empty() {
247        parts.push(last);
248    }
249    parts
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255
256    fn parsed(text: &str) -> SpecConfigType {
257        text.parse().unwrap_or_else(|e| panic!("{text}: {e}"))
258    }
259
260    #[test]
261    fn every_shape_round_trips_through_its_own_spelling() {
262        // The written form is the interchange, so parsing and displaying have to agree —
263        // otherwise a spec changes meaning by being saved.
264        for text in [
265            "bool",
266            "string",
267            "uint",
268            "path",
269            "duration",
270            "object",
271            "list<string>",
272            "set<path>",
273            "map<string, string>",
274            "option<int>",
275            "list<map<string, list<string>>>",
276            "bool|string",
277            "option<list<string>>",
278        ] {
279            assert_eq!(parsed(text).to_string(), text, "{text}");
280        }
281    }
282
283    #[test]
284    fn familiar_spellings_are_accepted() {
285        // The registries being migrated say `boolean`, `ListString`-ish `array<…>`,
286        // `usize`, `number`. Accepting the synonyms costs nothing and normalizes them.
287        assert_eq!(parsed("boolean"), SpecConfigType::Base(Base::Bool));
288        assert_eq!(parsed("usize"), SpecConfigType::Base(Base::Uint));
289        assert_eq!(parsed("number"), SpecConfigType::Base(Base::Float));
290        assert_eq!(
291            parsed("array<string>"),
292            SpecConfigType::List(Box::new(SpecConfigType::Base(Base::String)))
293        );
294        assert_eq!(
295            parsed("optional<path>"),
296            SpecConfigType::Option(Box::new(SpecConfigType::Base(Base::Path)))
297        );
298        // A one-argument map is keyed by strings, which is what the fleet means by it.
299        assert_eq!(
300            parsed("map<string>"),
301            SpecConfigType::Map(Base::String, Box::new(SpecConfigType::Base(Base::String)))
302        );
303    }
304
305    #[test]
306    fn an_unknown_name_is_kept_rather_than_refused() {
307        // The escape hatch: a tool may name a type only it understands, and a spec written
308        // for a newer usage must still load.
309        let ty = parsed("crate::PythonUvVenvAuto");
310        assert_eq!(
311            ty,
312            SpecConfigType::Base(Base::Custom("crate::PythonUvVenvAuto".into()))
313        );
314        assert_eq!(ty.to_string(), "crate::PythonUvVenvAuto");
315        // Including inside a composite.
316        assert_eq!(parsed("list<Weird>").to_string(), "list<Weird>");
317    }
318
319    #[test]
320    fn a_malformed_type_is_an_error() {
321        for text in [
322            "list<string",
323            "list<>",
324            "map<string, int, extra>",
325            "int<x>",
326            // A missing member, rather than a missing bracket. Each of these used to load as
327            // something else: `bool|` as plain `bool`, `bool||string` as the two-member
328            // union, `map<string,>` as a map to strings — a typo quietly changing what a
329            // setting holds, which the type is the interchange for precisely to avoid.
330            "bool|",
331            "|bool",
332            "bool||string",
333            "map<string,>",
334            "map<,string>",
335            // A stray delimiter is a broken expression, not the name of a type only this
336            // tool knows: `int>` used to become `Custom("int>")` and every consumer treated
337            // it as a string.
338            "int>",
339            "list<string>>",
340            "map<string>, int>",
341            "bool|>",
342        ] {
343            assert!(
344                text.parse::<SpecConfigType>().is_err(),
345                "`{text}` should not parse"
346            );
347        }
348    }
349
350    #[test]
351    fn simplified_picks_what_a_generator_can_write() {
352        assert_eq!(
353            parsed("option<list<string>>").simplified(),
354            &parsed("list<string>")
355        );
356        assert_eq!(parsed("bool|string").simplified(), &parsed("bool"));
357        assert!(parsed("option<int>").is_optional());
358        assert!(!parsed("int").is_optional());
359    }
360}