facet_args/
lib.rs

1#![warn(missing_docs)]
2#![warn(clippy::std_instead_of_core)]
3#![warn(clippy::std_instead_of_alloc)]
4#![forbid(unsafe_code)]
5#![doc = include_str!("../README.md")]
6
7extern crate alloc;
8use alloc::borrow::Cow;
9
10/// Apply field default values and function values using facet-deserialize
11pub(crate) mod defaults;
12/// Errors raised when CLI arguments are not parsed or otherwise fail during reflection
13pub mod error;
14/// Parsing utilities for CLI arguments
15pub(crate) mod parse;
16
17use defaults::apply_field_defaults;
18use error::{ArgsError, ArgsErrorKind};
19use facet_core::{Def, Facet, Type, UserType};
20use facet_reflect::{ReflectError, Wip};
21// use format::CliFormat;
22use parse::{parse_named_arg, parse_positional_arg, parse_short_arg};
23
24/// Process a field in the Wip
25pub(crate) fn parse_field<'facet>(
26    wip: Wip<'facet>,
27    value: &'facet str,
28) -> Result<Wip<'facet>, ArgsError> {
29    let shape = wip.shape();
30
31    if shape.is_type::<String>() {
32        log::trace!("shape is String");
33        wip.put(value.to_string())
34    } else if shape.is_type::<&str>() {
35        log::trace!("shape is &str");
36        wip.put(value)
37    } else if shape.is_type::<bool>() {
38        log::trace!("shape is bool, setting to true");
39        wip.put(value.to_lowercase() == "true")
40    } else {
41        match shape.def {
42            Def::Scalar(_) => {
43                log::trace!("shape is nothing known, falling back to parse: {}", shape);
44                wip.parse(value)
45            }
46            _def => {
47                return Err(ArgsError::new(ArgsErrorKind::GenericReflect(
48                    ReflectError::OperationFailed {
49                        shape,
50                        operation: "parsing field",
51                    },
52                )));
53            }
54        }
55    }
56    .map_err(|e| ArgsError::new(ArgsErrorKind::GenericReflect(e)))?
57    .pop()
58    .map_err(|e| ArgsError {
59        kind: ArgsErrorKind::GenericReflect(e),
60    })
61}
62
63fn kebab_to_snake(input: &str) -> Cow<str> {
64    // ASSUMPTION: We only support GNU/Unix kebab-case named argument
65    // ASSUMPTION: struct fields are snake_case
66    if !input.contains('-') {
67        return Cow::Borrowed(input);
68    }
69    Cow::Owned(input.replace('-', "_"))
70}
71
72/// Parses command-line arguments
73pub fn from_slice<'input, 'facet, T>(s: &[&'input str]) -> Result<T, ArgsError>
74where
75    T: Facet<'facet>,
76    'input: 'facet,
77{
78    log::trace!("Entering from_slice function");
79    let mut s = s;
80    let mut wip =
81        Wip::alloc::<T>().map_err(|e| ArgsError::new(ArgsErrorKind::GenericReflect(e)))?;
82    log::trace!("Allocated Poke for type T");
83    let Type::User(UserType::Struct(st)) = wip.shape().ty else {
84        return Err(ArgsError::new(ArgsErrorKind::GenericArgsError(
85            "Expected struct type".to_string(),
86        )));
87    };
88
89    while let Some(token) = s.first() {
90        log::trace!("Processing token: {}", token);
91        s = &s[1..];
92
93        if let Some(key) = token.strip_prefix("--") {
94            let key = kebab_to_snake(key);
95            log::trace!("Found named argument: {}", key);
96            wip = parse_named_arg(wip, &key, &mut s)?;
97        } else if let Some(key) = token.strip_prefix("-") {
98            log::trace!("Found short named argument: {}", key);
99            wip = parse_short_arg(wip, key, &mut s, &st)?;
100        } else {
101            log::trace!("Encountered positional argument: {}", token);
102            wip = parse_positional_arg(wip, token, &st)?;
103        }
104    }
105
106    // Apply defaults, except for absent booleans being implicitly default false
107    wip = apply_field_defaults(wip)?;
108
109    // If a boolean field is unset the value is set to `false`
110    // This behaviour means `#[facet(default = false)]` does not need to be explicitly set
111    // on each boolean field specified on a Command struct
112    for (field_index, f) in st.fields.iter().enumerate() {
113        if f.shape().is_type::<bool>() && !wip.is_field_set(field_index).expect("in bounds") {
114            let field = wip.field(field_index).expect("field_index is in bounds");
115            wip = parse_field(field, "false")?;
116        }
117    }
118
119    // Add this right after getting the struct type (st)
120    log::trace!("Checking field attributes");
121    for (i, field) in st.fields.iter().enumerate() {
122        log::trace!(
123            "Field {}: {} - Attributes: {:?}",
124            i,
125            field.name,
126            field.attributes
127        );
128    }
129
130    let heap_vale = wip
131        .build()
132        .map_err(|e| ArgsError::new(ArgsErrorKind::GenericReflect(e)))?;
133    let result = heap_vale
134        .materialize()
135        .map_err(|e| ArgsError::new(ArgsErrorKind::GenericReflect(e)))?;
136    Ok(result)
137}