• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

facet-rs / facet / 15113146359

19 May 2025 12:36PM UTC coverage: 56.947% (+0.2%) from 56.733%
15113146359

Pull #628

github

web-flow
Merge 37349d055 into 453013232
Pull Request #628: feat(args): convert reflection spans from arg-wise to char-wise

117 of 183 new or added lines in 5 files covered. (63.93%)

3 existing lines in 2 files now uncovered.

9218 of 16187 relevant lines covered (56.95%)

131.68 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

76.98
/facet-args/src/format.rs
1
use alloc::borrow::Cow;
2
use alloc::string::ToString;
3
use core::fmt;
4
use facet_core::{Facet, FieldAttribute, Type, UserType};
5
use 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
11
pub struct Cli;
12

13
impl fmt::Display for Cli {
14
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1✔
15
        write!(f, "Cli")
1✔
16
    }
1✔
17
}
18

19
impl Cli {
20
    /// Helper function to convert kebab-case to snake_case
21
    fn kebab_to_snake(input: &str) -> Cow<str> {
20✔
22
        if !input.contains('-') {
20✔
23
            return Cow::Borrowed(input);
18✔
24
        }
2✔
25
        Cow::Owned(input.replace('-', "_"))
2✔
26
    }
20✔
27
}
28

29
/// Parse command line arguments into a Facet-compatible type
30
pub fn from_slice<'input, 'facet, 'shape, T: Facet<'facet>>(
27✔
31
    args: &'input [&'input str],
27✔
32
) -> Result<T, DeserError<'input, 'shape>>
27✔
33
where
27✔
34
    'input: 'facet + 'shape,
27✔
35
{
36
    facet_deserialize::deserialize(args, &mut Cli)
27✔
37
}
27✔
38

39
impl Format for Cli {
40
    type Input<'input> = [&'input str];
41
    type SpanType = Raw;
42

43
    fn source(&self) -> &'static str {
56✔
44
        "args"
56✔
45
    }
56✔
46

47
    fn next<'input, 'facet, 'shape>(
119✔
48
        &mut self,
119✔
49
        nd: NextData<'input, 'facet, 'shape, Self::SpanType, Self::Input<'input>>,
119✔
50
        expectation: Expectation,
119✔
51
    ) -> NextResult<
119✔
52
        'input,
119✔
53
        'facet,
119✔
54
        'shape,
119✔
55
        Spanned<Outcome<'input>, Self::SpanType>,
119✔
56
        Spanned<DeserErrorKind<'shape>, Self::SpanType>,
119✔
57
        Self::SpanType,
119✔
58
        Self::Input<'input>,
119✔
59
    >
119✔
60
    where
119✔
61
        'shape: 'input,
