Skip to main content

usage/spec/
choices.rs

1#[cfg(feature = "unstable_choices_env")]
2use kdl::KdlEntry;
3use kdl::KdlNode;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7use crate::error::UsageErr;
8use crate::spec::context::ParsingContext;
9use crate::spec::helpers::NodeHelper;
10
11#[derive(Debug, Default, Clone, Serialize, Deserialize)]
12pub struct SpecChoices {
13    pub choices: Vec<String>,
14    #[cfg(feature = "unstable_choices_env")]
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub env: Option<String>,
17}
18
19impl SpecChoices {
20    #[cfg(feature = "unstable_choices_env")]
21    #[must_use]
22    pub fn env(&self) -> Option<&str> {
23        self.env.as_deref()
24    }
25
26    #[cfg(not(feature = "unstable_choices_env"))]
27    #[must_use]
28    pub fn env(&self) -> Option<&str> {
29        None
30    }
31
32    #[cfg(feature = "unstable_choices_env")]
33    pub fn set_env(&mut self, env: Option<String>) {
34        self.env = env;
35    }
36
37    pub(crate) fn parse(ctx: &ParsingContext, node: &NodeHelper) -> Result<Self, UsageErr> {
38        #[cfg(not(feature = "unstable_choices_env"))]
39        node.ensure_arg_len(1..)?;
40
41        #[cfg(feature = "unstable_choices_env")]
42        let mut config = Self {
43            choices: node
44                .args()
45                .map(|e| e.ensure_string())
46                .collect::<Result<_, _>>()?,
47            ..Default::default()
48        };
49
50        #[cfg(not(feature = "unstable_choices_env"))]
51        let config = Self {
52            choices: node
53                .args()
54                .map(|e| e.ensure_string())
55                .collect::<Result<_, _>>()?,
56            ..Default::default()
57        };
58
59        for (k, v) in node.props() {
60            match k {
61                #[cfg(feature = "unstable_choices_env")]
62                "env" => config.set_env(Some(v.ensure_string()?)),
63                k => bail_parse!(ctx, v.entry.span(), "unsupported choices key {k}"),
64            }
65        }
66
67        if config.choices.is_empty() {
68            #[cfg(feature = "unstable_choices_env")]
69            if config.env().is_none() {
70                bail_parse!(
71                    ctx,
72                    node.span(),
73                    "choices must have at least 1 argument or env property"
74                );
75            }
76            #[cfg(not(feature = "unstable_choices_env"))]
77            bail_parse!(ctx, node.span(), "choices must have at least 1 argument");
78        }
79
80        Ok(config)
81    }
82
83    pub fn values(&self) -> Vec<String> {
84        self.values_with_env(None)
85    }
86
87    pub(crate) fn values_with_env(&self, env: Option<&HashMap<String, String>>) -> Vec<String> {
88        #[cfg(feature = "unstable_choices_env")]
89        let mut values = self.choices.clone();
90
91        #[cfg(not(feature = "unstable_choices_env"))]
92        let values = self.choices.clone();
93
94        #[cfg(not(feature = "unstable_choices_env"))]
95        let _ = env;
96
97        #[cfg(feature = "unstable_choices_env")]
98        {
99            if let Some(env_key) = self.env() {
100                let env_value = if let Some(env_map) = env {
101                    env_map.get(env_key).cloned()
102                } else {
103                    std::env::var(env_key).ok()
104                };
105
106                if let Some(env_value) = env_value {
107                    for choice in env_value
108                        .split(|c: char| c == ',' || c.is_whitespace())
109                        .filter(|choice| !choice.is_empty())
110                    {
111                        let choice = choice.to_string();
112                        if !values.contains(&choice) {
113                            values.push(choice);
114                        }
115                    }
116                }
117            }
118        }
119
120        values
121    }
122}
123
124impl From<&SpecChoices> for KdlNode {
125    fn from(arg: &SpecChoices) -> Self {
126        let mut node = KdlNode::new("choices");
127        for choice in &arg.choices {
128            node.push(choice.to_string());
129        }
130        #[cfg(feature = "unstable_choices_env")]
131        if let Some(env) = arg.env() {
132            node.push(KdlEntry::new_prop("env", env.to_string()));
133        }
134        node
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    #[cfg(feature = "unstable_choices_env")]
141    use super::SpecChoices;
142    #[cfg(feature = "unstable_choices_env")]
143    use std::collections::HashMap;
144
145    #[cfg(feature = "unstable_choices_env")]
146    #[test]
147    fn values_with_env_splits_on_commas_and_whitespace() {
148        let mut choices = SpecChoices {
149            choices: vec!["local".into()],
150            ..Default::default()
151        };
152        choices.set_env(Some("DEPLOY_ENVS".into()));
153
154        let env = HashMap::from([("DEPLOY_ENVS".to_string(), "foo,bar baz\nqux".to_string())]);
155
156        assert_eq!(
157            choices.values_with_env(Some(&env)),
158            vec!["local", "foo", "bar", "baz", "qux"]
159        );
160    }
161
162    #[cfg(feature = "unstable_choices_env")]
163    #[test]
164    fn values_with_env_deduplicates_existing_choices() {
165        let mut choices = SpecChoices {
166            choices: vec!["foo".into()],
167            ..Default::default()
168        };
169        choices.set_env(Some("DEPLOY_ENVS".into()));
170
171        let env = HashMap::from([("DEPLOY_ENVS".to_string(), "foo,bar foo".to_string())]);
172
173        assert_eq!(choices.values_with_env(Some(&env)), vec!["foo", "bar"]);
174    }
175
176    #[cfg(feature = "unstable_choices_env")]
177    #[test]
178    fn values_with_env_does_not_fallback_when_custom_env_is_present() {
179        let mut choices = SpecChoices {
180            choices: vec!["local".into()],
181            ..Default::default()
182        };
183        choices.set_env(Some(
184            "USAGE_TEST_CHOICES_ENV_DOES_NOT_EXIST_A5E0F4D1".into(),
185        ));
186
187        assert_eq!(
188            choices.values_with_env(Some(&HashMap::new())),
189            vec!["local"]
190        );
191    }
192}