1use 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
31pub 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 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.as_bytes()[..start])?;
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
95pub 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}