1use anyhow::Context;
2use indexmap::IndexMap;
3use itertools::Itertools;
4use semver::Version;
5use spin_app::locked::InheritConfiguration as LockedInheritConfiguration;
6use spin_common::{ui::quoted_path, url::parse_file_url};
7use spin_serde::{DependencyName, KebabId};
8use std::collections::BTreeMap;
9use thiserror::Error;
10use wac_graph::types::{Package, SubtypeChecker, WorldId};
11use wac_graph::{CompositionGraph, NodeId};
12
13pub mod middleware;
14
15pub use spin_capabilities::InheritConfiguration;
16
17pub async fn compose<
36 L: ComponentSourceLoader,
37 Fut: std::future::Future<Output = Result<Vec<u8>, ComposeError>>,
38>(
39 loader: &L,
40 component: &L::Component,
41 apply_trigger_deps: impl Fn(Vec<u8>) -> Fut,
42) -> Result<Vec<u8>, ComposeError> {
43 Composer::new(loader)
44 .compose(component, apply_trigger_deps)
45 .await
46}
47
48#[async_trait::async_trait]
51pub trait DependencyLike {
52 fn inherit(&self) -> InheritConfiguration;
53 fn export(&self) -> &Option<String>;
54}
55
56#[async_trait::async_trait]
59pub trait ComponentLike {
60 type Dependency: DependencyLike;
61
62 fn dependencies(
63 &self,
64 ) -> impl std::iter::ExactSizeIterator<Item = (&DependencyName, &Self::Dependency)>;
65 fn id(&self) -> &str;
66}
67
68#[async_trait::async_trait]
69impl ComponentLike for spin_app::locked::LockedComponent {
70 type Dependency = spin_app::locked::LockedComponentDependency;
71
72 fn dependencies(
73 &self,
74 ) -> impl std::iter::ExactSizeIterator<Item = (&DependencyName, &Self::Dependency)> {
75 self.dependencies.iter()
76 }
77
78 fn id(&self) -> &str {
79 &self.id
80 }
81}
82
83#[async_trait::async_trait]
84impl DependencyLike for spin_app::locked::LockedComponentDependency {
85 fn inherit(&self) -> InheritConfiguration {
86 match &self.inherit {
87 LockedInheritConfiguration::All => InheritConfiguration::All,
88 LockedInheritConfiguration::Some(cfgs) => InheritConfiguration::Some(cfgs.clone()),
89 }
90 }
91
92 fn export(&self) -> &Option<String> {
93 &self.export
94 }
95}
96
97#[async_trait::async_trait]
99pub trait ComponentSourceLoader {
100 type Component: ComponentLike<Dependency = Self::Dependency>;
101 type Dependency: DependencyLike;
102 type Source;
103 async fn load_component_source(&self, source: &Self::Component) -> anyhow::Result<Vec<u8>>;
104 async fn load_dependency_source(&self, source: &Self::Dependency) -> anyhow::Result<Vec<u8>>;
105 async fn load_source(&self, source: &Self::Source) -> anyhow::Result<Vec<u8>>;
106}
107
108pub struct ComponentSourceLoaderFs;
110
111#[async_trait::async_trait]
112impl ComponentSourceLoader for ComponentSourceLoaderFs {
113 type Component = spin_app::locked::LockedComponent;
114 type Dependency = spin_app::locked::LockedComponentDependency;
115 type Source = spin_app::locked::LockedComponentSource;
116
117 async fn load_component_source(&self, source: &Self::Component) -> anyhow::Result<Vec<u8>> {
118 Self::load_from_locked_source(&source.source).await
119 }
120
121 async fn load_dependency_source(&self, source: &Self::Dependency) -> anyhow::Result<Vec<u8>> {
122 Self::load_from_locked_source(&source.source).await
123 }
124
125 async fn load_source(&self, source: &Self::Source) -> anyhow::Result<Vec<u8>> {
126 Self::load_from_locked_source(source).await
127 }
128}
129
130impl ComponentSourceLoaderFs {
131 async fn load_from_locked_source(
132 source: &spin_app::locked::LockedComponentSource,
133 ) -> anyhow::Result<Vec<u8>> {
134 let source = source
135 .content
136 .source
137 .as_ref()
138 .context("LockedComponentSource missing source field")?;
139
140 let path = parse_file_url(source)?;
141
142 let bytes: Vec<u8> = tokio::fs::read(&path).await.with_context(|| {
143 format!(
144 "failed to read component source from disk at path {}",
145 quoted_path(&path)
146 )
147 })?;
148
149 let component = spin_componentize::componentize_if_necessary(&bytes)
150 .with_context(|| format!("failed to componentize {}", quoted_path(&path)))?;
151
152 Ok(component.into())
153 }
154}
155
156#[derive(Debug, Error)]
158pub enum ComposeError {
159 #[error(
161 "dependency '{dependency_name}' doesn't match any imports of component '{component_id}'"
162 )]
163 UnmatchedDependencyName {
164 component_id: String,
165 dependency_name: Box<DependencyName>,
166 },
167 #[error("component '{component_id}' has dependency conflicts: {}", format_conflicts(.conflicts))]
169 DependencyConflicts {
170 component_id: String,
171 conflicts: Vec<(String, Vec<DependencyName>)>,
172 },
173 #[error(
175 "dependency '{dependency_name}' doesn't export '{export_name}' to satisfy import '{import_name}'"
176 )]
177 MissingExport {
178 dependency_name: Box<DependencyName>,
179 export_name: String,
180 import_name: String,
181 },
182 #[error("an error occurred when preparing dependencies")]
184 PrepareError(#[source] anyhow::Error),
185 #[error("failed to encode composition graph: {0}")]
187 EncodeError(#[source] anyhow::Error),
188}
189
190fn format_conflicts(conflicts: &[(String, Vec<DependencyName>)]) -> String {
191 conflicts
192 .iter()
193 .map(|(import_name, dependency_names)| {
194 format!(
195 "import '{}' satisfied by dependencies: '{}'",
196 import_name,
197 dependency_names.iter().join(", ")
198 )
199 })
200 .join("; ")
201}
202
203struct Composer<'a, L> {
204 graph: CompositionGraph,
205 loader: &'a L,
206}
207
208impl<'a, L: ComponentSourceLoader> Composer<'a, L> {
209 async fn compose<Fut: std::future::Future<Output = Result<Vec<u8>, ComposeError>>>(
210 mut self,
211 component: &L::Component,
212 apply_trigger_deps: impl Fn(Vec<u8>) -> Fut,
213 ) -> Result<Vec<u8>, ComposeError> {
214 let source = self
215 .loader
216 .load_component_source(component)
217 .await
218 .map_err(ComposeError::PrepareError)?;
219
220 let fulfilled_source = if component.dependencies().len() == 0 {
221 source
222 } else {
223 let (world_id, instantiation_id) = self
224 .register_package(component.id(), None, source)
225 .map_err(ComposeError::PrepareError)?;
226
227 let prepared = self.prepare_dependencies(world_id, component).await?;
228
229 let arguments = self
230 .build_instantiation_arguments(world_id, prepared)
231 .await?;
232
233 for (argument_name, argument) in arguments {
234 self.graph
235 .set_instantiation_argument(instantiation_id, &argument_name, argument)
236 .map_err(|e| ComposeError::PrepareError(e.into()))?;
237 }
238
239 self.export_dependents_exports(world_id, instantiation_id)
240 .map_err(ComposeError::PrepareError)?;
241
242 self.graph
243 .encode(Default::default())
244 .map_err(|e| ComposeError::EncodeError(e.into()))?
245 };
246
247 let with_extras = apply_trigger_deps(fulfilled_source).await?;
248
249 Ok(with_extras)
250 }
251
252 fn new(loader: &'a L) -> Self {
253 Self {
254 graph: CompositionGraph::new(),
255 loader,
256 }
257 }
258
259 async fn prepare_dependencies(
266 &mut self,
267 world_id: WorldId,
268 component: &L::Component,
269 ) -> Result<IndexMap<String, DependencyInfo>, ComposeError> {
270 let imports = self.graph.types()[world_id].imports.clone();
271
272 let import_keys = imports.keys().cloned().collect::<Vec<_>>();
273
274 let mut mappings: BTreeMap<String, Vec<DependencyInfo>> = BTreeMap::new();
275
276 for (dependency_name, dependency) in component.dependencies() {
277 let mut matched = Vec::new();
278
279 for import_name in &import_keys {
280 if matches_import(dependency_name, import_name)
281 .map_err(ComposeError::PrepareError)?
282 {
283 matched.push(import_name.clone());
284 }
285 }
286
287 if matched.is_empty() {
288 return Err(ComposeError::UnmatchedDependencyName {
289 component_id: component.id().to_owned(),
290 dependency_name: Box::new(dependency_name.clone()),
291 });
292 }
293
294 let info = self
295 .register_dependency(dependency_name.clone(), dependency)
296 .await
297 .map_err(ComposeError::PrepareError)?;
298
299 for import_name in matched {
301 mappings
302 .entry(import_name.to_string())
303 .or_default()
304 .push(info.clone());
305 }
306 }
307
308 let (conflicts, prepared): (Vec<_>, Vec<_>) =
309 mappings.into_iter().partition(|(_, infos)| infos.len() > 1);
310
311 if !conflicts.is_empty() {
312 return Err(ComposeError::DependencyConflicts {
313 component_id: component.id().to_owned(),
314 conflicts: conflicts
315 .into_iter()
316 .map(|(import_name, infos)| {
317 (
318 import_name,
319 infos.into_iter().map(|info| info.manifest_name).collect(),
320 )
321 })
322 .collect(),
323 });
324 }
325
326 Ok(prepared
327 .into_iter()
328 .map(|(import_name, mut infos)| {
329 assert_eq!(infos.len(), 1);
330 (import_name, infos.remove(0))
331 })
332 .collect())
333 }
334
335 async fn build_instantiation_arguments(
340 &mut self,
341 world_id: WorldId,
342 dependencies: IndexMap<String, DependencyInfo>,
343 ) -> Result<IndexMap<String, NodeId>, ComposeError> {
344 let mut cache = Default::default();
345 let mut checker = SubtypeChecker::new(&mut cache);
346
347 let mut arguments = IndexMap::new();
348
349 for (import_name, dependency_info) in dependencies {
350 let (export_name, export_ty) = match dependency_info.export_name {
351 Some(export_name) => {
352 let Some(export_ty) = self.graph.types()[dependency_info.world_id]
353 .exports
354 .get(&export_name)
355 else {
356 return Err(ComposeError::MissingExport {
357 dependency_name: Box::new(dependency_info.manifest_name),
358 export_name,
359 import_name: import_name.clone(),
360 });
361 };
362
363 (export_name, export_ty)
364 }
365 None => {
366 let Some(export_ty) = self.graph.types()[dependency_info.world_id]
367 .exports
368 .get(&import_name)
369 else {
370 return Err(ComposeError::MissingExport {
371 dependency_name: Box::new(dependency_info.manifest_name),
372 export_name: import_name.clone(),
373 import_name: import_name.clone(),
374 });
375 };
376
377 (import_name.clone(), export_ty)
378 }
379 };
380
381 let import_ty = self.graph.types()[world_id]
382 .imports
383 .get(&import_name)
384 .unwrap();
385
386 checker.is_subtype(
388 *export_ty,
389 self.graph.types(),
390 *import_ty,
391 self.graph.types(),
392 ).with_context(|| {
393 format!(
394 "dependency '{dependency_name}' exports '{export_name}' which is not compatible with import '{import_name}'",
395 dependency_name = dependency_info.manifest_name,
396 )
397 })
398 .map_err(ComposeError::PrepareError)?;
399
400 let export_id = self
401 .graph
402 .alias_instance_export(dependency_info.instantiation_id, &export_name)
403 .map_err(|e| ComposeError::PrepareError(e.into()))?;
404
405 assert!(arguments.insert(import_name, export_id).is_none());
406 }
407
408 Ok(arguments)
409 }
410
411 async fn register_dependency(
416 &mut self,
417 dependency_name: DependencyName,
418 dependency: &L::Dependency,
419 ) -> anyhow::Result<DependencyInfo> {
420 let mut dependency_source = self.loader.load_dependency_source(dependency).await?;
421
422 let package_name = match &dependency_name {
423 DependencyName::Package(name) => name.package.to_string(),
424 DependencyName::Plain(name) => name.to_string(),
425 };
426
427 dependency_source =
428 spin_capabilities::apply_deny_adapter(&dependency_source, dependency.inherit())?;
429
430 let (world_id, instantiation_id) =
431 self.register_package(&package_name, None, dependency_source)?;
432
433 Ok(DependencyInfo {
434 manifest_name: dependency_name,
435 instantiation_id,
436 world_id,
437 export_name: dependency.export().clone(),
438 })
439 }
440
441 fn register_package(
442 &mut self,
443 name: &str,
444 version: Option<&Version>,
445 source: impl Into<Vec<u8>>,
446 ) -> anyhow::Result<(WorldId, NodeId)> {
447 let package = Package::from_bytes(name, version, source, self.graph.types_mut())?;
448 let world_id = package.ty();
449 let package_id = self.graph.register_package(package)?;
450 let instantiation_id = self.graph.instantiate(package_id);
451
452 Ok((world_id, instantiation_id))
453 }
454
455 fn export_dependents_exports(
456 &mut self,
457 world_id: WorldId,
458 instantiation_id: NodeId,
459 ) -> anyhow::Result<()> {
460 for export_name in self.graph.types()[world_id]
462 .exports
463 .keys()
464 .cloned()
465 .collect::<Vec<_>>()
466 {
467 let export_id = self
468 .graph
469 .alias_instance_export(instantiation_id, &export_name)?;
470
471 self.graph.export(export_id, &export_name)?;
472 }
473
474 Ok(())
475 }
476}
477
478#[derive(Clone)]
479struct DependencyInfo {
480 manifest_name: DependencyName,
484 instantiation_id: NodeId,
486 world_id: WorldId,
488 export_name: Option<String>,
490}
491enum ImportName {
492 Plain(KebabId),
493 Package {
494 package: String,
495 interface: String,
496 version: Option<Version>,
497 },
498}
499
500impl std::str::FromStr for ImportName {
501 type Err = anyhow::Error;
502
503 fn from_str(s: &str) -> Result<Self, Self::Err> {
504 if s.contains([':', '/']) {
505 let (package, rest) = s
506 .split_once('/')
507 .with_context(|| format!("invalid import name: {s}"))?;
508
509 let (interface, version) = match rest.split_once('@') {
510 Some((interface, version)) => {
511 let version = Version::parse(version)
512 .with_context(|| format!("invalid version in import name: {s}"))?;
513
514 (interface, Some(version))
515 }
516 None => (rest, None),
517 };
518
519 Ok(Self::Package {
520 package: package.to_string(),
521 interface: interface.to_string(),
522 version,
523 })
524 } else {
525 Ok(Self::Plain(
526 s.to_string()
527 .try_into()
528 .map_err(|e| anyhow::anyhow!("{e}"))?,
529 ))
530 }
531 }
532}
533
534fn matches_import(dependency_name: &DependencyName, import_name: &str) -> anyhow::Result<bool> {
536 let import_name = import_name.parse::<ImportName>()?;
537
538 match (dependency_name, import_name) {
539 (DependencyName::Plain(dependency_name), ImportName::Plain(import_name)) => {
540 Ok(dependency_name == &import_name)
542 }
543 (
544 DependencyName::Package(dependency_name),
545 ImportName::Package {
546 package: import_package,
547 interface: import_interface,
548 version: import_version,
549 },
550 ) => {
551 if import_package != dependency_name.package.to_string() {
552 return Ok(false);
553 }
554
555 if let Some(interface) = dependency_name.interface.as_ref()
556 && import_interface != interface.as_ref()
557 {
558 return Ok(false);
559 }
560
561 if let Some(version) = dependency_name.version.as_ref()
562 && import_version != Some(version.clone())
563 {
564 return Ok(false);
565 }
566
567 Ok(true)
568 }
569 (_, _) => {
570 Ok(false)
572 }
573 }
574}
575
576#[cfg(test)]
577mod test {
578 use super::*;
579
580 #[test]
581 fn test_matches_import() {
582 for (dep_name, import_names) in [
583 ("foo:bar/baz@0.1.0", vec!["foo:bar/baz@0.1.0"]),
584 ("foo:bar/baz", vec!["foo:bar/baz@0.1.0", "foo:bar/baz"]),
585 ("foo:bar", vec!["foo:bar/baz@0.1.0", "foo:bar/baz"]),
586 ("foo:bar@0.1.0", vec!["foo:bar/baz@0.1.0"]),
587 ("foo-bar", vec!["foo-bar"]),
588 ] {
589 let dep_name: DependencyName = dep_name.parse().unwrap();
590 for import_name in import_names {
591 assert!(matches_import(&dep_name, import_name).unwrap());
592 }
593 }
594
595 for (dep_name, import_names) in [
596 ("foo:bar/baz@0.1.0", vec!["foo:bar/baz"]),
597 ("foo:bar/baz", vec!["foo:bar/bub", "foo:bar/bub@0.1.0"]),
598 ("foo:bar", vec!["foo:bub/bib"]),
599 ("foo:bar@0.1.0", vec!["foo:bar/baz"]),
600 ("foo:bar/baz", vec!["foo:bar/baz-bub", "foo-bar"]),
601 ] {
602 let dep_name: DependencyName = dep_name.parse().unwrap();
603 for import_name in import_names {
604 assert!(!matches_import(&dep_name, import_name).unwrap());
605 }
606 }
607 }
608}