1use std::fmt;
10use std::str::FromStr;
11
12use serde::Serialize;
13
14use crate::error::UsageErr;
15
16#[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 Object,
30 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
56const 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#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
82#[serde(rename_all = "snake_case", tag = "kind", content = "of")]
83pub enum SpecConfigType {
84 Base(Base),
85 List(Box<SpecConfigType>),
87 Set(Box<SpecConfigType>),
89 Map(Base, Box<SpecConfigType>),
91 Option(Box<SpecConfigType>),
93 Union(Vec<SpecConfigType>),
95}
96
97impl Default for SpecConfigType {
98 fn default() -> Self {
99 Self::Base(Base::String)
100 }
101}
102
103impl SpecConfigType {
104 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 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
146fn base(text: &str) -> Result<Base, UsageErr> {
151 if text.is_empty() {
152 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
172fn 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 [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
226fn 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 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 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 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 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 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 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 "bool|",
331 "|bool",
332 "bool||string",
333 "map<string,>",
334 "map<,string>",
335 "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}