spin_expressions/
lib.rs

1pub mod provider;
2mod template;
3
4use std::{borrow::Cow, collections::HashMap, fmt::Debug};
5
6use spin_locked_app::Variable;
7
8pub use async_trait;
9
10pub use provider::Provider;
11use template::Part;
12pub use template::Template;
13
14/// A [`ProviderResolver`] that can be shared.
15pub type SharedPreparedResolver =
16    std::sync::Arc<std::sync::OnceLock<std::sync::Arc<PreparedResolver>>>;
17
18/// A [`Resolver`] which is extended by [`Provider`]s.
19#[derive(Debug, Default)]
20pub struct ProviderResolver {
21    internal: Resolver,
22    providers: Vec<Box<dyn Provider>>,
23}
24
25impl ProviderResolver {
26    /// Creates a Resolver for the given Tree.
27    pub fn new(variables: impl IntoIterator<Item = (String, Variable)>) -> Result<Self> {
28        Ok(Self {
29            internal: Resolver::new(variables)?,
30            providers: Default::default(),
31        })
32    }
33
34    /// Adds component variable values to the Resolver.
35    pub fn add_component_variables(
36        &mut self,
37        component_id: impl Into<String>,
38        variables: impl IntoIterator<Item = (String, String)>,
39    ) -> Result<()> {
40        self.internal
41            .add_component_variables(component_id, variables)
42    }
43
44    /// Adds a variable Provider to the Resolver.
45    pub fn add_provider(&mut self, provider: Box<dyn Provider>) {
46        self.providers.push(provider);
47    }
48
49    /// Resolves a variable value for the given path.
50    pub async fn resolve(&self, component_id: &str, key: Key<'_>) -> Result<String> {
51        let template = self.internal.get_template(component_id, key)?;
52        self.resolve_template(template).await
53    }
54
55    /// Resolves all variables for the given component.
56    pub async fn resolve_all(&self, component_id: &str) -> Result<Vec<(String, String)>> {
57        use futures::FutureExt;
58
59        let Some(keys2templates) = self.internal.component_configs.get(component_id) else {
60            return Ok(vec![]);
61        };
62
63        let resolve_futs = keys2templates.iter().map(|(key, template)| {
64            self.resolve_template(template)
65                .map(|r| r.map(|value| (key.to_string(), value)))
66        });
67
68        futures::future::try_join_all(resolve_futs).await
69    }
70
71    /// Resolves the given template.
72    pub async fn resolve_template(&self, template: &Template) -> Result<String> {
73        let mut resolved_parts: Vec<Cow<str>> = Vec::with_capacity(template.parts().len());
74        for part in template.parts() {
75            resolved_parts.push(match part {
76                Part::Lit(lit) => lit.as_ref().into(),
77                Part::Expr(var) => self.resolve_variable(var).await?.into(),
78            });
79        }
80        Ok(resolved_parts.concat())
81    }
82
83    /// Fully resolve all variables into a [`PreparedResolver`].
84    pub async fn prepare(&self) -> Result<PreparedResolver> {
85        let mut variables = HashMap::new();
86        for name in self.internal.variables.keys() {
87            let value = self.resolve_variable(name).await?;
88            variables.insert(name.clone(), value);
89        }
90        Ok(PreparedResolver { variables })
91    }
92
93    async fn resolve_variable(&self, key: &str) -> Result<String> {
94        for provider in &self.providers {
95            if let Some(value) = provider.get(&Key(key)).await.map_err(Error::Provider)? {
96                return Ok(value);
97            }
98        }
99        self.internal.resolve_variable(key)
100    }
101}
102
103/// A variable resolver.
104#[derive(Debug, Default)]
105pub struct Resolver {
106    // variable key -> variable
107    variables: HashMap<String, Variable>,
108    // component ID -> variable key -> variable value template
109    component_configs: HashMap<String, HashMap<String, Template>>,
110}
111
112impl Resolver {
113    /// Creates a Resolver for the given Tree.
114    pub fn new(variables: impl IntoIterator<Item = (String, Variable)>) -> Result<Self> {
115        let variables: HashMap<_, _> = variables.into_iter().collect();
116        // Validate keys so that we can rely on them during resolution
117        variables.keys().try_for_each(|key| Key::validate(key))?;
118        Ok(Self {
119            variables,
120            component_configs: Default::default(),
121        })
122    }
123
124    /// Adds component variable values to the Resolver.
125    pub fn add_component_variables(
126        &mut self,
127        component_id: impl Into<String>,
128        variables: impl IntoIterator<Item = (String, String)>,
129    ) -> Result<()> {
130        let component_id = component_id.into();
131        let templates = variables
132            .into_iter()
133            .map(|(key, val)| {
134                // Validate variable keys so that we can rely on them during resolution
135                Key::validate(&key)?;
136                let template = self.validate_template(val)?;
137                Ok((key, template))
138            })
139            .collect::<Result<_>>()?;
140
141        self.component_configs.insert(component_id, templates);
142
143        Ok(())
144    }
145
146    /// Resolves a variable value for the given path.
147    pub fn resolve(&self, component_id: &str, key: Key<'_>) -> Result<String> {
148        let template = self.get_template(component_id, key)?;
149        self.resolve_template(template)
150    }
151
152    /// Resolves the given template.
153    fn resolve_template(&self, template: &Template) -> Result<String> {
154        let mut resolved_parts: Vec<Cow<str>> = Vec::with_capacity(template.parts().len());
155        for part in template.parts() {
156            resolved_parts.push(match part {
157                Part::Lit(lit) => lit.as_ref().into(),
158                Part::Expr(var) => self.resolve_variable(var)?.into(),
159            });
160        }
161        Ok(resolved_parts.concat())
162    }
163
164    /// Gets a template for the given path.
165    fn get_template(&self, component_id: &str, key: Key<'_>) -> Result<&Template> {
166        let configs = self.component_configs.get(component_id).ok_or_else(|| {
167            Error::Undefined(format!("no variable for component {component_id:?}"))
168        })?;
169        let key = key.as_ref();
170        let template = configs
171            .get(key)
172            .ok_or_else(|| Error::Undefined(format!("no variable for {component_id:?}.{key:?}")))?;
173        Ok(template)
174    }
175
176    fn resolve_variable(&self, key: &str) -> Result<String> {
177        let var = self
178            .variables
179            .get(key)
180            // This should have been caught by validate_template
181            .ok_or_else(|| Error::InvalidName(key.to_string()))?;
182
183        var.default.clone().ok_or_else(|| {
184            Error::Provider(anyhow::anyhow!(
185                "no provider resolved required variable {key:?}"
186            ))
187        })
188    }
189
190    fn validate_template(&self, template: String) -> Result<Template> {
191        let template = Template::new(template)?;
192        // Validate template variables are valid
193        template.parts().try_for_each(|part| match part {
194            Part::Expr(var) if !self.variables.contains_key(var.as_ref()) => {
195                Err(Error::InvalidTemplate(format!("unknown variable {var:?}")))
196            }
197            _ => Ok(()),
198        })?;
199        Ok(template)
200    }
201}
202
203/// A resolver who has resolved all variables.
204#[derive(Default)]
205pub struct PreparedResolver {
206    variables: HashMap<String, String>,
207}
208
209impl PreparedResolver {
210    /// Resolves a the given template.
211    pub fn resolve_template(&self, template: &Template) -> Result<String> {
212        let mut resolved_parts: Vec<Cow<str>> = Vec::with_capacity(template.parts().len());
213        for part in template.parts() {
214            resolved_parts.push(match part {
215                Part::Lit(lit) => lit.as_ref().into(),
216                Part::Expr(var) => self.resolve_variable(var)?.into(),
217            });
218        }
219        Ok(resolved_parts.concat())
220    }
221
222    fn resolve_variable(&self, key: &str) -> Result<String> {
223        self.variables
224            .get(key)
225            .cloned()
226            .ok_or(Error::InvalidName(key.to_string()))
227    }
228}
229
230/// A variable key
231#[derive(Debug, PartialEq, Eq)]
232pub struct Key<'a>(&'a str);
233
234impl<'a> Key<'a> {
235    /// Creates a new Key.
236    pub fn new(key: &'a str) -> Result<Self> {
237        Self::validate(key)?;
238        Ok(Self(key))
239    }
240
241    pub fn as_str(&self) -> &str {
242        self.0
243    }
244
245    // To allow various (env var, file path) transformations:
246    // - must start with an ASCII letter
247    // - underscores are allowed; one at a time between other characters
248    // - all other characters must be ASCII alphanumeric
249    fn validate(key: &str) -> Result<()> {
250        {
251            if key.is_empty() {
252                Err("must not be empty".to_string())
253            } else if let Some(invalid) = key
254                .chars()
255                .find(|c| !(c.is_ascii_lowercase() || c.is_ascii_digit() || c == &'_'))
256            {
257                Err(format!("invalid character {:?}. Variable names may contain only lower-case letters, numbers, and underscores.", invalid))
258            } else if !key.bytes().next().unwrap().is_ascii_lowercase() {
259                Err("must start with a lowercase ASCII letter".to_string())
260            } else if !key.bytes().last().unwrap().is_ascii_alphanumeric() {
261                Err("must end with a lowercase ASCII letter or digit".to_string())
262            } else if key.contains("__") {
263                Err("must not contain multiple consecutive underscores".to_string())
264            } else {
265                Ok(())
266            }
267        }
268        .map_err(|reason| Error::InvalidName(format!("{key:?}: {reason}")))
269    }
270}
271
272impl<'a> TryFrom<&'a str> for Key<'a> {
273    type Error = Error;
274
275    fn try_from(value: &'a str) -> std::prelude::v1::Result<Self, Self::Error> {
276        Self::new(value)
277    }
278}
279
280impl AsRef<str> for Key<'_> {
281    fn as_ref(&self) -> &str {
282        self.0
283    }
284}
285
286pub type Result<T> = std::result::Result<T, Error>;
287
288/// A variable resolution error.
289#[derive(Debug, thiserror::Error)]
290pub enum Error {
291    /// Invalid variable name.
292    #[error("invalid variable name: {0}")]
293    InvalidName(String),
294
295    /// Invalid variable template.
296    #[error("invalid variable template: {0}")]
297    InvalidTemplate(String),
298
299    /// Variable provider error.
300    #[error("provider error: {0:?}")]
301    Provider(#[source] anyhow::Error),
302
303    /// Undefined variable.
304    #[error("undefined variable: {0}")]
305    Undefined(String),
306}
307
308#[cfg(test)]
309mod tests {
310    use async_trait::async_trait;
311
312    use super::*;
313
314    #[derive(Debug)]
315    struct TestProvider;
316
317    #[async_trait]
318    impl Provider for TestProvider {
319        async fn get(&self, key: &Key) -> anyhow::Result<Option<String>> {
320            match key.as_ref() {
321                "required" => Ok(Some("provider-value".to_string())),
322                "broken" => anyhow::bail!("broken"),
323                _ => Ok(None),
324            }
325        }
326    }
327
328    async fn test_resolve(template: &str) -> Result<String> {
329        let mut resolver = ProviderResolver::new([
330            (
331                "required".into(),
332                Variable {
333                    default: None,
334                    secret: false,
335                },
336            ),
337            (
338                "default".into(),
339                Variable {
340                    default: Some("default-value".into()),
341                    secret: false,
342                },
343            ),
344        ])
345        .unwrap();
346        resolver
347            .add_component_variables("test-component", [("test_key".into(), template.into())])
348            .unwrap();
349        resolver.add_provider(Box::new(TestProvider));
350        resolver.resolve("test-component", Key("test_key")).await
351    }
352
353    #[tokio::test]
354    async fn resolve_static() {
355        assert_eq!(test_resolve("static-value").await.unwrap(), "static-value");
356    }
357
358    #[tokio::test]
359    async fn resolve_variable_default() {
360        assert_eq!(
361            test_resolve("prefix-{{ default }}-suffix").await.unwrap(),
362            "prefix-default-value-suffix"
363        );
364    }
365
366    #[tokio::test]
367    async fn resolve_variable_provider() {
368        assert_eq!(
369            test_resolve("prefix-{{ required }}-suffix").await.unwrap(),
370            "prefix-provider-value-suffix"
371        );
372    }
373
374    #[test]
375    fn keys_good() {
376        for key in ["a", "abc", "a1b2c3", "a_1", "a_1_b_3"] {
377            Key::new(key).expect(key);
378        }
379    }
380
381    #[test]
382    fn keys_bad() {
383        for key in ["", "aX", "1bc", "_x", "x.y", "x_", "a__b", "x-y"] {
384            Key::new(key).expect_err(key);
385        }
386    }
387
388    #[test]
389    fn template_literal() {
390        assert!(Template::new("hello").unwrap().is_literal());
391        assert!(!Template::new("hello {{ world }}").unwrap().is_literal());
392    }
393}