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}