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