1use 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
37pub 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 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
101pub 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}