Skip to main content

bake/
arguments.rs

1// Released under the MIT License.
2// Copyright, 2026, by Samuel Williams.
3
4use crate::{Error, Result};
5use std::collections::BTreeMap;
6use std::fmt::Display;
7use std::str::FromStr;
8
9/// A task parameter. Names use underscores; command-line flags also accept hyphens.
10pub struct Parameter {
11    pub(crate) name: String,
12    pub(crate) type_name: &'static str,
13    pub(crate) positional: bool,
14    pub(crate) required: bool,
15    pub(crate) repeated: bool,
16    pub(crate) default: Option<String>,
17    pub(crate) description: String,
18    validate: fn(&str) -> Result<()>,
19}
20
21impl Parameter {
22    /// Values use `FromStr`; custom types can implement it too.
23    pub fn new<Value: FromStr>(name: &str) -> Self
24    where
25        Value::Err: Display,
26    {
27        Self {
28            name: name.into(),
29            type_name: std::any::type_name::<Value>(),
30            positional: true,
31            required: true,
32            repeated: false,
33            default: None,
34            description: String::new(),
35            validate: |value| parse::<Value>(value).map(|_| ()),
36        }
37    }
38    pub fn named(mut self) -> Self {
39        self.positional = false;
40        self
41    }
42    pub fn optional(mut self) -> Self {
43        self.required = false;
44        self
45    }
46    pub fn repeated(mut self) -> Self {
47        self.repeated = true;
48        self.positional = false;
49        self.required = false;
50        self
51    }
52    pub fn default(mut self, value: impl Into<String>) -> Self {
53        self.default = Some(value.into());
54        self.required = false;
55        self.positional = false;
56        self
57    }
58    pub fn help(mut self, description: impl Into<String>) -> Self {
59        self.description = description.into();
60        self
61    }
62    pub fn name(&self) -> &str {
63        &self.name
64    }
65}
66
67fn parse<Value: FromStr>(value: &str) -> Result<Value>
68where
69    Value::Err: Display,
70{
71    value.parse().map_err(|error| {
72        Error::new(format!(
73            "invalid {} value {value:?}: {error}",
74            std::any::type_name::<Value>()
75        ))
76    })
77}
78
79/// Validated command-line values, converted to Rust types by the task adapter.
80#[derive(Default, Debug)]
81pub struct Arguments(BTreeMap<String, Vec<String>>);
82
83impl Arguments {
84    pub fn required<Value: FromStr>(&self, name: &str) -> Result<Value>
85    where
86        Value::Err: Display,
87    {
88        self.optional(name)?
89            .ok_or_else(|| Error::new(format!("missing argument {name:?}")))
90    }
91    pub fn optional<Value: FromStr>(&self, name: &str) -> Result<Option<Value>>
92    where
93        Value::Err: Display,
94    {
95        self.0
96            .get(name)
97            .and_then(|values| values.first())
98            .map(|value| parse(value))
99            .transpose()
100    }
101    pub fn repeated<Value: FromStr>(&self, name: &str) -> Result<Vec<Value>>
102    where
103        Value::Err: Display,
104    {
105        self.0
106            .get(name)
107            .into_iter()
108            .flatten()
109            .map(|value| parse(value))
110            .collect()
111    }
112
113    fn insert(&mut self, parameter: &Parameter, value: &str) -> Result<()> {
114        if !parameter.repeated && self.0.contains_key(&parameter.name) {
115            return Err(Error::new(format!(
116                "argument {:?} was supplied more than once",
117                parameter.name
118            )));
119        }
120        (parameter.validate)(value)
121            .map_err(|error| Error::new(format!("{}: {error}", parameter.name)))?;
122        self.0
123            .entry(parameter.name.clone())
124            .or_default()
125            .push(value.into());
126        Ok(())
127    }
128
129    pub(crate) fn extract(parameters: &[Parameter], tokens: &[String]) -> Result<(Self, usize)> {
130        let mut arguments = Self::default();
131        let mut consumed = 0;
132        let mut options = true;
133        while let Some(token) = tokens.get(consumed) {
134            if token == "::" {
135                break;
136            }
137            if options && token == "--" {
138                options = false;
139                consumed += 1;
140                continue;
141            }
142            let named = if options {
143                token.strip_prefix("--")
144            } else {
145                None
146            };
147            if let Some(name) = named {
148                if name.contains('=') {
149                    return Err(Error::new(
150                        "named arguments use separate `--name value` tokens",
151                    ));
152                }
153                let name = name.replace('-', "_");
154                let parameter = parameters
155                    .iter()
156                    .find(|parameter| parameter.name == name)
157                    .ok_or_else(|| Error::new(format!("unknown argument {name:?}")))?;
158                consumed += 1;
159                let value = tokens
160                    .get(consumed)
161                    .filter(|value| value.as_str() != "::" && value.as_str() != "--")
162                    .ok_or_else(|| Error::new(format!("argument {name:?} requires a value")))?;
163                consumed += 1;
164                arguments.insert(parameter, value)?;
165            } else if let Some(parameter) = parameters.iter().find(|parameter| {
166                parameter.positional && !arguments.0.contains_key(&parameter.name)
167            }) {
168                arguments.insert(parameter, token)?;
169                consumed += 1;
170            } else {
171                break;
172            }
173        }
174        for parameter in parameters {
175            if parameter.required && !arguments.0.contains_key(&parameter.name) {
176                return Err(Error::new(format!("missing argument {:?}", parameter.name)));
177            }
178        }
179        Ok((arguments, consumed))
180    }
181}