1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
//! Main module of the library that handles parsing of the input string and loading values from the
//! relevant loaders.
//!
//! Allows for custom loaders to be used via the `add_custom_loader` method
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;

/// A `Seed` is responsible for parsing the template string, loading the values, and optionally
/// making the replacements via the germinate method
pub struct Seed<'a> {
    template: &'a str,
    loaders: HashMap<Source, Box<dyn Loader>>,
}

impl<'a> Seed<'a> {
    /// Create a new `Seed` with the given template string
    pub fn new(template: &'a str) -> Self {
        Self {
            template,
            loaders: HashMap::new(),
        }
    }

    /// Adds a custom loader to allow users of the library to add their own value sources
    ///
    /// # Example
    /// ```
    /// use germinate::{Seed, Loader};
    ///
    /// struct LanguageLoader {}
    ///
    /// #[async_trait::async_trait]
    /// impl Loader for LanguageLoader {
    ///     async fn load(&self, key: &str) -> anyhow::Result<String> {
    ///         // Add your logic for loading the value here
    ///
    ///         Ok(match key {
    ///             "go" => String::from("Go"),
    ///             _ => String::from("Rust"),
    ///         })
    ///     }
    /// }
    ///
    /// #[tokio::main]
    /// async fn main() {
    ///     std::env::set_var("NAME", "John");
    ///
    ///     let mut seed = Seed::new("Hi %env:NAME%, Welcome to %language:rust%! Say goodbye to %language:go%...");
    ///     seed.add_custom_loader("language".into(), Box::new(LanguageLoader{}));
    ///     let output = seed.germinate().await.unwrap();
    ///
    ///     assert_eq!(String::from("Hi John, Welcome to Rust! Say goodbye to Go..."), output);
    /// }
    /// ```
    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 a loader with the given key exists, return it
        if self.loaders.contains_key(source) {
            // Unwrap should be safe here as we know the key exists
            return Ok(self.loaders.get(source).unwrap().as_ref());
        }

        // Instantiate a new loader for the given key. If the key is for a custom source, we return
        // an error as that should have been set using the add_custom_loader function before
        // parsing
        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
                    )
                ),
        };

        // Store the new loader
        self.loaders.insert(source.clone(), loader);

        // Return a reference to the newly created loader
        Ok(self.loaders.get(source).unwrap().as_ref())
    }

    /// Parses the template string and generates a `HashMap` of key value replacements, loading the
    /// value for each replacement as it goes. If it finds a template string with a custom source
    /// without a related loader, it will return an error. It will also return an error if a value
    /// fails to load
    ///
    /// # Examples
    /// ```
    /// #[tokio::main]
    /// async fn main() {
    ///     std::env::set_var("NAME", "John");
    ///
    ///     let mut seed = germinate::Seed::new("Hi %env:NAME%, Welcome to Rust!");
    ///     let replacements = seed.parse().await.unwrap();
    ///
    ///     assert_eq!(replacements.get("%env:NAME%").unwrap(), &String::from("John"));
    /// }
    /// ```
    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) {
            // capture[1] will be the find string. If the map contains the key then we have already
            // processed this replacement
            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")?;

            // This is the key to use when loading the value
            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)
    }

    /// The germinate is a wrapper around the parse function which follows up by actually making
    /// the replacements in the template string and returning the result.
    ///
    /// # Examples
    /// ```
    /// #[tokio::main]
    /// async fn main() {
    ///     std::env::set_var("NAME", "John");
    ///
    ///     let mut seed = germinate::Seed::new("Hi %env:NAME%, Welcome to Rust!");
    ///     let output = seed.germinate().await.unwrap();
    ///
    ///     assert_eq!(String::from("Hi John, Welcome to Rust!"), output);
    /// }
    ///
    /// ```
    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);
    }
}