spin_environments/environment/
definition.rs1use std::collections::HashMap;
8
9use anyhow::Context;
10
11#[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#[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#[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#[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 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#[derive(Clone, Debug, Default, serde::Deserialize)]
175#[serde(deny_unknown_fields)]
176pub struct ConfigurationConstraints {
177 pub key_value_stores: Option<Vec<String>>,
179 pub sqlite_databases: Option<Vec<String>>,
181 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}