use crate::loader::{Loader, Source};
use anyhow::{anyhow, Context, Result};
use regex::Regex;
use std::collections::HashMap;
#[cfg(feature = "aws")]
use crate::loader::awsec2metadata::AwsEc2MetadataLoader;
#[cfg(feature = "aws")]
use crate::loader::awsec2tag::AwsEc2TagLoader;
#[cfg(feature = "aws")]
use crate::loader::awsssm::AwsSsmLoader;
use crate::loader::env::EnvironmentLoader;
pub struct Seed<'a> {
template: &'a str,
loaders: HashMap<Source, Box<dyn Loader>>,
}
impl<'a> Seed<'a> {
pub fn new(template: &'a str) -> Self {
Self {
template,
loaders: HashMap::new(),
}
}
pub fn add_custom_loader(&mut self, key: String, loader: Box<dyn Loader>) {
self.loaders.insert(Source::Custom(key), loader);
}
async fn get_loader(&mut self, source: &Source) -> Result<&dyn Loader> {
if self.loaders.contains_key(source) {
return Ok(self.loaders.get(source).unwrap().as_ref());
}
let loader: Box<dyn Loader> = match source {
#[cfg(feature = "aws")]
Source::AwsEc2Tag => Box::new(AwsEc2TagLoader::new().await?),
#[cfg(feature = "aws")]
Source::AwsEc2Metadata => Box::new(AwsEc2MetadataLoader::new()),
#[cfg(feature = "aws")]
Source::AwsSsm => Box::new(AwsSsmLoader::new().await?),
Source::Environment => Box::new(EnvironmentLoader::new()),
Source::Custom(key) => return Err(
anyhow!(
"Unsupported value source: {}. If you're using a custom source, make sure you added the loader before parsing",
key
)
),
};
self.loaders.insert(source.clone(), loader);
Ok(self.loaders.get(source).unwrap().as_ref())
}
pub async fn parse(&mut self) -> Result<HashMap<String, String>> {
let mut replacements = HashMap::new();
let pattern = Regex::new(r"(%([a-z0-9]+):([^%]+)%)").unwrap();
for capture in pattern.captures_iter(self.template) {
if replacements.contains_key(&capture[1].to_string()) {
continue;
}
let source = Source::from(&capture[2]);
let loader = self
.get_loader(&source)
.await
.context("Failed to parse template string")?;
let key = &capture[3];
let value = loader
.load(&key.to_string())
.await
.context("Failed to load value")?;
replacements.insert(capture[1].to_string(), value);
}
Ok(replacements)
}
pub async fn germinate(&mut self) -> Result<String> {
let mut output = self.template.to_string();
for (k, v) in self.parse().await? {
output = output.replace(&k, &v);
}
Ok(output)
}
}
#[cfg(test)]
mod test {
use super::Seed;
use crate::Loader;
use anyhow::Result;
struct TestLoader {
value: String,
}
impl TestLoader {
pub fn with_value(value: String) -> Self {
Self { value }
}
}
#[async_trait::async_trait]
impl Loader for TestLoader {
async fn load(&self, _: &str) -> Result<String> {
Ok(self.value.clone())
}
}
#[tokio::test]
async fn test_germinate_basic() {
std::env::set_var("TEST_VAR", "Test");
let mut seed = Seed::new("Test %env:TEST_VAR% Test");
let output = seed.germinate().await.unwrap();
assert_eq!(String::from("Test Test Test"), output);
}
#[tokio::test]
async fn test_geminate_with_custom_loader() {
let mut seed = Seed::new("Test %custom:test% Test");
seed.add_custom_loader(
"custom".into(),
Box::new(TestLoader::with_value("Test".into())),
);
let output = seed.germinate().await.unwrap();
assert_eq!(String::from("Test Test Test"), output);
}
}