use crate::{Error, ErrorKind, Result};
use futures::future::BoxFuture;
use std::{borrow::Cow, io, str::FromStr};
pub fn parse_key_value<T>(value: &str) -> Result<(String, T)>
where
T: FromStr,
Error: From<<T as FromStr>::Err>,
{
let idx = value
.find("=")
.ok_or_else(|| format!("no '=' found in '{value}'"))?;
Ok((value[..idx].to_string(), value[idx + 1..].parse()?))
}
pub fn parse_key_value_opt<T>(value: &str) -> Result<(String, Option<T>)>
where
T: FromStr,
Error: From<<T as FromStr>::Err>,
{
if let Some(idx) = value.find("=") {
return Ok((value[..idx].to_string(), Some(value[idx + 1..].parse()?)));
}
Ok((value.to_string(), None))
}
pub async fn replace_expressions<W, F>(mut template: &str, w: &mut W, f: F) -> Result<()>
where
W: io::Write,
F: Fn(&str) -> BoxFuture<'_, Result<String>>,
{
const START: &str = "{{";
const START_LEN: usize = START.len();
const END: &str = "}}";
const END_LEN: usize = END.len();
while let Some(mut start) = template.find(START) {
let Some(mut end) = template[start + START_LEN..].find(END) else {
return Err(Error::with_message(
ErrorKind::InvalidData,
"missing closing '}}'",
));
};
end += start + START_LEN;
w.write_all(&template.as_bytes()[..start])?;
start += START_LEN;
let id = template[start..end].trim();
let secret = f(id).await?;
w.write_all(secret.as_bytes())?;
end += END_LEN;
template = &template[end..];
}
w.write_all(template.as_bytes())?;
Ok(())
}
pub fn replace_vars<F>(input: &str, f: F) -> Result<Cow<'_, str>>
where
F: Fn(&str) -> Result<String>,
{
let mut cur = input;
let mut output = String::new();
while let Some(start) = cur.find('$') {
output += &cur[..start];
cur = &cur[start + 1..];
let mut end = cur.len();
for (i, c) in cur.char_indices() {
if !c.is_ascii_alphanumeric() && c != '_' {
end = i;
break;
}
}
let name = &cur[..end];
if !name.is_empty() {
output += &f(name)?;
}
cur = &cur[end..];
}
if output.is_empty() {
Ok(Cow::Borrowed(input))
} else {
output += cur;
Ok(Cow::Owned(output))
}
}
#[cfg(test)]
mod tests {
use super::*;
use futures::FutureExt as _;
#[test]
fn test_parse_key_value() {
let kv = parse_key_value::<String>("key=value");
assert!(matches!(kv, Ok(kv) if kv.0 == "key" && kv.1 == "value"));
let kv = parse_key_value::<String>("key=value=other");
assert!(matches!(kv, Ok(kv) if kv.0 == "key" && kv.1 == "value=other"));
parse_key_value::<String>("key").expect_err("requires '='");
let k = parse_key_value::<i32>("key=1");
assert!(matches!(k, Ok(k) if k.0 == "key" && k.1 == 1));
parse_key_value::<i32>("key=value").expect_err("should not parse 'value' as i32");
}
#[test]
fn test_parse_key_value_opt() {
let kv = parse_key_value_opt::<String>("key=value");
assert!(matches!(kv, Ok(kv) if kv.0 == "key" && kv.1 == Some("value".into())));
let kv = parse_key_value_opt::<String>("key=value=other");
assert!(matches!(kv, Ok(kv) if kv.0 == "key" && kv.1 == Some("value=other".into())));
let k = parse_key_value_opt::<i32>("key");
assert!(matches!(k, Ok(k) if k.0 == "key" && k.1.is_none()));
parse_key_value_opt::<i32>("key=value").expect_err("should not parse 'value' as i32");
}
#[tokio::test]
async fn test_replace_expressions() {
let s = "Hello, {{ var }}!";
let mut buf = Vec::new();
replace_expressions(s, &mut buf, |v| {
assert_eq!(v, "var");
async { Ok(String::from("world")) }.boxed()
})
.await
.unwrap();
assert_eq!(String::from_utf8(buf).unwrap(), "Hello, world!");
}
#[tokio::test]
async fn replace_expressions_overlap() {
let s = "Hello, {{ {{var}} }}!";
let mut buf = Vec::new();
replace_expressions(s, &mut buf, |v| {
assert_eq!(v, "{{var");
async { Ok(String::from("world")) }.boxed()
})
.await
.unwrap();
assert_eq!(String::from_utf8(buf).unwrap(), "Hello, world }}!");
}
#[tokio::test]
async fn replace_expressions_missing_end() {
let s = "Hello, {{ var!";
let mut buf = Vec::new();
replace_expressions(s, &mut buf, |_| async { Ok(String::from("world")) }.boxed())
.await
.expect_err("missing end");
}
#[tokio::test]
async fn replace_expressions_missing_empty() {
let s = "";
let mut buf = Vec::new();
replace_expressions(s, &mut buf, |_| async { Ok(String::from("world")) }.boxed())
.await
.unwrap();
assert_eq!(String::from_utf8(buf).unwrap(), "");
}
#[tokio::test]
async fn replace_expressions_missing_no_template() {
let s = "Hello, world!";
let mut buf = Vec::new();
replace_expressions(s, &mut buf, |_| {
async { Ok(String::from("Ferris")) }.boxed()
})
.await
.unwrap();
assert_eq!(String::from_utf8(buf).unwrap(), "Hello, world!");
}
#[test]
fn replace_vars_borrowed() {
let s = "echo NONE";
let out = replace_vars(s, |name| {
assert_eq!(name, "VAR");
Ok(String::from("VALUE"))
})
.expect("replaces $VAR with VALUE");
assert!(matches!(out, Cow::Borrowed(out) if out == "echo NONE"));
}
#[test]
fn replace_vars_owned() {
let s = "echo $VAR";
let out = replace_vars(s, |name| {
assert_eq!(name, "VAR");
Ok(String::from("VALUE"))
})
.expect("replaces $VAR with VALUE");
assert!(matches!(out, Cow::Owned(out) if out == "echo VALUE"));
}
#[test]
fn replace_only_vars() {
let s = "$VAR";
let out = replace_vars(s, |name| {
assert_eq!(name, "VAR");
Ok(String::from("VALUE"))
})
.expect("replaces $VAR with VALUE");
assert!(matches!(out, Cow::Owned(out) if out == "VALUE"));
}
#[test]
fn replace_vars_errs() {
let s = "echo $VAR";
replace_vars(s, |name| {
assert_eq!(name, "VAR");
Err(Error::with_message(ErrorKind::Other, "test"))
})
.expect_err("expected error");
}
#[tokio::test]
async fn replace_expression_with_var() {
let s = "Hello, {{ $VAR }}!";
let mut buf = Vec::new();
replace_expressions(s, &mut buf, |expr| {
async move {
assert_eq!(expr, "$VAR");
replace_vars(expr, |var| {
assert_eq!(var, "VAR");
Ok(String::from("world"))
})
.map(Into::into)
}
.boxed()
})
.await
.expect("replaces $VAR with 'world'");
assert_eq!(String::from_utf8(buf).unwrap(), "Hello, world!");
}
}