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}