spin_expressions/
lib.rs

1pub mod provider;
2mod template;
3
4use std::{borrow::Cow, collections::HashMap, fmt::Debug, vec};
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    /// Ensures that all required variables are not unresolvable
94    pub fn ensure_required_variables_resolvable(&self) -> Result<()> {
95        let mut unresolvable_keys = vec![];
96        for key in self.internal.required_variables() {
97            let key = Key::new(key)?;
98            let resolvable = self
99                .providers
100                .iter()
101                .any(|provider| provider.may_resolve(&key));
102            if !resolvable {
103                unresolvable_keys.push(key);
104            }
105        }
106
107        if unresolvable_keys.is_empty() {
108            Ok(())
109        } else {
110            Err(Error::Provider(anyhow::anyhow!(
111                "no provider resolved required variable(s): {unresolvable_keys:?}",
112            )))
113        }
114    }
115
116    async fn resolve_variable(&self, key: &str) -> Result<String> {
117        for provider in &self.providers {
118            if let Some(value) = provider.get(&Key(key)).await.map_err(Error::Provider)? {
119                return Ok(value);
120            }
121        }
122        self.internal.resolve_variable(key)
123    }
124}
125
126/// A variable resolver.
127#[derive(Debug, Default)]
128pub struct Resolver {
129    // variable key -> variable
130    variables: HashMap<String, Variable>,
131    // component ID -> variable key -> variable value template
132    component_configs: HashMap<String, HashMap<String, Template>>,
133}
134
135impl Resolver {
136    /// Creates a Resolver for the given Tree.
137    pub fn new(variables: impl IntoIterator<Item = (String, Variable)>) -> Result<Self> {
138        let variables: HashMap<_, _> = variables.into_iter().collect();
139        // Validate keys so that we can rely on them during resolution
140        variables.keys().try_for_each(|key| Key::validate(key))?;
141        Ok(Self {
142            variables,
143            component_configs: Default::default(),
144        })
145    }
146
147    /// Adds component variable values to the Resolver.
148    pub fn add_component_variables(
149        &mut self,
150        component_id: impl Into<String>,
151        variables: impl IntoIterator<Item = (String, String)>,
152    ) -> Result<()> {
153        let component_id = component_id.into();
154        let templates = variables
155            .into_iter()
156            .map(|(key, val)| {
157                // Validate variable keys so that we can rely on them during resolution
158                Key::validate(&key)?;
159                let template = self.validate_template(val)?;
160                Ok((key, template))
161            })
162            .collect::<Result<_>>()?;
163
164        self.component_configs.insert(component_id, templates);
165
166        Ok(())
167    }
168
169    /// Resolves a variable value for the given path.
170    pub fn resolve(&self, component_id: &str, key: Key<'_>) -> Result<String> {
171        let template = self.get_template(component_id, key)?;
172        self.resolve_template(template)
173    }
174
175    /// Resolves the given template.
176    pub fn resolve_template(&self, template: &Template) -> Result<String> {
177        let mut resolved_parts: Vec<Cow<str>> = Vec::with_capacity(template.parts().len());
178        for part in template.parts() {
179            resolved_parts.push(match part {
180                Part::Lit(lit) => lit.as_ref().into(),
181                Part::Expr(var) => self.resolve_variable(var)?.into(),
182            });
183        }
184        Ok(resolved_parts.concat())
185    }
186
187    /// Gets a template for the given path.
188    fn get_template(&self, component_id: &str, key: Key<'_>) -> Result<&Template> {
189        let configs = self.component_configs.get(component_id).ok_or_else(|| {
190            Error::Undefined(format!("no variable for component {component_id:?}"))
191        })?;
192        let key = key.as_ref();
193        let template = configs
194            .get(key)
195            .ok_or_else(|| Error::Undefined(format!("no variable for {component_id:?}.{key:?}")))?;
196        Ok(template)
197    }
198
199    fn resolve_variable(&self, key: &str) -> Result<String> {
200        let var = self
201            .variables
202            .get(key)
203            // This should have been caught by validate_template
204            .ok_or_else(|| Error::InvalidName(key.to_string()))?;
205
206        var.default.clone().ok_or_else(|| {
207            Error::Provider(anyhow::anyhow!(
208                "no provider resolved required variable {key:?}"
209            ))
210        })
211    }
212
213    fn validate_template(&self, template: String) -> Result<Template> {
214        let template = Template::new(template)?;
215        // Validate template variables are valid
216        template.parts().try_for_each(|part| match part {
217            Part::Expr(var) if !self.variables.contains_key(var.as_ref()) => {
218                Err(Error::InvalidTemplate(format!("unknown variable {var:?}")))
219            }
220            _ => Ok(()),
221        })?;
222        Ok(template)
223    }
224
225    fn required_variables(&self) -> impl Iterator<Item = &str> {
226        self.variables
227            .iter()
228            .filter_map(|(name, variable)| variable.default.is_none().then_some(name.as_str()))
229    }
230}
231
232/// A resolver who has resolved all variables.
233#[derive(Default)]
234pub struct PreparedResolver {
235    variables: HashMap<String, String>,
236}
237
238impl PreparedResolver {
239    /// Resolves a the given template.
240    pub fn resolve_template(&self, template: &Template) -> Result<String> {
241        let mut resolved_parts: Vec<Cow<str>> = Vec::with_capacity(template.parts().len());
242        for part in template.parts() {
243            resolved_parts.push(match part {
244                Part::Lit(lit) => lit.as_ref().into(),
245                Part::Expr(var) => self.resolve_variable(var)?.into(),
246            });
247        }
248        Ok(resolved_parts.concat())
249    }
250
251    fn resolve_variable(&self, key: &str) -> Result<String> {
252        self.variables
253            .get(key)
254            .cloned()
255            .ok_or(Error::InvalidName(key.to_string()))
256    }
257}
258
259/// A variable key
260#[derive(Debug, PartialEq, Eq)]
261pub struct Key<'a>(&'a str);
262
263impl<'a> Key<'a> {
264    /// Creates a new Key.
265    pub fn new(key: &'a str) -> Result<Self> {
266        Self::validate(key)?;
267        Ok(Self(key))
268    }
269
270    pub fn as_str(&self) -> &str {
271        self.0
272    }
273
274    // To allow various (env var, file path) transformations:
275    // - must start with an ASCII letter
276    // - underscores are allowed; one at a time between other characters
277    // - all other characters must be ASCII alphanumeric
278    fn validate(key: &str) -> Result<()> {
279        {
280            if key.is_empty() {
281                Err("must not be empty".to_string())
282            } else if let Some(invalid) = key
283                .chars()
284                .find(|c| !(c.is_ascii_lowercase() || c.is_ascii_digit() || c == &'_'))
285            {
286                Err(format!("invalid character {invalid:?}. Variable names may contain only lower-case letters, numbers, and underscores."))
287            } else if !key.bytes().next().unwrap().is_ascii_lowercase() {
288                Err("must start with a lowercase ASCII letter".to_string())
289            } else if !key.bytes().last().unwrap().is_ascii_alphanumeric() {
290                Err("must end with a lowercase ASCII letter or digit".to_string())
291            } else if key.contains("__") {
292                Err("must not contain multiple consecutive underscores".to_string())
293            } else {
294                Ok(())
295            }
296        }
297        .map_err(|reason| Error::InvalidName(format!("{key:?}: {reason}")))
298    }
299}
300
301impl<'a> TryFrom<&'a str> for Key<'a> {
302    type Error = Error;
303
304    fn try_from(value: &'a str) -> std::prelude::v1::Result<Self, Self::Error> {
305        Self::new(value)
306    }
307}
308
309impl AsRef<str> for Key<'_> {
310    fn as_ref(&self) -> &str {
311        self.0
312    }
313}
314
315pub type Result<T> = std::result::Result<T, Error>;
316
317/// A variable resolution error.
318#[derive(Debug, thiserror::Error)]
319pub enum Error {
320    /// Invalid variable name.
321    #[error("invalid variable name: {0}")]
322    InvalidName(String),
323
324    /// Invalid variable template.
325    #[error("invalid variable template: {0}")]
326    InvalidTemplate(String),
327
328    /// Variable provider error.
329    #[error("provider error: {0:?}")]
330    Provider(#[source] anyhow::Error),
331
332    /// Undefined variable.
333    #[error("undefined variable: {0}")]
334    Undefined(String),
335}
336
337#[cfg(test)]
338mod tests {
339    use async_trait::async_trait;
340
341    use super::*;
342
343    #[derive(Debug)]
344    struct TestProvider;
345
346    #[async_trait]
347    impl Provider for TestProvider {
348        async fn get(&self, key: &Key) -> anyhow::Result<Option<String>> {
349            match key.as_ref() {
350                "required" => Ok(Some("provider-value".to_string())),
351                "broken" => anyhow::bail!("broken"),
352                _ => Ok(None),
353            }
354        }
355
356        fn may_resolve(&self, key: &Key) -> bool {
357            key.as_ref() == "required"
358        }
359    }
360
361    async fn test_resolve(template: &str) -> Result<String> {
362        let mut resolver = ProviderResolver::new([
363            (
364                "required".into(),
365                Variable {
366                    description: None,
367                    default: None,
368                    secret: false,
369                },
370            ),
371            (
372                "default".into(),
373                Variable {
374                    description: None,
375                    default: Some("default-value".into()),
376                    secret: false,
377                },
378            ),
379        ])
380        .unwrap();
381        resolver
382            .add_component_variables("test-component", [("test_key".into(), template.into())])
383            .unwrap();
384        resolver.add_provider(Box::new(TestProvider));
385        resolver.resolve("test-component", Key("test_key")).await
386    }
387
388    #[tokio::test]
389    async fn resolve_static() {
390        assert_eq!(test_resolve("static-value").await.unwrap(), "static-value");
391    }
392
393    #[tokio::test]
394    async fn resolve_variable_default() {
395        assert_eq!(
396            test_resolve("prefix-{{ default }}-suffix").await.unwrap(),
397            "prefix-default-value-suffix"
398        );
399    }
400
401    #[tokio::test]
402    async fn resolve_variable_provider() {
403        assert_eq!(
404            test_resolve("prefix-{{ required }}-suffix").await.unwrap(),
405            "prefix-provider-value-suffix"
406        );
407    }
408
409    #[test]
410    fn keys_good() {
411        for key in ["a", "abc", "a1b2c3", "a_1", "a_1_b_3"] {
412            Key::new(key).expect(key);
413        }
414    }
415
416    #[test]
417    fn keys_bad() {
418        for key in ["", "aX", "1bc", "_x", "x.y", "x_", "a__b", "x-y"] {
419            Key::new(key).expect_err(key);
420        }
421    }
422
423    #[test]
424    fn template_literal() {
425        assert!(Template::new("hello").unwrap().is_literal());
426        assert!(!Template::new("hello {{ world }}").unwrap().is_literal());
427    }
428}