Skip to main content

spin_environments/environment/
definition.rs

1//! Environment definition types and serialisation (TOML) formats
2//!
3//! This module does *not* cover loading those definitions from remote
4//! sources, or materialising WIT packages from files or registry references -
5//! only the types.
6
7use std::collections::HashMap;
8
9use anyhow::Context;
10
11/// An environment definition, usually deserialised from a TOML document.
12/// Example:
13///
14/// ```ignore
15/// # spin-up.3.2.toml
16/// [triggers]
17/// http = { worlds = ["spin:up/http-trigger@3.2.0", "spin:up/http-trigger-rc20231018@3.2.0"], capabilities = ["local_service_chaining"] }
18/// redis = { worlds = ["spin:up/redis-trigger@3.2.0"] }
19/// ```
20#[derive(Debug, serde::Deserialize)]
21#[serde(deny_unknown_fields)]
22pub struct EnvironmentDefinition {
23    triggers: HashMap<String, TriggerEnvironment>,
24    #[serde(default)]
25    default: Option<TriggerEnvironment>,
26    #[serde(default)]
27    configuration: ConfigurationConstraints,
28    #[serde(default)]
29    metadata: Metadata,
30}
31
32/// The environment definition for a trigger, comprising the worlds which are
33/// compatible with that trigger and the host capabilities which the trigger
34/// supports.
35#[derive(Debug, serde::Deserialize)]
36#[serde(deny_unknown_fields)]
37pub struct TriggerEnvironment {
38    worlds: Vec<WorldRef>,
39    #[serde(default)]
40    capabilities: Vec<String>,
41}
42
43impl TriggerEnvironment {
44    pub fn world_refs(&self) -> &[WorldRef] {
45        &self.worlds
46    }
47
48    pub fn capabilities(&self) -> Vec<String> {
49        self.capabilities.clone()
50    }
51}
52
53impl EnvironmentDefinition {
54    pub fn triggers(&self) -> &HashMap<String, TriggerEnvironment> {
55        &self.triggers
56    }
57
58    pub fn default(&self) -> Option<&TriggerEnvironment> {
59        self.default.as_ref()
60    }
61
62    pub fn templates(&self) -> Option<&GitRepo> {
63        self.metadata.templates.as_ref()
64    }
65
66    pub fn plugins(&self) -> &[String] {
67        &self.metadata.plugins
68    }
69
70    pub fn configuration_constraints(&self) -> &ConfigurationConstraints {
71        &self.configuration
72    }
73}
74
75/// A reference to a world in an [EnvironmentDefinition]. This is formed
76/// of a fully qualified (ns:pkg/id) world name, optionally with
77/// a location from which to get the package (a registry or WIT directory).
78#[derive(Clone, Debug, serde::Deserialize)]
79#[serde(untagged, deny_unknown_fields)]
80pub enum WorldRef {
81    DefaultRegistry(WorldName),
82    Registry {
83        registry: String,
84        world: WorldName,
85    },
86    OciRegistry {
87        reference: String,
88        world: WorldName,
89    },
90    WitDirectory {
91        path: std::path::PathBuf,
92        world: WorldName,
93    },
94}
95
96/// The qualified name of a world, e.g. spin:up/http-trigger@3.2.0.
97///
98/// (Internally it is represented as a PackageName plus unqualified
99/// world name, but it stringises to the standard WIT qualified name.)
100#[derive(Clone, Debug, serde::Deserialize)]
101#[serde(try_from = "String")]
102pub struct WorldName {
103    package: wit_parser::PackageName,
104    world: String,
105}
106
107impl WorldName {
108    pub fn package(&self) -> &wit_parser::PackageName {
109        &self.package
110    }
111
112    pub fn name(&self) -> &str {
113        &self.world
114    }
115
116    pub fn package_namespaced_name(&self) -> String {
117        format!("{}:{}", self.package.namespace, self.package.name)
118    }
119
120    pub fn package_ref(&self) -> anyhow::Result<wasm_pkg_client::PackageRef> {
121        let pkg_name = self.package_namespaced_name();
122        pkg_name
123            .parse()
124            .with_context(|| format!("Environment {pkg_name} is not a valid package name"))
125    }
126
127    pub fn package_version(&self) -> Option<&semver::Version> {
128        self.package.version.as_ref()
129    }
130}
131
132impl TryFrom<String> for WorldName {
133    type Error = anyhow::Error;
134
135    fn try_from(value: String) -> Result<Self, Self::Error> {
136        use wasmparser::names::{ComponentName, ComponentNameKind};
137
138        // World qnames have the same syntactic form as interface qnames
139        let parsed = ComponentName::new(&value, 0)?;
140        let ComponentNameKind::Interface(itf) = parsed.kind() else {
141            anyhow::bail!("{value} is not a well-formed world name");
142        };
143
144        let package = wit_parser::PackageName {
145            namespace: itf.namespace().to_string(),
146            name: itf.package().to_string(),
147            version: itf.version(None)?,
148        };
149
150        let world = itf.interface().to_string();
151
152        Ok(Self { package, world })
153    }
154}
155
156impl std::fmt::Display for WorldName {
157    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158        f.write_str(&self.package.namespace)?;
159        f.write_str(":")?;
160        f.write_str(&self.package.name)?;
161        f.write_str("/")?;
162        f.write_str(&self.world)?;
163
164        if let Some(v) = self.package.version.as_ref() {
165            f.write_str("@")?;
166            f.write_str(&v.to_string())?;
167        }
168
169        Ok(())
170    }
171}
172
173/// The configuration settings allowed in the environment.
174#[derive(Clone, Debug, Default, serde::Deserialize)]
175#[serde(deny_unknown_fields)]
176pub struct ConfigurationConstraints {
177    /// Key-value stores allowed in the environment (None for no restrictions)
178    pub key_value_stores: Option<Vec<String>>,
179    /// SQLite databases allowed in the environment (None for no restrictions)
180    pub sqlite_databases: Option<Vec<String>>,
181    /// AI models allowed in the environment (None for no restrictions)
182    pub ai_models: Option<Vec<String>>,
183}
184
185#[derive(Debug, Default, serde::Deserialize)]
186pub struct Metadata {
187    #[serde(default)]
188    templates: Option<GitRepo>,
189    #[serde(default)]
190    plugins: Vec<String>,
191}
192
193#[derive(Debug, serde::Deserialize)]
194pub struct GitRepo {
195    url: String,
196    tag: Option<String>,
197}
198
199impl GitRepo {
200    pub fn url(&self) -> &str {
201        &self.url
202    }
203
204    pub fn tag(&self) -> &Option<String> {
205        &self.tag
206    }
207}
208
209#[cfg(test)]
210mod test {
211    use super::*;
212
213    #[test]
214    fn can_parse_versioned_world_name() {
215        let text = "ns:name/world@1.0.0";
216        let w = WorldName::try_from(text.to_owned()).unwrap();
217
218        assert_eq!("ns", w.package().namespace);
219        assert_eq!("name", w.package().name);
220        assert_eq!("ns:name", w.package_namespaced_name());
221        assert_eq!("ns", w.package_ref().unwrap().namespace().to_string());
222        assert_eq!("name", w.package_ref().unwrap().name().to_string());
223        assert_eq!("world", w.world);
224        assert_eq!(
225            &semver::Version::new(1, 0, 0),
226            w.package().version.as_ref().unwrap()
227        );
228
229        assert_eq!(text, w.to_string());
230    }
231
232    #[test]
233    fn can_parse_unversioned_world_name() {
234        let text = "ns:name/world";
235        let w = WorldName::try_from("ns:name/world".to_owned()).unwrap();
236
237        assert_eq!("ns", w.package().namespace);
238        assert_eq!("name", w.package().name);
239        assert_eq!("ns:name", w.package_namespaced_name());
240        assert_eq!("ns", w.package_ref().unwrap().namespace().to_string());
241        assert_eq!("name", w.package_ref().unwrap().name().to_string());
242        assert_eq!("world", w.world);
243        assert!(w.package().version.is_none());
244
245        assert_eq!(text, w.to_string());
246    }
247}