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