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