Skip to main content

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