facet_args/format.rs
1use alloc::borrow::Cow;
2use alloc::string::ToString;
3use core::fmt;
4use facet_core::{Facet, FieldAttribute, Type, UserType};
5use facet_deserialize::{
6 DeserError, DeserErrorKind, Expectation, Format, NextData, NextResult, Outcome, Scalar, Span,
7 Spanned,
8};
9
10/// Command-line argument format for Facet deserialization
11pub struct Cli;
12
13impl fmt::Display for Cli {
14 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
15 write!(f, "Cli")
16 }
17}
18
19impl Cli {
20 /// Helper function to convert kebab-case to snake_case
21 fn kebab_to_snake(input: &str) -> Cow<str> {
22 if !input.contains('-') {
23 return Cow::Borrowed(input);
24 }
25 Cow::Owned(input.replace('-', "_"))
26 }
27
28 /// Converts an argument index position to a character-based span for error reporting
29 ///
30 /// This function calculates the character position of the argument in the joined command line
31 /// and returns a span that correctly points to that position for error visualization.
32 ///
33 /// * `args` - The full array of command line arguments
34 /// * `arg_idx` - The index of the current argument (0-based)
35 /// * `width` - Optional length to highlight (defaults to the full argument length)
36 fn char_span(
37 args: &[&str],
38 arg_idx: usize,
39 width: Option<usize>,
40 char_offset: Option<isize>,
41 ) -> Span {
42 if arg_idx >= args.len() {
43 // If we're at the end, point to the end of the last argument
44 if args.is_empty() {
45 return Span::new(0, 0);
46 }
47
48 // Calculate position after the last argument
49 let mut char_pos = 0;
50 for (i, arg) in args.iter().enumerate() {
51 char_pos += arg.len();
52 if i < args.len() - 1 {
53 char_pos += 1; // Add space between arguments
54 }
55 }
56 return Span::new(char_pos, 0);
57 }
58
59 // Calculate the character position of this argument in the joined string
60 let mut char_pos = 0;
61 for arg in args.iter().take(arg_idx) {
62 char_pos += arg.len() + 1; // +1 for space
63 }
64
65 // Determine how much of the argument to highlight
66 let len = width.unwrap_or_else(|| args[arg_idx].len());
67 let offset = char_offset.unwrap_or(0);
68 // Apply the offset to char_pos
69 let effective_pos = if offset >= 0 {
70 char_pos.saturating_add(offset as usize)
71 } else {
72 char_pos.saturating_sub((-offset) as usize)
73 };
74
75 Span::new(effective_pos, len)
76 }
77}
78
79/// Parse command line arguments into a Facet-compatible type
80pub fn from_slice<'input: 'facet, 'facet, T: Facet<'facet>>(
81 args: &'input [&'input str],
82) -> Result<T, DeserError<'input>> {
83 facet_deserialize::deserialize(args, Cli)
84}
85
86impl Format for Cli {
87 type Input<'input> = [&'input str];
88
89 fn source(&self) -> &'static str {
90 "args"
91 }
92
93 fn next<'input, 'facet>(
94 &mut self,
95 nd: NextData<'input, 'facet, Self::Input<'input>>,
96 expectation: Expectation,
97 ) -> NextResult<
98 'input,
99 'facet,
100 Spanned<Outcome<'input>>,
101 Spanned<DeserErrorKind>,
102 Self::Input<'input>,
103 > {
104 let arg_idx = nd.start();
105 let shape = nd.wip.shape();
106 let args = nd.input();
107
108 match expectation {
109 // Top-level value
110 Expectation::Value => {
111 // Check if it's a struct type
112 if !matches!(shape.ty, Type::User(UserType::Struct(_))) {
113 return (
114 nd,
115 Err(Spanned {
116 node: DeserErrorKind::UnsupportedType {
117 got: shape,
118 wanted: "struct",
119 },
120 span: Self::char_span(args, arg_idx, Some(0), None),
121 }),
122 );
123 }
124 // For CLI args, we always start with an object (struct)
125 (
126 nd,
127 Ok(Spanned {
128 node: Outcome::ObjectStarted,
129 span: Span::new(arg_idx, 0),
130 }),
131 )
132 }
133
134 // Object key (or finished)
135 Expectation::ObjectKeyOrObjectClose => {
136 /* Check if we have more arguments */
137 if arg_idx < args.len() {
138 let arg = args[arg_idx];
139 let span = Span::new(arg_idx, 1);
140
141 // Named long argument?
142 if let Some(key) = arg.strip_prefix("--") {
143 let key = Self::kebab_to_snake(key);
144
145 // Check if the field exists in the struct
146 if let Type::User(UserType::Struct(_)) = shape.ty {
147 if nd.wip.field_index(&key).is_none() {
148 return (
149 nd,
150 Err(Spanned {
151 node: DeserErrorKind::UnknownField {
152 field_name: key.to_string(),
153 shape,
154 },
155 span: Self::char_span(args, arg_idx, None, None),
156 }),
157 );
158 }
159 }
160 return (
161 nd,
162 Ok(Spanned {
163 node: Outcome::Scalar(Scalar::String(key)),
164 span,
165 }),
166 );
167 }
168
169 // Short flag?
170 if let Some(key) = arg.strip_prefix('-') {
171 // Convert short argument to field name via shape
172 if let Type::User(UserType::Struct(st)) = shape.ty {
173 for field in st.fields.iter() {
174 for attr in field.attributes {
175 if let FieldAttribute::Arbitrary(a) = attr {
176 // Don't require specifying a short key for a single-char key
177 if a.contains("short")
178 && (a.contains(key)
179 || (key.len() == 1 && field.name == key))
180 {
181 return (
182 nd,
183 Ok(Spanned {
184 node: Outcome::Scalar(Scalar::String(
185 Cow::Borrowed(field.name),
186 )),
187 span,
188 }),
189 );
190 }
191 }
192 }
193 }
194 }
195 return (
196 nd,
197 Err(Spanned {
198 node: DeserErrorKind::UnknownField {
199 field_name: key.to_string(),
200 shape,
201 },
202 span: Self::char_span(args, arg_idx, None, None),
203 }),
204 );
205 }
206
207 // positional argument
208 if let Type::User(UserType::Struct(st)) = &shape.ty {
209 for (idx, field) in st.fields.iter().enumerate() {
210 for attr in field.attributes.iter() {
211 if let FieldAttribute::Arbitrary(a) = attr {
212 if a.contains("positional") {
213 // Check if this field is already set
214 let is_set = nd.wip.is_field_set(idx).unwrap_or(false);
215
216 if !is_set {
217 // Use this positional field
218 return (
219 nd,
220 Ok(Spanned {
221 node: Outcome::Scalar(Scalar::String(
222 Cow::Borrowed(field.name),
223 )),
224 span: Span::new(arg_idx, 0),
225 }),
226 );
227 }
228 }
229 }
230 }
231 }
232 }
233
234 // If no positional field was found
235 return (
236 nd,
237 Err(Spanned {
238 node: DeserErrorKind::UnknownField {
239 field_name: "positional argument".to_string(),
240 shape,
241 },
242 span: Self::char_span(args, arg_idx, None, None),
243 }),
244 );
245 }
246
247 // EOF: inject implicit-false-if-absent bool flags, if there are any
248 if let Type::User(UserType::Struct(st)) = &shape.ty {
249 for (idx, field) in st.fields.iter().enumerate() {
250 if !nd.wip.is_field_set(idx).unwrap_or(false)
251 && field.shape().is_type::<bool>()
252 {
253 return (
254 nd,
255 Ok(Spanned {
256 node: Outcome::Scalar(Scalar::String(Cow::Borrowed(
257 field.name,
258 ))),
259 span: Span::new(arg_idx, 0),
260 }),
261 );
262 }
263 }
264 }
265
266 // Real end of object
267 (
268 nd,
269 Ok(Spanned {
270 node: Outcome::ObjectEnded,
271 span: Span::new(arg_idx, 0),
272 }),
273 )
274 }
275
276 // Value for the current key
277 Expectation::ObjectVal => {
278 // Synthetic implicit-false
279 if arg_idx >= args.len() && shape.is_type::<bool>() {
280 return (
281 nd,
282 Ok(Spanned {
283 node: Outcome::Scalar(Scalar::Bool(false)),
284 span: Span::new(arg_idx, 0),
285 }),
286 );
287 }
288
289 // Explicit boolean true
290 if shape.is_type::<bool>() {
291 // For boolean fields, we don't need an explicit value
292 return (
293 nd,
294 Ok(Spanned {
295 node: Outcome::Scalar(Scalar::Bool(true)),
296 span: Span::new(arg_idx, 0),
297 }),
298 );
299 }
300
301 // For other types, get the next arg as the value.
302 // Need another CLI token:
303 if arg_idx >= args.len() {
304 return (
305 nd,
306 Err(Spanned {
307 node: DeserErrorKind::MissingValue {
308 expected: "argument value",
309 field: args[arg_idx.saturating_sub(1)].to_string(),
310 },
311 span: Self::char_span(args, arg_idx, Some(1), Some(-1)),
312 }),
313 );
314 }
315
316 let arg = args[arg_idx];
317 let span = Span::new(arg_idx, 1);
318
319 // Skip this value if it starts with - (it's probably another flag)
320 if arg.starts_with('-') {
321 // This means we're missing a value for the previous argument
322 return (
323 nd,
324 Err(Spanned {
325 node: DeserErrorKind::MissingValue {
326 expected: "argument value",
327 field: args[arg_idx.saturating_sub(1)].to_string(),
328 },
329 span: Self::char_span(args, arg_idx, Some(1), Some(-1)),
330 }),
331 );
332 }
333
334 // Try to parse as appropriate type
335 // Handle numeric types
336 if let Ok(v) = arg.parse::<u64>() {
337 return (
338 nd,
339 Ok(Spanned {
340 node: Outcome::Scalar(Scalar::U64(v)),
341 span,
342 }),
343 );
344 }
345 if let Ok(v) = arg.parse::<i64>() {
346 return (
347 nd,
348 Ok(Spanned {
349 node: Outcome::Scalar(Scalar::I64(v)),
350 span,
351 }),
352 );
353 }
354 if let Ok(v) = arg.parse::<f64>() {
355 return (
356 nd,
357 Ok(Spanned {
358 node: Outcome::Scalar(Scalar::F64(v)),
359 span,
360 }),
361 );
362 }
363
364 // Default to string type
365 (
366 nd,
367 Ok(Spanned {
368 node: Outcome::Scalar(Scalar::String(Cow::Borrowed(arg))),
369 span,
370 }),
371 )
372 }
373
374 // List items
375 Expectation::ListItemOrListClose => {
376 // End the list if we're out of arguments, or if it's a new flag
377 if arg_idx >= args.len() || args[arg_idx].starts_with('-') {
378 return (
379 nd,
380 Ok(Spanned {
381 node: Outcome::ListEnded,
382 span: Span::new(arg_idx, 0),
383 }),
384 );
385 }
386
387 // Process the next item in the list
388 (
389 nd,
390 Ok(Spanned {
391 node: Outcome::Scalar(Scalar::String(Cow::Borrowed(args[arg_idx]))),
392 span: Span::new(arg_idx, 1),
393 }),
394 )
395 }
396 }
397 }
398
399 fn skip<'input, 'facet>(
400 &mut self,
401 nd: NextData<'input, 'facet, Self::Input<'input>>,
402 ) -> NextResult<'input, 'facet, Span, Spanned<DeserErrorKind>, Self::Input<'input>> {
403 let arg_idx = nd.start();
404 let args = nd.input();
405
406 if arg_idx < args.len() {
407 // Simply skip one position
408 (nd, Ok(Span::new(arg_idx, 1)))
409 } else {
410 // No argument to skip
411 (
412 nd,
413 Err(Spanned {
414 node: DeserErrorKind::UnexpectedEof {
415 wanted: "argument to skip",
416 },
417 span: Self::char_span(args, arg_idx, None, None),
418 }),
419 )
420 }
421 }
422}