1use schemars::JsonSchema;
8use serde::{Deserialize, Deserializer, de::Error as _};
9
10#[derive(Deserialize, JsonSchema)]
11#[serde(untagged)]
12#[allow(dead_code)]
13pub(crate) enum BoolOrString {
14 Bool(bool),
15 String(String),
16}
17
18#[derive(Deserialize, JsonSchema)]
19#[serde(untagged)]
20#[allow(dead_code)]
21pub(crate) enum IntOrString {
22 Int(u64),
23 String(String),
24}
25
26fn parse_bool(s: &str) -> Option<bool> {
27 match s.trim().to_ascii_lowercase().as_str() {
28 "true" | "yes" | "on" | "1" => Some(true),
29 "false" | "no" | "off" | "0" | "" => Some(false),
30 _ => None,
31 }
32}
33
34pub(crate) fn bool<'de, D: Deserializer<'de>>(d: D) -> Result<bool, D::Error> {
35 match BoolOrString::deserialize(d)? {
36 BoolOrString::Bool(b) => Ok(b),
37 BoolOrString::String(s) => {
38 parse_bool(&s).ok_or_else(|| D::Error::custom(format!("expected a boolean, got {s:?}")))
39 }
40 }
41}
42
43pub(crate) fn opt_bool<'de, D: Deserializer<'de>>(d: D) -> Result<Option<bool>, D::Error> {
44 bool(d).map(Some)
45}
46
47pub(crate) fn string<'de, D: Deserializer<'de>>(d: D) -> Result<String, D::Error> {
49 Ok(match IntOrString::deserialize(d)? {
50 IntOrString::Int(n) => n.to_string(),
51 IntOrString::String(s) => s,
52 })
53}
54
55pub fn opt_string_or_null<'de, D: Deserializer<'de>>(d: D) -> Result<Option<String>, D::Error> {
57 Ok(Option::<IntOrString>::deserialize(d)?.map(|v| match v {
58 IntOrString::Int(n) => n.to_string(),
59 IntOrString::String(s) => s,
60 }))
61}
62
63pub(crate) fn opt_string<'de, D: Deserializer<'de>>(d: D) -> Result<Option<String>, D::Error> {
64 Ok(Some(match IntOrString::deserialize(d)? {
65 IntOrString::Int(n) => n.to_string(),
66 IntOrString::String(s) => s,
67 }))
68}
69
70#[derive(serde::Serialize, Deserialize, JsonSchema)]
72#[serde(untagged)]
73#[allow(dead_code)]
74pub(crate) enum Scalar {
75 String(String),
76 Bool(bool),
77 Int(i64),
78 Float(f64),
79}
80
81impl Scalar {
82 pub(crate) fn into_string(self) -> String {
83 match self {
84 Scalar::String(s) => s,
85 Scalar::Bool(b) => b.to_string(),
86 Scalar::Int(i) => i.to_string(),
87 Scalar::Float(f) => f.to_string(),
88 }
89 }
90}
91
92pub(crate) fn string_map<'de, D: Deserializer<'de>>(
95 d: D,
96) -> Result<std::collections::BTreeMap<String, String>, D::Error> {
97 let m = std::collections::BTreeMap::<String, Scalar>::deserialize(d)?;
98 Ok(m.into_iter().map(|(k, v)| (k, v.into_string())).collect())
99}
100
101#[derive(Deserialize, JsonSchema)]
104#[serde(untagged, deny_unknown_fields)]
105pub(crate) enum EnvValue {
106 Scalar(Scalar),
107 Secret {
108 secret: String,
110 #[serde(default)]
113 on_change: Option<crate::spec::OnChange>,
114 #[serde(default, rename = "as")]
118 delivery: Option<crate::spec::SecretAs>,
119 },
120}
121
122#[derive(Deserialize, JsonSchema)]
125#[serde(untagged)]
126pub(crate) enum EnvMapOrList {
127 Map(std::collections::BTreeMap<String, EnvValue>),
128 List(Vec<String>),
129}
130
131#[derive(Deserialize, JsonSchema)]
133#[serde(untagged)]
134#[allow(dead_code)]
135pub(crate) enum MapOrList {
136 Map(std::collections::BTreeMap<String, Scalar>),
137 List(Vec<String>),
138}
139
140fn map_or_list<'de, D: Deserializer<'de>>(
141 d: D,
142 bare: impl Fn(&str) -> Result<String, String>,
143) -> Result<std::collections::BTreeMap<String, String>, D::Error> {
144 match MapOrList::deserialize(d)? {
145 MapOrList::Map(m) => Ok(m.into_iter().map(|(k, v)| (k, v.into_string())).collect()),
146 MapOrList::List(l) => l
147 .into_iter()
148 .map(|item| match item.split_once('=') {
149 Some((k, v)) => Ok((k.to_string(), v.to_string())),
150 None => bare(&item).map(|v| (item.clone(), v)),
151 })
152 .collect::<Result<_, _>>()
153 .map_err(D::Error::custom),
154 }
155}
156
157pub(crate) fn string_map_or_list<'de, D: Deserializer<'de>>(
160 d: D,
161) -> Result<std::collections::BTreeMap<String, String>, D::Error> {
162 map_or_list(d, |_| Ok(String::new()))
163}
164
165pub(crate) fn env_map_or_list<'de, D: Deserializer<'de>>(
169 d: D,
170) -> Result<std::collections::BTreeMap<String, String>, D::Error> {
171 map_or_list(d, |k| {
172 Err(format!(
173 "environment entry {k:?} has no value: write {k}=VALUE"
174 ))
175 })
176}
177
178#[derive(Deserialize, JsonSchema)]
180#[serde(untagged)]
181#[allow(dead_code)]
182pub(crate) enum Command {
183 String(String),
184 Argv(Vec<Scalar>),
185}
186
187pub(crate) fn opt_command<'de, D: Deserializer<'de>>(
188 d: D,
189) -> Result<Option<Vec<String>>, D::Error> {
190 match Command::deserialize(d)? {
191 Command::Argv(v) => Ok(Some(v.into_iter().map(Scalar::into_string).collect())),
192 Command::String(s) => split_words(&s).map(Some).map_err(D::Error::custom),
193 }
194}
195
196pub fn split_words(s: &str) -> Result<Vec<String>, String> {
200 let mut words = Vec::new();
201 let mut cur = String::new();
202 let mut in_word = false;
203 let mut chars = s.chars();
204 while let Some(c) = chars.next() {
205 match c {
206 c if c.is_whitespace() => {
207 if in_word {
208 words.push(std::mem::take(&mut cur));
209 in_word = false;
210 }
211 }
212 '\'' => {
213 in_word = true;
214 loop {
215 match chars.next() {
216 Some('\'') => break,
217 Some(c) => cur.push(c),
218 None => return Err(format!("unterminated ' in {s:?}")),
219 }
220 }
221 }
222 '"' => {
223 in_word = true;
224 loop {
225 match chars.next() {
226 Some('"') => break,
227 Some('\\') => match chars.next() {
228 Some(c @ ('"' | '\\' | '$' | '`')) => cur.push(c),
229 Some('\n') => {}
230 Some(c) => {
231 cur.push('\\');
232 cur.push(c);
233 }
234 None => return Err(format!("unterminated \" in {s:?}")),
235 },
236 Some(c) => cur.push(c),
237 None => return Err(format!("unterminated \" in {s:?}")),
238 }
239 }
240 }
241 '\\' => {
242 in_word = true;
243 match chars.next() {
244 Some('\n') => {}
245 Some(c) => cur.push(c),
246 None => return Err(format!("trailing \\ in {s:?}")),
247 }
248 }
249 c => {
250 in_word = true;
251 cur.push(c);
252 }
253 }
254 }
255 if in_word {
256 words.push(cur);
257 }
258 if words.is_empty() {
259 return Err("command is empty".into());
260 }
261 Ok(words)
262}
263
264pub(crate) fn string_map_map<'de, D: Deserializer<'de>>(
265 d: D,
266) -> Result<std::collections::BTreeMap<String, std::collections::BTreeMap<String, String>>, D::Error>
267{
268 let m = std::collections::BTreeMap::<String, std::collections::BTreeMap<String, Scalar>>::deserialize(d)?;
269 Ok(m.into_iter()
270 .map(|(k, v)| {
271 (
272 k,
273 v.into_iter().map(|(a, b)| (a, b.into_string())).collect(),
274 )
275 })
276 .collect())
277}
278
279pub fn parse_duration(s: &str) -> Result<std::time::Duration, String> {
281 let s = s.trim();
282 let (num, unit) = match s.find(|c: char| !c.is_ascii_digit() && c != '.') {
283 Some(i) => (&s[..i], s[i..].trim()),
284 None => (s, "s"),
285 };
286 let n: f64 = num
287 .parse()
288 .map_err(|_| format!("invalid duration {s:?} (use e.g. 90s, 5m, 1h)"))?;
289 let secs = match unit {
290 "ms" => n / 1000.0,
291 "s" | "sec" | "secs" => n,
292 "m" | "min" | "mins" => n * 60.0,
293 "h" => n * 3600.0,
294 "d" => n * 86400.0,
295 _ => return Err(format!("invalid duration unit in {s:?} (ms, s, m, h, d)")),
296 };
297 Ok(std::time::Duration::from_secs_f64(secs))
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303 use std::time::Duration;
304
305 #[test]
306 fn durations() {
307 assert_eq!(parse_duration("90").unwrap(), Duration::from_secs(90));
308 assert_eq!(parse_duration("90s").unwrap(), Duration::from_secs(90));
309 assert_eq!(parse_duration("5m").unwrap(), Duration::from_secs(300));
310 assert_eq!(parse_duration("1h").unwrap(), Duration::from_secs(3600));
311 assert_eq!(
312 parse_duration("90d").unwrap(),
313 Duration::from_secs(90 * 86400)
314 );
315 assert_eq!(
316 parse_duration("1500ms").unwrap(),
317 Duration::from_millis(1500)
318 );
319 assert!(parse_duration("5 parsecs").is_err());
320 }
321
322 #[test]
323 fn words() {
324 assert_eq!(
325 split_words(r#"sh -c 'bun install && exec bun run dev'"#).unwrap(),
326 ["sh", "-c", "bun install && exec bun run dev"]
327 );
328 assert_eq!(
329 split_words(r#"echo "a \"b\" $X" c\ d ''"#).unwrap(),
330 ["echo", r#"a "b" $X"#, "c d", ""]
331 );
332 assert!(split_words("echo 'oops").is_err());
333 assert!(split_words(" ").is_err());
334 }
335
336 #[test]
337 fn bools() {
338 assert_eq!(parse_bool("TRUE"), Some(true));
339 assert_eq!(parse_bool("off"), Some(false));
340 assert_eq!(parse_bool("maybe"), None);
341 }
342}