1use std::fmt;
10use std::str::FromStr;
11
12use serde::Serialize;
13
14use crate::error::UsageErr;
15use crate::miette;
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
19#[serde(rename_all = "snake_case")]
20pub enum Base {
21 Bool,
22 String,
23 Int,
24 Uint,
25 Float,
26 Path,
27 Url,
28 Duration,
29 Object,
31 Custom(String),
37}
38
39impl fmt::Display for Base {
40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41 let name = match self {
42 Self::Bool => "bool",
43 Self::String => "string",
44 Self::Int => "int",
45 Self::Uint => "uint",
46 Self::Float => "float",
47 Self::Path => "path",
48 Self::Url => "url",
49 Self::Duration => "duration",
50 Self::Object => "object",
51 Self::Custom(name) => name,
52 };
53 f.write_str(name)
54 }
55}
56
57const DELIMITERS: [char; 4] = ['<', '>', ',', '|'];
63
64impl From<&str> for Base {
65 fn from(name: &str) -> Self {
66 match name {
67 "bool" | "boolean" => Self::Bool,
68 "string" | "str" => Self::String,
69 "int" | "integer" => Self::Int,
70 "uint" | "usize" => Self::Uint,
71 "float" | "number" => Self::Float,
72 "path" => Self::Path,
73 "url" => Self::Url,
74 "duration" => Self::Duration,
75 "object" | "table" => Self::Object,
76 other => Self::Custom(other.to_string()),
77 }
78 }
79}
80
81#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
83#[serde(rename_all = "snake_case", tag = "kind", content = "of")]
84pub enum SpecConfigType {
85 Base(Base),
86 List(Box<SpecConfigType>),
88 Set(Box<SpecConfigType>),
90 Map(Base, Box<SpecConfigType>),
92 Option(Box<SpecConfigType>),
94 Union(Vec<SpecConfigType>),
96}
97
98impl Default for SpecConfigType {
99 fn default() -> Self {
100 Self::Base(Base::String)
101 }
102}
103
104impl SpecConfigType {
105 pub fn simplified(&self) -> &SpecConfigType {
110 match self {
111 Self::Option(inner) => inner.simplified(),
112 Self::Union(members) => members.first().map_or(self, |m| m.simplified()),
113 other => other,
114 }
115 }
116
117 pub fn is_optional(&self) -> bool {
119 matches!(self, Self::Option(_))
120 }
121}
122
123impl fmt::Display for SpecConfigType {
124 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125 match self {
126 Self::Base(base) => write!(f, "{base}"),
127 Self::List(inner) => write!(f, "list<{inner}>"),
128 Self::Set(inner) => write!(f, "set<{inner}>"),
129 Self::Map(key, value) => write!(f, "map<{key}, {value}>"),
130 Self::Option(inner) => write!(f, "option<{inner}>"),
131 Self::Union(members) => {
132 let rendered: Vec<String> = members.iter().map(|m| m.to_string()).collect();
133 f.write_str(&rendered.join("|"))
134 }
135 }
136 }
137}
138
139impl FromStr for SpecConfigType {
140 type Err = UsageErr;
141
142 fn from_str(text: &str) -> Result<Self, Self::Err> {
143 parse_union(text.trim())
144 }
145}
146
147fn base(text: &str) -> Result<Base, UsageErr> {
152 if text.is_empty() {
153 return Err(invalid(text, "it is empty"));
155 }
156 if let Some(delimiter) = text.chars().find(|c| DELIMITERS.contains(c)) {
157 return Err(invalid(
158 text,
159 &format!("a stray `{delimiter}` — check the brackets"),
160 ));
161 }
162 Ok(Base::from(text))
163}
164
165fn invalid(text: &str, why: &str) -> UsageErr {
166 UsageErr::InvalidInput(
167 format!("`{text}` is not a config type: {why}"),
168 (0, 0).into(),
169 miette::NamedSource::new("", String::new()),
170 )
171}
172
173fn parse_union(text: &str) -> Result<SpecConfigType, UsageErr> {
175 let members = split_top_level(text, '|');
176 match members.as_slice() {
177 [] => Err(invalid(text, "it is empty")),
178 [one] => parse_single(one),
179 many => Ok(SpecConfigType::Union(
180 many.iter()
181 .map(|m| parse_single(m))
182 .collect::<Result<Vec<_>, _>>()?,
183 )),
184 }
185}
186
187fn parse_single(text: &str) -> Result<SpecConfigType, UsageErr> {
188 let text = text.trim();
189 if text.is_empty() {
190 return Err(invalid(text, "it is empty"));
191 }
192 let Some(open) = text.find('<') else {
193 return Ok(SpecConfigType::Base(base(text)?));
194 };
195 if !text.ends_with('>') {
196 return Err(invalid(text, "a `<` without a closing `>`"));
197 }
198 let name = text[..open].trim();
199 let inner = &text[open + 1..text.len() - 1];
200 match name {
201 "list" | "array" => Ok(SpecConfigType::List(Box::new(parse_union(inner)?))),
202 "set" => Ok(SpecConfigType::Set(Box::new(parse_union(inner)?))),
203 "option" | "optional" => Ok(SpecConfigType::Option(Box::new(parse_union(inner)?))),
204 "map" | "table" => {
205 let parts = split_top_level(inner, ',');
206 match parts.as_slice() {
207 [value] => Ok(SpecConfigType::Map(
210 Base::String,
211 Box::new(parse_union(value)?),
212 )),
213 [key, value] => Ok(SpecConfigType::Map(
214 base(key.trim())?,
215 Box::new(parse_union(value)?),
216 )),
217 _ => Err(invalid(text, "a map takes a key and a value")),
218 }
219 }
220 other => Err(invalid(
221 text,
222 &format!("`{other}` does not take a type argument"),
223 )),
224 }
225}
226
227fn split_top_level(text: &str, sep: char) -> Vec<&str> {
229 let mut parts = Vec::new();
230 let mut depth = 0usize;
231 let mut start = 0usize;
232 for (i, c) in text.char_indices() {
233 match c {
234 '<' => depth += 1,
235 '>' => depth = depth.saturating_sub(1),
236 c if c == sep && depth == 0 => {
237 parts.push(text[start..i].trim());
238 start = i + c.len_utf8();
239 }
240 _ => {}
241 }
242 }
243 let last = text[start..].trim();
244 if !last.is_empty() || !parts.is_empty() {
248 parts.push(last);
249 }
250 parts
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 fn parsed(text: &str) -> SpecConfigType {
258 text.parse().unwrap_or_else(|e| panic!("{text}: {e}"))
259 }
260
261 #[test]
262 fn every_shape_round_trips_through_its_own_spelling() {
263 for text in [
266 "bool",
267 "string",
268 "uint",
269 "path",
270 "duration",
271 "object",
272 "list<string>",
273 "set<path>",
274 "map<string, string>",
275 "option<int>",
276 "list<map<string, list<string>>>",
277 "bool|string",
278 "option<list<string>>",
279 ] {
280 assert_eq!(parsed(text).to_string(), text, "{text}");
281 }
282 }
283
284 #[test]
285 fn familiar_spellings_are_accepted() {
286 assert_eq!(parsed("boolean"), SpecConfigType::Base(Base::Bool));
289 assert_eq!(parsed("usize"), SpecConfigType::Base(Base::Uint));
290 assert_eq!(parsed("number"), SpecConfigType::Base(Base::Float));
291 assert_eq!(
292 parsed("array<string>"),
293 SpecConfigType::List(Box::new(SpecConfigType::Base(Base::String)))
294 );
295 assert_eq!(
296 parsed("optional<path>"),
297 SpecConfigType::Option(Box::new(SpecConfigType::Base(Base::Path)))
298 );
299 assert_eq!(
301 parsed("map<string>"),
302 SpecConfigType::Map(Base::String, Box::new(SpecConfigType::Base(Base::String)))
303 );
304 }
305
306 #[test]
307 fn an_unknown_name_is_kept_rather_than_refused() {
308 let ty = parsed("crate::PythonUvVenvAuto");
311 assert_eq!(
312 ty,
313 SpecConfigType::Base(Base::Custom("crate::PythonUvVenvAuto".into()))
314 );
315 assert_eq!(ty.to_string(), "crate::PythonUvVenvAuto");
316 assert_eq!(parsed("list<Weird>").to_string(), "list<Weird>");
318 }
319
320 #[test]
321 fn a_malformed_type_is_an_error() {
322 for text in [
323 "list<string",
324 "list<>",
325 "map<string, int, extra>",
326 "int<x>",
327 "bool|",
332 "|bool",
333 "bool||string",
334 "map<string,>",
335 "map<,string>",
336 "int>",
340 "list<string>>",
341 "map<string>, int>",
342 "bool|>",
343 ] {
344 assert!(
345 text.parse::<SpecConfigType>().is_err(),
346 "`{text}` should not parse"
347 );
348 }
349 }
350
351 #[test]
352 fn simplified_picks_what_a_generator_can_write() {
353 assert_eq!(
354 parsed("option<list<string>>").simplified(),
355 &parsed("list<string>")
356 );
357 assert_eq!(parsed("bool|string").simplified(), &parsed("bool"));
358 assert!(parsed("option<int>").is_optional());
359 assert!(!parsed("int").is_optional());
360 }
361}