119✔
62
    {
63
        let arg_idx = nd.start();
119✔
64
        let shape = nd.wip.shape();
119✔
65
        let args = nd.input();
119✔
66

67
        match expectation {
119✔
68
            // Top-level value
69
            Expectation::Value => {
70
                // Check if it's a struct type
71
                if !matches!(shape.ty, Type::User(UserType::Struct(_))) {
28✔
72
                    return (
1✔
73
                        nd,
1✔
74
                        Err(Spanned {
1✔
75
                            node: DeserErrorKind::UnsupportedType {
1✔
76
                                got: shape,
1✔
77
                                wanted: "struct",
1✔
78
                            },
1✔
79
                            span: Span::new(arg_idx, 0),
1✔
80
                        }),
1✔
81
                    );
1✔
82
                }
27✔
83
                // For CLI args, we always start with an object (struct)
84
                (
27✔
85
                    nd,
27✔
86
                    Ok(Spanned {
27✔
87
                        node: Outcome::ObjectStarted,
27✔
88
                        span: Span::new(arg_idx, 0),
27✔
89
                    }),
27✔
90
                )
27✔
91
            }
92

93
            // Object key (or finished)
94
            Expectation::ObjectKeyOrObjectClose => {
95
                /* Check if we have more arguments */
96
                if arg_idx < args.len() {
53✔
97
                    let arg = args[arg_idx];
42✔
98
                    let span = Span::new(arg_idx, 1);
42✔
99

100
                    // Named long argument?
101
                    if let Some(key) = arg.strip_prefix("--") {
42✔
102
                        let key = Self::kebab_to_snake(key);
20✔
103

104
                        // Check if the field exists in the struct
105
                        if let Type::User(UserType::Struct(_)) = shape.ty {
20✔
106
                            if nd.wip.field_index(&key).is_none() {
20✔
107
                                return (
2✔
108
                                    nd,
2✔
109
                                    Err(Spanned {
2✔
110
                                        node: DeserErrorKind::UnknownField {
2✔
111
                                            field_name: key.to_string(),
2✔
112
                                            shape,
2✔
113
                                        },
2✔
114
                                        span: Span::new(arg_idx, 0),
2✔
115
                                    }),
2✔
116
                                );
2✔
117
                            }
18✔
118
                        }
×
119
                        return (
18✔
120
                            nd,
18✔
121
                            Ok(Spanned {
18✔
122
                                node: Outcome::Scalar(Scalar::String(key)),
18✔
123
                                span,
18✔
124
                            }),
18✔
125
                        );
18✔
126
                    }
22✔
127

128
                    // Short flag?
129
                    if let Some(key) = arg.strip_prefix('-') {
22✔
130
                        // Convert short argument to field name via shape
131
                        if let Type::User(UserType::Struct(st)) = shape.ty {
13✔
132
                            for field in st.fields.iter() {
26✔
133
                                for attr in field.attributes {
59✔
134
                                    if let FieldAttribute::Arbitrary(a) = attr {
46✔
135
                                        // Don't require specifying a short key for a single-char key
136
                                        if a.contains("short")
46✔
137
                                            && (a.contains(key)
20✔
138
                                                || (key.len() == 1 && field.name == key))
9✔
139
                                        {
140
                                            return (
13✔
141
                                                nd,
13✔
142
                                                Ok(Spanned {
13✔
143
                                                    node: Outcome::Scalar(Scalar::String(
13✔
144
                                                        Cow::Borrowed(field.name),
13✔
145
                                                    )),
13✔
146
                                                    span,
13✔
147
                                                }),
13✔
148
                                            );
13✔
149
                                        }
33✔
150
                                    }
×
151
                                }
152
                            }
153
                        }
×
154
                        return (
×
155
                            nd,
×
156
                            Err(Spanned {
×
157
                                node: DeserErrorKind::UnknownField {
×
158
                                    field_name: key.to_string(),
×
159
                                    shape,
×
160
                                },
×
NEW
161
                                span: Span::new(arg_idx, 0),
×
162
                            }),
×
163
                        );
×
164
                    }
9✔
165

166
                    // positional argument
167
                    if let Type::User(UserType::Struct(st)) = &shape.ty {
9✔
168
                        for (idx, field) in st.fields.iter().enumerate() {
11✔
169
                            for attr in field.attributes.iter() {
12✔
170
                                if let FieldAttribute::Arbitrary(a) = attr {
12✔
171
                                    if a.contains("positional") {
12✔
172
                                        // Check if this field is already set
173
                                        let is_set = nd.wip.is_field_set(idx).unwrap_or(false);
7✔
174

175
                                        if !is_set {
7✔
176
                                            // Use this positional field
177
                                            return (
6✔
178
                                                nd,
6✔
179
                                                Ok(Spanned {
6✔
180
                                                    node: Outcome::Scalar(Scalar::String(
6✔
181
                                                        Cow::Borrowed(field.name),
6✔
182
                                                    )),
6✔
183
                                                    span: Span::new(arg_idx, 0),
6✔
184
                                                }),
6✔
185
                                            );
6✔
186
                                        }
1✔
187
                                    }
5✔
188
                                }
×
189
                            }
190
                        }
191
                    }
×
192

193
                    // If no positional field was found
194
                    return (
3✔
195
                        nd,
3✔
196
                        Err(Spanned {
3✔
197
                            node: DeserErrorKind::UnknownField {
3✔
198
                                field_name: "positional argument".to_string(),
3✔
199
                                shape,
3✔
200
                            },
3✔
201
                            span: Span::new(arg_idx, 0),
3✔
202
                        }),
3✔
203
                    );
3✔
204
                }
11✔
205

206
                // EOF: inject implicit-false-if-absent bool flags, if there are any
207
                if let Type::User(UserType::Struct(st)) = &shape.ty {
11✔
208
                    for (idx, field) in st.fields.iter().enumerate() {
27✔
209
                        if !nd.wip.is_field_set(idx).unwrap_or(false)
27✔
210
                            && field.shape().is_type::<bool>()
3✔
211
                        {
212
                            return (
1✔
213
                                nd,
1✔
214
                                Ok(Spanned {
1✔
215
                                    node: Outcome::Scalar(Scalar::String(Cow::Borrowed(
1✔
216
                                        field.name,
1✔
217
                                    ))),
1✔
218
                                    span: Span::new(arg_idx, 0),
1✔
219
                                }),
1✔
220
                            );
1✔
221
                        }
26✔
222
                    }
223
                }
×
224

225
                // Real end of object
226
                (
10✔
227
                    nd,
10✔
228
                    Ok(Spanned {
10✔
229
                        node: Outcome::ObjectEnded,
10✔
230
                        span: Span::new(arg_idx, 0),
10✔
231
                    }),
10✔
232
                )
10✔
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>() {
38✔
239
                    return (
1✔
240
                        nd,
1✔
241
                        Ok(Spanned {
1✔
242
                            node: Outcome::Scalar(Scalar::Bool(false)),
1✔
243
                            span: Span::new(arg_idx, 0),
1✔
244
                        }),
1✔
245
                    );
1✔
246
                }
37✔
247

248
                // Explicit boolean true
249
                if shape.is_type::<bool>() {
37✔
250
                    // For boolean fields, we don't need an explicit value
251
                    return (
4✔
252
                        nd,
4✔
253
                        Ok(Spanned {
4✔
254
                            node: Outcome::Scalar(Scalar::Bool(true)),
4✔
255
                            span: Span::new(arg_idx, 0),
4✔
256
                        }),
4✔
257
                    );
4✔
258
                }
33✔
259

260
                // For other types, get the next arg as the value.
261
                // Need another CLI token:
262
                if arg_idx >= args.len() {
33✔
263
                    return (
2✔
264
                        nd,
2✔
265
                        Err(Spanned {
2✔
266
                            node: DeserErrorKind::MissingValue {
2✔
267
                                expected: "argument value",
2✔
268
                                field: args[arg_idx.saturating_sub(1)].to_string(),
2✔
269
                            },
2✔
270
                            span: Span::new(arg_idx.saturating_sub(1), 0),
2✔
271
                        }),
2✔
272
                    );
2✔
273
                }
31✔
274

275
                let arg = args[arg_idx];
31✔
276
                let span = Span::new(arg_idx, 1);
31✔
277

278
                // Skip this value if it starts with - (it's probably another flag)
279
                if arg.starts_with('-') {
31✔
280
                    // This means we're missing a value for the previous argument
281
                    return (
2✔
282
                        nd,
2✔
283
                        Err(Spanned {
2✔
284
                            node: DeserErrorKind::MissingValue {
2✔
285
                                expected: "argument value",
2✔
286
                                field: args[arg_idx.saturating_sub(1)].to_string(),
2✔
287
                            },
2✔
288
                            span: Span::new(arg_idx.saturating_sub(1), 0),
2✔
289
                        }),
2✔
290
                    );
2✔
291
                }
29✔
292

293
                // Try to parse as appropriate type
294
                // Handle numeric types
295
                if let Ok(v) = arg.parse::<u64>() {
29✔
296
                    return (
13✔
297
                        nd,
13✔
298
                        Ok(Spanned {
13✔
299
                            node: Outcome::Scalar(Scalar::U64(v)),
13✔
300
                            span,
13✔
301
                        }),
13✔
302
                    );
13✔
303
                }
16✔
304
                if let Ok(v) = arg.parse::<i64>() {
16✔
305
                    return (
×
306
                        nd,
×
307
                        Ok(Spanned {
×
308
                            node: Outcome::Scalar(Scalar::I64(v)),
×
309
                            span,
×
310
                        }),
×
311
                    );
×
312
                }
16✔
313
                if let Ok(v) = arg.parse::<f64>() {
16✔
314
                    return (
1✔
315
                        nd,
1✔
316
                        Ok(Spanned {
1✔
317
                            node: Outcome::Scalar(Scalar::F64(v)),
1✔
318
                            span,
1✔
319
                        }),
1✔
320
                    );
1✔
321
                }
15✔
322

323
                // Default to string type
324
                (
15✔
325
                    nd,
15✔
326
                    Ok(Spanned {
15✔
327
                        node: Outcome::Scalar(Scalar::String(Cow::Borrowed(arg))),
15✔
328
                        span,
15✔
329
                    }),
15✔
330
                )
15✔
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
    }
119✔
357

358
    fn skip<'input, 'facet, 'shape>(
×
359
        &mut self,
×
NEW
360
        nd: NextData<'input, 'facet, 'shape, Self::SpanType, Self::Input<'input>>,
×
361
    ) -> NextResult<
×
362
        'input,
×
363
        'facet,
×
364
        'shape,
×
NEW
365
        Span<Self::SpanType>,
×
NEW
366
        Spanned<DeserErrorKind<'shape>, Self::SpanType>,
×
NEW
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
                    },
×
NEW
387
                    span: Span::new(arg_idx, 1),
×
388
                }),
×
389
            )
×
390
        }
391
    }
×
392
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc