akv_cli/
parsing.rs

1// Copyright 2025 Heath Stewart.
2// Licensed under the MIT License. See LICENSE.txt in the project root for license information.
3
4//! Parsing utility functions.
5
6use crate::{Error, ErrorKind, Result, ResultExt};
7use futures::future::BoxFuture;
8use std::{borrow::Cow, io, str::FromStr};
9use time::{format_description::well_known, OffsetDateTime};
10
11/// Parse an optional [`OffsetDateTime`] argument from `clap`.
12pub fn parse_date_time_opt(value: &str) -> Result<OffsetDateTime> {
13    OffsetDateTime::parse(value, &well_known::Rfc3339)
14        .with_context(ErrorKind::InvalidData, "failed to parse date-time")
15}
16
17/// Parse a `key=value` argument from `clap`.
18pub fn parse_key_value<T>(value: &str) -> Result<(String, T)>
19where
20    T: FromStr,
21    Error: From<<T as FromStr>::Err>,
22{
23    let idx = value
24        .find("=")
25        .ok_or_else(|| format!("no '=' found in '{value}'"))?;
26    Ok((value[..idx].to_string(), value[idx + 1..].parse()?))
27}
28
29/// Parse an `key=(value)` argument from `clap` where `value` is optional.
30pub fn parse_key_value_opt<T>(value: &str) -> Result<(String, Option<T>)>
31where
32    T: FromStr,
33    Error: From<<T as FromStr>::Err>,
34{
35    if let Some(idx) = value.find("=") {
36        return Ok((value[..idx].to_string(), Some(value[idx + 1..].parse()?)));
37    }
38
39    Ok((value.to_string(), None))
40}
41
42/// Replaces variables between `{{ }}` with text returned from function.
43///
44/// The function `f` receives a variable named (trimmed of leading and trailing whitespace)
45/// and must return an `Ok(String)` or an `Err`, which will terminate the replacement. Any text written to
46/// the [`io::Write`] `w` will be left as-is.
47///
48/// Overlapping templates e.g., `{{ {{foo}} }}` with attempt to replace the text
49/// between the first occurrences of both `{{` and `}}`, so the function would receive `{{foo`
50/// and, even if the function returned `Ok("".to_string())`, would still contain the final `}}` in the text.
51///
52/// # Examples
53///
54/// ```
55/// use akv_cli::parsing::replace_expressions;
56/// # use futures::{FutureExt, StreamExt};
57///
58/// # async fn test_replace_expressions() {
59/// let s = "Hello, {{ var }}!";
60/// let mut buf = Vec::new();
61///
62/// replace_expressions(s, &mut buf, |v| {
63///     assert_eq!(v, "var");
64///     async { Ok(String::from("world")) }.boxed()
65/// }).await.unwrap();
66///
67/// assert_eq!(String::from_utf8(buf).unwrap(), "Hello, world!");
68/// # }
69/// ```
70pub async fn replace_expressions<W, F>(mut template: &str, w: &mut W, f: F) -> Result<()>
71where
72    W: io::Write,
73    F: Fn(&str) -> BoxFuture<'_, Result<String>>,
74{
75    const START: &str = "{{";
76    const START_LEN: usize = START.len();
77    const END: &str = "}}";
78    const END_LEN: usize = END.len();
79
80    while let Some(mut start) = template.find(START) {
81        // Start only after the first "{{".
82        let Some(mut end) = template[start + START_LEN..].find(END) else {
83            return Err(Error::with_message(
84                ErrorKind::InvalidData,
85                "missing closing '}}'",
86            ));
87        };
88        end += start + START_LEN;
89
90        w.write_all(&template.as_bytes()[..start])?;
91        start += START_LEN;
92
93        let id = template[start..end].trim();
94        let secret = f(id).await?;
95
96        w.write_all(secret.as_bytes())?;
97        end += END_LEN;
98
99        template = &template[end..];
100    }
101
102    w.write_all(template.as_bytes())?;
103    Ok(())
104}
105
106/// Replaces variables in the form $VAR_NAME with text returned from a function.
107pub fn replace_vars<F>(input: &str, f: F) -> Result<Cow<'_, str>>
108where
109    F: Fn(&str) -> Result<String>,
110{
111    let mut cur = input;
112    let mut output = String::new();
113
114    while let Some(start) = cur.find('$') {
115        output += &cur[..start];
116        cur = &cur[start + 1..];
117
118        let mut end = cur.len();
119        for (i, c) in cur.char_indices() {
120            if !c.is_ascii_alphanumeric() && c != '_' {
121                end = i;
122                break;
123            }
124        }
125
126        let name = &cur[..end];
127        if !name.is_empty() {
128            output += &f(name)?;
129        }
130        cur = &cur[end..];
131    }
132
133    if output.is_empty() {
134        Ok(Cow::Borrowed(input))
135    } else {
136        output += cur;
137        Ok(Cow::Owned(output))
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144    use futures::FutureExt as _;
145
146    #[test]
147    fn test_parse_key_value() {
148        let kv = parse_key_value::<String>("key=value");
149        assert!(matches!(kv, Ok(kv) if kv.0 == "key" && kv.1 == "value"));
150
151        let kv = parse_key_value::<String>("key=value=other");
152        assert!(matches!(kv, Ok(kv) if kv.0 == "key" && kv.1 == "value=other"));
153
154        parse_key_value::<String>("key").expect_err("requires '='");
155
156        let k = parse_key_value::<i32>("key=1");
157        assert!(matches!(k, Ok(k) if k.0 == "key" && k.1 == 1));
158
159        parse_key_value::<i32>("key=value").expect_err("should not parse 'value' as i32");
160    }
161
162    #[test]
163    fn test_parse_key_value_opt() {
164        let kv = parse_key_value_opt::<String>("key=value");
165        assert!(matches!(kv, Ok(kv) if kv.0 == "key" && kv.1 == Some("value".into())));
166
167        let kv = parse_key_value_opt::<String>("key=value=other");
168        assert!(matches!(kv, Ok(kv) if kv.0 == "key" && kv.1 == Some("value=other".into())));
169
170        let k = parse_key_value_opt::<i32>("key");
171        assert!(matches!(k, Ok(k) if k.0 == "key" && k.1.is_none()));
172
173        parse_key_value_opt::<i32>("key=value").expect_err("should not parse 'value' as i32");
174    }
175
176    #[tokio::test]
177    async fn test_replace_expressions() {
178        let s = "Hello, {{ var }}!";
179        let mut buf = Vec::new();
180
181        replace_expressions(s, &mut buf, |v| {
182            assert_eq!(v, "var");
183            async { Ok(String::from("world")) }.boxed()
184        })
185        .await
186        .unwrap();
187        assert_eq!(String::from_utf8(buf).unwrap(), "Hello, world!");
188    }
189
190    #[tokio::test]
191    async fn replace_expressions_overlap() {
192        let s = "Hello, {{ {{var}} }}!";
193        let mut buf = Vec::new();
194
195        replace_expressions(s, &mut buf, |v| {
196            assert_eq!(v, "{{var");
197            async { Ok(String::from("world")) }.boxed()
198        })
199        .await
200        .unwrap();
201        assert_eq!(String::from_utf8(buf).unwrap(), "Hello, world }}!");
202    }
203
204    #[tokio::test]
205    async fn replace_expressions_missing_end() {
206        let s = "Hello, {{ var!";
207        let mut buf = Vec::new();
208
209        replace_expressions(s, &mut buf, |_| async { Ok(String::from("world")) }.boxed())
210            .await
211            .expect_err("missing end");
212    }
213
214    #[tokio::test]
215    async fn replace_expressions_missing_empty() {
216        let s = "";
217        let mut buf = Vec::new();
218
219        replace_expressions(s, &mut buf, |_| async { Ok(String::from("world")) }.boxed())
220            .await
221            .unwrap();
222        assert_eq!(String::from_utf8(buf).unwrap(), "");
223    }
224
225    #[tokio::test]
226    async fn replace_expressions_missing_no_template() {
227        let s = "Hello, world!";
228        let mut buf = Vec::new();
229
230        replace_expressions(s, &mut buf, |_| {
231            async { Ok(String::from("Ferris")) }.boxed()
232        })
233        .await
234        .unwrap();
235        assert_eq!(String::from_utf8(buf).unwrap(), "Hello, world!");
236    }
237
238    #[test]
239    fn replace_vars_borrowed() {
240        let s = "echo NONE";
241        let out = replace_vars(s, |name| {
242            assert_eq!(name, "VAR");
243            Ok(String::from("VALUE"))
244        })
245        .expect("replaces $VAR with VALUE");
246        assert!(matches!(out, Cow::Borrowed(out) if out == "echo NONE"));
247    }
248
249    #[test]
250    fn replace_vars_owned() {
251        let s = "echo $VAR";
252        let out = replace_vars(s, |name| {
253            assert_eq!(name, "VAR");
254            Ok(String::from("VALUE"))
255        })
256        .expect("replaces $VAR with VALUE");
257        assert!(matches!(out, Cow::Owned(out) if out == "echo VALUE"));
258    }
259
260    #[test]
261    fn replace_only_vars() {
262        let s = "$VAR";
263        let out = replace_vars(s, |name| {
264            assert_eq!(name, "VAR");
265            Ok(String::from("VALUE"))
266        })
267        .expect("replaces $VAR with VALUE");
268        assert!(matches!(out, Cow::Owned(out) if out == "VALUE"));
269    }
270
271    #[test]
272    fn replace_vars_errs() {
273        let s = "echo $VAR";
274        replace_vars(s, |name| {
275            assert_eq!(name, "VAR");
276            Err(Error::with_message(ErrorKind::Other, "test"))
277        })
278        .expect_err("expected error");
279    }
280
281    #[tokio::test]
282    async fn replace_expression_with_var() {
283        let s = "Hello, {{ $VAR }}!";
284        let mut buf = Vec::new();
285
286        replace_expressions(s, &mut buf, |expr| {
287            async move {
288                assert_eq!(expr, "$VAR");
289                replace_vars(expr, |var| {
290                    assert_eq!(var, "VAR");
291                    Ok(String::from("world"))
292                })
293                .map(Into::into)
294            }
295            .boxed()
296        })
297        .await
298        .expect("replaces $VAR with 'world'");
299        assert_eq!(String::from_utf8(buf).unwrap(), "Hello, world!");
300    }
301}