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