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
10mod error;
11
12use error::{ArgsError, ArgsErrorKind};
13use facet_core::{Def, Facet, FieldAttribute};
14use facet_reflect::{ReflectError, Wip};
15
16fn parse_field<'facet>(wip: Wip<'facet>, value: &'facet str) -> Result<Wip<'facet>, ArgsError> {
17    let shape = wip.shape();
18    match shape.def {
19        Def::Scalar(_) => {
20            if shape.is_type::<String>() {
21                wip.put(value.to_string())
22            } else if shape.is_type::<&str>() {
23                wip.put(value)
24            } else if shape.is_type::<bool>() {
25                log::trace!("Boolean field detected, setting to true");
26                wip.put(value.to_lowercase() == "true")
27            } else {
28                wip.parse(value)
29            }
30        }
31        _def => {
32            return Err(ArgsError::new(ArgsErrorKind::GenericReflect(
33                ReflectError::OperationFailed {
34                    shape,
35                    operation: "parsing field",
36                },
37            )));
38        }
39    }
40    .map_err(|e| ArgsError::new(ArgsErrorKind::GenericReflect(e)))?
41    .pop()
42    .map_err(|e| ArgsError {
43        kind: ArgsErrorKind::GenericReflect(e),
44    })
45}
46
47fn kebab_to_snake(input: &str) -> Cow<str> {
48    // ASSUMPTION: We only support GNU/Unix kebab-case named argument
49    // ASSUMPTION: struct fields are snake_case
50    if !input.contains('-') {
51        return Cow::Borrowed(input);
52    }
53    Cow::Owned(input.replace('-', "_"))
54}
55
56/// Parses command-line arguments
57pub fn from_slice<'input, 'facet, T>(s: &[&'input str]) -> Result<T, ArgsError>
58where
59    T: Facet<'facet>,
60    'input: 'facet,
61{
62    log::trace!("Entering from_slice function");
63    let mut s = s;
64    let mut wip =
65        Wip::alloc::<T>().map_err(|e| ArgsError::new(ArgsErrorKind::GenericReflect(e)))?;
66    log::trace!("Allocated Poke for type T");
67    let Def::Struct(sd) = wip.shape().def else {
68        return Err(ArgsError::new(ArgsErrorKind::GenericArgsError(
69            "Expected struct defintion".to_string(),
70        )));
71    };
72
73    while let Some(token) = s.first() {
74        log::trace!("Processing token: {}", token);
75        s = &s[1..];
76
77        if let Some(key) = token.strip_prefix("--") {
78            let key = kebab_to_snake(key);
79            let field_index = match wip.field_index(&key) {
80                Some(index) => index,
81                None => {
82                    return Err(ArgsError::new(ArgsErrorKind::GenericArgsError(format!(
83                        "Unknown argument `{key}`",
84                    ))));
85                }
86            };
87            log::trace!("Found named argument: {}", key);
88
89            let field = wip
90                .field(field_index)
91                .expect("field_index should be a valid field bound");
92
93            if field.shape().is_type::<bool>() {
94                // TODO: absence i.e "false" case is not handled
95                wip = parse_field(field, "true")?;
96            } else {
97                let value = s
98                    .first()
99                    .ok_or(ArgsError::new(ArgsErrorKind::GenericArgsError(format!(
100                        "expected value after argument `{key}`"
101                    ))))?;
102                log::trace!("Field value: {}", value);
103                s = &s[1..];
104                wip = parse_field(field, value)?;
105            }
106        } else if let Some(key) = token.strip_prefix("-") {
107            log::trace!("Found short named argument: {}", key);
108            for (field_index, f) in sd.fields.iter().enumerate() {
109                if f.attributes
110                    .iter()
111                    .any(|a| matches!(a, FieldAttribute::Arbitrary(a) if a.contains("short") && a.contains(key))
112                   )
113                {
114                    log::trace!("Found field matching short_code: {} for field {}", key, f.name);
115                    let field = wip.field(field_index).expect("field_index is in bounds");
116                    if field.shape().is_type::<bool>() {
117                        wip = parse_field(field, "true")?;
118                    } else {
119                        let value = s
120                            .first()
121                            .ok_or(ArgsError::new(ArgsErrorKind::GenericArgsError(format!(
122                                "expected value after argument `{key}`"
123                            ))))?;
124                        log::trace!("Field value: {}", value);
125                        s = &s[1..];
126                        wip = parse_field(field, value)?;
127                    }
128                    break;
129                }
130            }
131        } else {
132            log::trace!("Encountered positional argument: {}", token);
133            for (field_index, f) in sd.fields.iter().enumerate() {
134                if f.attributes
135                    .iter()
136                    .any(|a| matches!(a, FieldAttribute::Arbitrary(a) if a.contains("positional")))
137                {
138                    if wip
139                        .is_field_set(field_index)
140                        .expect("field_index is in bounds")
141                    {
142                        continue;
143                    }
144                    let field = wip.field(field_index).expect("field_index is in bounds");
145                    wip = parse_field(field, token)?;
146                    break;
147                }
148            }
149        }
150    }
151
152    // If a boolean field is unset the value is set to `false`
153    // This behaviour means `#[facet(default = false)]` does not need to be explicitly set
154    // on each boolean field specified on a Command struct
155    for (field_index, f) in sd.fields.iter().enumerate() {
156        if f.shape().is_type::<bool>() && !wip.is_field_set(field_index).expect("in bounds") {
157            let field = wip.field(field_index).expect("field_index is in bounds");
158            wip = parse_field(field, "false")?;
159        }
160    }
161
162    let heap_vale = wip
163        .build()
164        .map_err(|e| ArgsError::new(ArgsErrorKind::GenericReflect(e)))?;
165    let result = heap_vale
166        .materialize()
167        .map_err(|e| ArgsError::new(ArgsErrorKind::GenericReflect(e)))?;
168    Ok(result)
169}