1use std::path::Path;
2
3use anyhow::Context;
4use spin_loader::WasmLoader;
5use spin_manifest::schema::v2::ComponentDependency;
6use spin_serde::DependencyName;
7use wit_component::DecodedWasm;
8use wit_parser::Span;
9
10const GENERATED_COMMENT: &str = "// This file is automatically generated by Spin\n// It is not intended for manual editing.\n\n";
11
12pub struct ComponentInterfaces {
18 exports: Vec<String>,
19 is_http_middleware: bool,
20}
21
22impl ComponentInterfaces {
23 pub fn from_component(wasm_bytes: &[u8]) -> anyhow::Result<Self> {
25 let decoded = read_wasm(wasm_bytes)?;
26 let (resolve, world_id) = match &decoded {
27 DecodedWasm::WitPackage(resolve, pkg_id) => {
28 let world_id = resolve.select_world(&[*pkg_id], None)?;
29 (resolve, world_id)
30 }
31 DecodedWasm::Component(resolve, world_id) => (resolve, *world_id),
32 };
33
34 let world = resolve
35 .worlds
36 .get(world_id)
37 .context("world not found in decoded Wasm")?;
38
39 let exports = world
40 .exports
41 .keys()
42 .filter_map(|key| format_world_key(resolve, key))
43 .collect();
44
45 let is_http_middleware = contains_wasi_http_handler(resolve, world.imports.keys())
49 && contains_wasi_http_handler(resolve, world.exports.keys());
50
51 Ok(Self {
52 exports,
53 is_http_middleware,
54 })
55 }
56
57 pub fn is_http_middleware(&self) -> bool {
59 self.is_http_middleware
60 }
61
62 pub fn exports(&self) -> &[String] {
69 &self.exports
70 }
71}
72
73fn format_world_key(resolve: &wit_parser::Resolve, key: &wit_parser::WorldKey) -> Option<String> {
76 match key {
77 wit_parser::WorldKey::Name(name) => Some(name.clone()),
78 wit_parser::WorldKey::Interface(iface_id) => {
79 let iface = resolve.interfaces.get(*iface_id)?;
80 let pkg = resolve.packages.get(iface.package?)?;
81 let ns = &pkg.name.namespace;
82 let name = &pkg.name.name;
83 let iface_name = iface.name.as_deref().unwrap_or("unknown");
84 Some(match &pkg.name.version {
85 Some(version) => format!("{ns}:{name}/{iface_name}@{version}"),
86 None => format!("{ns}:{name}/{iface_name}"),
87 })
88 }
89 }
90}
91
92fn contains_wasi_http_handler<'a>(
93 resolve: &wit_parser::Resolve,
94 keys: impl Iterator<Item = &'a wit_parser::WorldKey>,
95) -> bool {
96 keys.filter_map(|key| match key {
97 wit_parser::WorldKey::Interface(id) => resolve.interfaces.get(*id),
98 wit_parser::WorldKey::Name(_) => None,
99 })
100 .any(|iface| {
101 let Some(pkg_id) = iface.package else {
102 return false;
103 };
104 let Some(pkg) = resolve.packages.get(pkg_id) else {
105 return false;
106 };
107 pkg.name.namespace == "wasi"
108 && pkg.name.name == "http"
109 && iface.name.as_deref() == Some("handler")
110 })
111}
112
113pub async fn extract_wits_into(
114 source: impl ExactSizeIterator<Item = (&DependencyName, &ComponentDependency)>,
115 app_root: impl AsRef<Path>,
116 dest_file: impl AsRef<Path>,
117) -> anyhow::Result<()> {
118 if source.len() == 0 {
119 if dest_file.as_ref().is_file() {
120 _ = tokio::fs::remove_file(dest_file).await; }
122 return Ok(());
123 }
124
125 let wit_text = format!(
126 "{}{}",
127 GENERATED_COMMENT,
128 extract_wits(source, app_root).await?
129 );
130
131 tokio::fs::create_dir_all(
132 dest_file
133 .as_ref()
134 .parent()
135 .context("file is root directory")?,
136 )
137 .await?;
138 tokio::fs::write(dest_file, wit_text.as_bytes()).await?;
139
140 Ok(())
141}
142
143enum ImportKind {
144 Function(spin_serde::KebabId),
145 WholePackage,
146 Interface(spin_serde::KebabId),
147}
148
149pub async fn extract_wits(
150 source: impl Iterator<Item = (&DependencyName, &ComponentDependency)>,
151 app_root: impl AsRef<Path>,
152) -> anyhow::Result<String> {
153 let loader = WasmLoader::new(app_root.as_ref().to_owned(), None, None).await?;
154
155 let mut package_wits = indexmap::IndexMap::new();
156
157 let mut aggregating_resolve = wit_parser::Resolve::default();
158 let aggregating_pkg_id =
159 aggregating_resolve.push_str("dummy.wit", "package root:component;\n\nworld root {}")?;
160 let aggregating_world_id =
161 aggregating_resolve.select_world(&[aggregating_pkg_id], Some("root"))?;
162
163 for (index, (dependency_name, dependency)) in source.enumerate() {
165 let import_kind = match dependency_name {
166 DependencyName::Plain(name) => ImportKind::Function(name.clone()),
167 DependencyName::Package(dependency_package_name) => {
168 match &dependency_package_name.interface {
169 Some(itf) => ImportKind::Interface(itf.clone()),
170 None => ImportKind::WholePackage,
171 }
172 }
173 };
174
175 let (wasm_path, export) = loader
176 .load_dependency_content(dependency_name, dependency)
177 .await
178 .with_context(|| format!("failed to load dependency {dependency_name}"))?;
179 let wasm_bytes = tokio::fs::read(&wasm_path).await?;
180
181 let decoded = read_wasm(&wasm_bytes)?;
182 let decoded = match export {
183 None => decoded,
184 Some(export) => {
185 munge_aliased_export(decoded, &export, dependency_name).with_context(|| {
186 format!("failed to map named export {export} to dependency {dependency_name}")
187 })?
188 }
189 };
190 let impo_world = format!("impo-world{index}");
191 let importised = importize(decoded, Some(&impo_world))
192 .with_context(|| format!("failed to map importize dependency {dependency_name}"))?;
193
194 let imports = match &import_kind {
195 ImportKind::WholePackage => all_imports(&importised),
196 ImportKind::Interface(itf) => one_import(&importised, itf.as_ref())?,
197 ImportKind::Function(_) => Default::default(),
198 };
199 let func_import = match &import_kind {
200 ImportKind::Function(f) => one_func_import(&importised, f.as_ref())?,
201 _ => Default::default(),
202 };
203 let type_imports = match &import_kind {
204 ImportKind::Function(_) => world_type_imports(&importised),
205 _ => Default::default(),
206 };
207
208 let root_pkg = importised.package();
213 let useful_pkgs = importised
214 .resolve()
215 .packages
216 .iter()
217 .map(|p| p.0)
218 .filter(|pid| *pid != root_pkg)
219 .collect::<Vec<_>>();
220
221 for p in &useful_pkgs {
222 let pkg_name = importised
223 .resolve()
224 .packages
225 .get(*p)
226 .context("package not found in importised (id lookup failed)")? .name
228 .clone();
229 let output = wit_component::OutputToString::default();
230 let mut printer = wit_component::WitPrinter::new(output);
231 printer.print_package(importised.resolve(), *p, false)?;
232 package_wits.insert(pkg_name, printer.output.to_string());
233 }
234
235 let remap = aggregating_resolve.merge(importised.resolve().clone())?;
238 for iid in imports {
239 let mapped_iid = remap.map_interface(iid, Span::default())?;
240 let wk = wit_parser::WorldKey::Interface(mapped_iid);
241 let world_item = wit_parser::WorldItem::Interface {
242 id: mapped_iid,
243 stability: wit_parser::Stability::Unknown,
244 span: Span::default(),
245 docs: Default::default(),
246 external_id: None,
247 };
248 let previous_world_item = aggregating_resolve
249 .worlds
250 .get_mut(aggregating_world_id)
251 .context("aggregated dependency world doesn't exist")? .imports
253 .insert(wk, world_item);
254 if let Some(previous_world_item) = previous_world_item {
255 debug_assert!({
256 let wit_parser::WorldItem::Interface { id, .. } = previous_world_item else {
257 debug_assert!(
258 false,
259 "previous WorldItem did not match inserted WorldItem: not an interface"
260 );
261 break;
262 };
263 debug_assert_eq!(
264 id, mapped_iid,
265 "replacing WorldItem had a different InterfaceId from replaced one"
266 );
267
268 true
269 });
270 }
271 }
272 if let Some(mut func) = func_import {
273 for param in &mut func.params {
275 if let wit_parser::Type::Id(id) = &mut param.ty {
276 *id = remap.map_type(*id, Span::default())?;
277 }
278 }
279 if let Some(wit_parser::Type::Id(id)) = &mut func.result {
280 *id = remap.map_type(*id, Span::default())?;
281 }
282
283 let aggregating_world = aggregating_resolve
285 .worlds
286 .get_mut(aggregating_world_id)
287 .context("aggregated dependency world doesn't exist")?;
288 for (name, type_id) in &type_imports {
289 let mapped_id = remap.map_type(*type_id, Span::default())?;
290 let wk = wit_parser::WorldKey::Name(name.clone());
291 let world_item = wit_parser::WorldItem::Type {
292 id: mapped_id,
293 span: Span::default(),
294 };
295 aggregating_world.imports.insert(wk, world_item);
296 }
297
298 let wk = wit_parser::WorldKey::Name(func.name.clone());
299 let world_item = wit_parser::WorldItem::Function(func);
300 aggregating_world.imports.insert(wk, world_item);
301 }
302 }
303
304 let world_output = wit_component::OutputToString::default();
306 let mut world_printer = wit_component::WitPrinter::new(world_output);
307 world_printer.print(&aggregating_resolve, aggregating_pkg_id, &[])?;
308
309 let mut buf = String::new();
310
311 buf.push_str(&world_printer.output.to_string());
313
314 for package_wit in package_wits.values() {
316 buf.push_str(package_wit);
317 }
318
319 Ok(buf)
320}
321
322fn munge_aliased_export(
323 decoded: DecodedWasm,
324 export: &str,
325 new_name: &DependencyName,
326) -> anyhow::Result<DecodedWasm> {
327 let export_qname = spin_serde::DependencyPackageName::try_from(export.to_string())?;
328 let Some(export_itf_name) = export_qname.interface.as_ref() else {
329 anyhow::bail!(
330 "the export name should be a qualified interface name - {export_qname} doesn't specify interface"
331 );
332 };
333 let export_pkg_name = wit_parser::PackageName {
334 namespace: export_qname.package.namespace().to_string(),
335 name: export_qname.package.name().to_string(),
336 version: export_qname.version,
337 };
338
339 let DependencyName::Package(new_name) = new_name else {
340 anyhow::bail!(
341 "the dependency name should be a qualified interface name - {new_name} not qualified"
342 );
343 };
344 let Some(new_itf_name) = new_name.interface.as_ref() else {
345 anyhow::bail!(
346 "the dependency name should be a qualified interface name - {new_name} doesn't specify an interface"
347 );
348 };
349 let new_pkg_name = wit_parser::PackageName {
350 namespace: new_name.package.namespace().to_string(),
351 name: new_name.package.name().to_string(),
352 version: new_name.version.clone(),
353 };
354
355 let (mut resolve, decode_id) = match decoded {
356 DecodedWasm::WitPackage(resolve, id) => (resolve, WorldOrPackageId::Package(id)),
357 DecodedWasm::Component(resolve, id) => (resolve, WorldOrPackageId::World(id)),
358 };
359
360 let existing_pkg = resolve
367 .packages
368 .iter()
369 .find(|(_pkg_id, pkg)| pkg.name == new_pkg_name);
370
371 let (inserting_into_pkg_id, inserting_into_pkg) = match existing_pkg {
373 Some(tuple) => tuple,
374 None => {
375 let package_wit = format!("package {new_pkg_name};");
377 let pkg_id = resolve
378 .push_str(
379 std::env::current_dir().context("no current dir")?, &package_wit,
381 )
382 .with_context(|| format!("failed to create import alias package {new_pkg_name}"))?;
383 let pkg = resolve
384 .packages
385 .get(pkg_id)
386 .context("export alias package created but doesn't exist")?; (pkg_id, pkg)
388 }
389 };
390
391 let existing_itf = inserting_into_pkg.interfaces.get(new_itf_name.as_ref());
393 if existing_itf.is_some() {
394 return Ok(decode_id.make_decoded_wasm(resolve));
402 }
403
404 let Some(export_pkg_id) = resolve.package_names.get(&export_pkg_name) else {
407 anyhow::bail!(
408 "export is from a package ({}) that doesn't exist (name lookup failed)",
409 export_pkg_name
410 );
411 };
412 let Some(export_pkg) = resolve.packages.get(*export_pkg_id) else {
413 anyhow::bail!(
414 "export is from a package ({}) that doesn't exist (id lookup failed)",
415 export_pkg_name
416 );
417 };
418 let Some(export_itf_id) = export_pkg.interfaces.get(export_itf_name.as_ref()) else {
419 anyhow::bail!(
420 "export package ({}) doesn't contain interface {} (name lookup failed)",
421 export_pkg_name,
422 export_itf_name
423 );
424 };
425 let Some(export_itf) = resolve.interfaces.get(*export_itf_id) else {
426 anyhow::bail!(
427 "export package ({}) doesn't contain interface {} (id lookup failed)",
428 export_pkg_name,
429 export_itf_name
430 );
431 };
432
433 let mut export_itf = export_itf.clone();
435 export_itf.package = Some(inserting_into_pkg_id);
436 export_itf.name = Some(new_itf_name.to_string());
437
438 let export_itf_id_new = resolve.interfaces.alloc(export_itf);
440 let inserting_into_pkg_mut = resolve
441 .packages
442 .get_mut(inserting_into_pkg_id)
443 .context("package id lookup that succeeded before failed now")?; inserting_into_pkg_mut
445 .interfaces
446 .insert(new_itf_name.to_string(), export_itf_id_new);
447
448 let decoded = decode_id.make_decoded_wasm(resolve);
449
450 Ok(decoded)
451}
452
453enum WorldOrPackageId {
454 Package(wit_parser::PackageId),
455 World(wit_parser::WorldId),
456}
457
458impl WorldOrPackageId {
459 pub fn make_decoded_wasm(&self, resolve: wit_parser::Resolve) -> DecodedWasm {
460 match self {
461 Self::Package(id) => DecodedWasm::WitPackage(resolve, *id),
462 Self::World(id) => DecodedWasm::Component(resolve, *id),
463 }
464 }
465}
466
467fn all_imports(wasm: &DecodedWasm) -> Vec<wit_parser::InterfaceId> {
468 wasm.resolve()
469 .worlds
470 .iter()
471 .flat_map(|(_wid, w)| w.imports.values())
472 .flat_map(as_interface)
473 .collect()
474}
475
476fn as_interface(wi: &wit_parser::WorldItem) -> Option<wit_parser::InterfaceId> {
477 match wi {
478 wit_parser::WorldItem::Interface { id, .. } => Some(*id),
479 _ => None,
480 }
481}
482
483fn as_func(wi: &wit_parser::WorldItem) -> Option<&wit_parser::Function> {
484 match wi {
485 wit_parser::WorldItem::Function(func) => Some(func),
486 _ => None,
487 }
488}
489
490fn one_import(wasm: &DecodedWasm, name: &str) -> anyhow::Result<Vec<wit_parser::InterfaceId>> {
491 fn find_interface(
492 resolve: &wit_parser::Resolve,
493 iid: wit_parser::InterfaceId,
494 ) -> Option<(wit_parser::InterfaceId, &wit_parser::Interface)> {
495 resolve.interfaces.get(iid).map(|itf| (iid, itf))
496 }
497
498 let id = wasm
499 .resolve()
500 .worlds
501 .iter()
502 .flat_map(|(_wid, w)| w.imports.values())
503 .flat_map(as_interface)
504 .flat_map(|iid| find_interface(wasm.resolve(), iid))
505 .find(|itf| itf.1.name.as_deref() == Some(name))
506 .map(|t| t.0)
507 .with_context(|| format!("interface {name} not found in component binary"))?;
508 Ok(vec![id])
509}
510
511fn world_type_imports(wasm: &DecodedWasm) -> Vec<(String, wit_parser::TypeId)> {
512 wasm.resolve()
513 .worlds
514 .iter()
515 .flat_map(|(_wid, w)| {
516 w.imports.iter().filter_map(|(wk, wi)| {
517 if let wit_parser::WorldItem::Type { id, .. } = wi {
518 let name = match wk {
519 wit_parser::WorldKey::Name(n) => n.clone(),
520 wit_parser::WorldKey::Interface(_) => return None,
521 };
522 Some((name, *id))
523 } else {
524 None
525 }
526 })
527 })
528 .collect()
529}
530
531fn one_func_import(wasm: &DecodedWasm, name: &str) -> anyhow::Result<Option<wit_parser::Function>> {
532 let funcs = wasm
533 .resolve()
534 .worlds
535 .iter()
536 .flat_map(|w| {
537 w.1.imports
538 .iter()
539 .flat_map(|(_wk, wi)| as_func(wi))
540 .filter(|f| f.name == name)
541 })
542 .collect::<Vec<_>>();
543
544 if funcs.len() > 1 {
547 anyhow::bail!("Dependency exports more than one function named {name}");
548 }
549 Ok(funcs.first().cloned().cloned())
550}
551
552fn read_wasm(wasm_bytes: &[u8]) -> anyhow::Result<DecodedWasm> {
553 if wasmparser::Parser::is_component(wasm_bytes) {
554 wit_component::decode(wasm_bytes)
555 } else {
556 let (wasm, bindgen) = wit_component::metadata::decode(wasm_bytes)?;
557 if wasm.is_none() {
558 anyhow::bail!(
559 "input is a core wasm module with no `component-type*` \
560 custom sections meaning that there is no WIT information; \
561 is the information not embedded or is this supposed \
562 to be a component?"
563 )
564 }
565 Ok(DecodedWasm::Component(bindgen.resolve, bindgen.world))
566 }
567}
568
569fn importize(decoded: DecodedWasm, out_world_name: Option<&String>) -> anyhow::Result<DecodedWasm> {
570 let (mut resolve, world_id) = match decoded {
571 DecodedWasm::Component(resolve, world) => (resolve, world),
572 DecodedWasm::WitPackage(resolve, id) => {
573 let world = resolve.select_world(&[id], None)?;
574 (resolve, world)
575 }
576 };
577
578 resolve
579 .importize(world_id, out_world_name.cloned())
580 .context("failed to move world exports to imports")?;
581
582 Ok(DecodedWasm::Component(resolve, world_id))
583}
584
585#[cfg(test)]
586mod test {
587 use super::*;
588
589 fn parse_wit(wit: &str) -> anyhow::Result<wit_parser::Resolve> {
590 let mut resolve = wit_parser::Resolve::new();
591 resolve.push_str("dummy.wit", wit)?;
592 Ok(resolve)
593 }
594
595 fn generate_dummy_component(wit: &str, world: &str) -> Vec<u8> {
596 generate_dummy_component_dep_has_imports(wit, world, &[])
597 }
598
599 fn generate_dummy_component_dep_has_imports(
600 wit: &str,
601 world: &str,
602 dep_imports_wits: &[&str],
603 ) -> Vec<u8> {
604 let mut resolve = wit_parser::Resolve::default();
605 for (index, dep_import_wit) in dep_imports_wits.iter().enumerate() {
606 resolve
607 .push_str(format!("test{index}"), dep_import_wit)
608 .expect("should parse dep import WIT");
609 }
610 let package_id = resolve.push_str("test", wit).expect("should parse WIT");
611 let world_id = resolve
612 .select_world(&[package_id], Some(world))
613 .expect("should select world");
614
615 let mut wasm = wit_component::dummy_module(
616 &resolve,
617 world_id,
618 wit_parser::ManglingAndAbi::Legacy(wit_parser::LiftLowerAbi::Sync),
619 );
620 wit_component::embed_component_metadata(
621 &mut wasm,
622 &resolve,
623 world_id,
624 wit_component::StringEncoding::UTF8,
625 false,
626 )
627 .expect("should embed component metadata");
628
629 wit_component::ComponentEncoder::default()
630 .validate(true)
631 .module(&wasm)
632 .expect("should set module")
633 .encode()
634 .expect("should encode component")
635 }
636
637 #[tokio::test]
638 async fn if_no_dependencies_then_empty_valid_wit() -> anyhow::Result<()> {
639 let wit = extract_wits(std::iter::empty(), ".").await?;
640
641 let resolve = parse_wit(&wit).expect("should have emitted valid WIT");
642
643 assert_eq!(1, resolve.packages.len());
644 assert_eq!(
645 "root:component",
646 resolve.packages.iter().next().unwrap().1.name.to_string()
647 );
648
649 assert_eq!(0, resolve.interfaces.len());
650
651 assert_eq!(1, resolve.worlds.len());
652
653 let world = resolve.worlds.iter().next().unwrap().1;
654 assert_eq!("root", world.name);
655 assert_eq!(0, world.imports.len());
656
657 Ok(())
658 }
659
660 #[tokio::test]
661 async fn single_dep_wit_extracted() -> anyhow::Result<()> {
662 let tempdir = tempfile::TempDir::new()?;
663 let dep_file = tempdir.path().join("regex.wasm");
664
665 let dep_wit = "package my:regex@1.0.0;\n\ninterface regex {\n matches: func(s: string) -> bool;\n}\nworld matcher {\n export regex;\n}";
666 let dep_wasm = generate_dummy_component(dep_wit, "matcher");
667 tokio::fs::write(&dep_file, &dep_wasm).await?;
668
669 let dep_name =
670 DependencyName::Package("my:regex/regex@1.0.0".to_string().try_into().unwrap());
671 let dep_src = ComponentDependency::Local {
672 path: dep_file,
673 export: None,
674 inherit_configuration: None,
675 };
676 let deps = std::iter::once((&dep_name, &dep_src));
677
678 let wit = extract_wits(deps, ".").await?;
679
680 let resolve = parse_wit(&wit).expect("should have emitted valid WIT");
681
682 assert_eq!(2, resolve.packages.len()); let (_rc_pkg_id, rc_pkg) = resolve
684 .packages
685 .iter()
686 .find(|(_, p)| p.name.to_string() == "root:component")
687 .expect("should have had `root:component`");
688 let (_mr_pkg_id, _mr_pkg) = resolve
689 .packages
690 .iter()
691 .find(|(_, p)| p.name.to_string() == "my:regex@1.0.0")
692 .expect("should have had `my:regex`");
693
694 assert_eq!(1, resolve.interfaces.len());
695 assert_eq!(
696 "regex",
697 resolve
698 .interfaces
699 .iter()
700 .next()
701 .unwrap()
702 .1
703 .name
704 .as_ref()
705 .unwrap()
706 );
707 let regex_itf_id = resolve.interfaces.iter().next().unwrap().0;
708
709 assert_eq!(2, rc_pkg.worlds.len()); let root_world_id = rc_pkg
711 .worlds
712 .iter()
713 .find(|w| w.0 == "root")
714 .expect("should have had `root` world")
715 .1;
716
717 let world = resolve.worlds.get(*root_world_id).unwrap();
718 assert_eq!(1, world.imports.len());
719 let expected_import = wit_parser::WorldItem::Interface {
720 id: regex_itf_id,
721 stability: wit_parser::Stability::Unknown,
722 span: Span::default(),
723 docs: Default::default(),
724 external_id: None,
725 };
726 let import = world.imports.values().next().unwrap();
727 assert_eq!(&expected_import, import);
728
729 Ok(())
730 }
731
732 #[tokio::test]
733 async fn world_level_func_extracted() -> anyhow::Result<()> {
734 let tempdir = tempfile::TempDir::new()?;
735 let dep_file = tempdir.path().join("crimes.wasm");
736
737 let dep_wit = "package my:crimes@1.0.0;\n\nworld crimes {\n export is-curse: func(s: string) -> bool;\n}";
738 let dep_wasm = generate_dummy_component(dep_wit, "crimes");
739 tokio::fs::write(&dep_file, &dep_wasm).await?;
740
741 let dep_name = DependencyName::Plain("is-curse".to_string().try_into().unwrap());
742 let dep_src = ComponentDependency::Local {
743 path: dep_file,
744 export: None,
745 inherit_configuration: None,
746 };
747 let deps = std::iter::once((&dep_name, &dep_src));
748
749 let wit = extract_wits(deps, ".").await?;
750
751 let resolve = parse_wit(&wit).expect("should have emitted valid WIT");
752
753 assert_eq!(1, resolve.packages.len()); let (_rc_pkg_id, rc_pkg) = resolve
755 .packages
756 .iter()
757 .find(|(_, p)| p.name.to_string() == "root:component")
758 .expect("should have had `root:component`");
759
760 let root_world_id = rc_pkg
761 .worlds
762 .get("root")
763 .expect("should have had root world");
764 let root_world = resolve
765 .worlds
766 .get(*root_world_id)
767 .expect("should have had root world at that id");
768
769 let func = root_world
770 .imports
771 .iter()
772 .filter_map(|(_, wi)| as_func(wi))
773 .find(|f| f.name == "is-curse")
774 .expect("is-curse function does not appear in root imports");
775
776 assert_eq!(1, func.params.len());
777 assert_eq!(wit_parser::Type::String, func.params.first().unwrap().ty);
778 assert_eq!(wit_parser::Type::Bool, func.result.unwrap());
779
780 Ok(())
781 }
782
783 #[tokio::test]
784 async fn world_level_func_with_record_param() -> anyhow::Result<()> {
785 let tempdir = tempfile::TempDir::new()?;
786 let dep_file = tempdir.path().join("greeter.wasm");
787
788 let dep_wit = r#"package my:greeter@1.0.0;
789
790world greeter {
791 record person {
792 name: string,
793 age: u32,
794 }
795 export greet: func(who: person) -> string;
796}"#;
797 let dep_wasm = generate_dummy_component(dep_wit, "greeter");
798 tokio::fs::write(&dep_file, &dep_wasm).await?;
799
800 let dep_name = DependencyName::Plain("greet".to_string().try_into().unwrap());
801 let dep_src = ComponentDependency::Local {
802 path: dep_file,
803 export: None,
804 inherit_configuration: None,
805 };
806 let deps = std::iter::once((&dep_name, &dep_src));
807
808 let wit = extract_wits(deps, ".").await?;
809
810 let resolve = parse_wit(&wit).expect("should have emitted valid WIT");
811
812 let (_rc_pkg_id, rc_pkg) = resolve
813 .packages
814 .iter()
815 .find(|(_, p)| p.name.to_string() == "root:component")
816 .expect("should have had `root:component`");
817
818 let root_world_id = rc_pkg
819 .worlds
820 .get("root")
821 .expect("should have had root world");
822 let root_world = resolve
823 .worlds
824 .get(*root_world_id)
825 .expect("should have had root world at that id");
826
827 let func = root_world
828 .imports
829 .iter()
830 .filter_map(|(_, wi)| as_func(wi))
831 .find(|f| f.name == "greet")
832 .expect("greet function does not appear in root imports");
833
834 assert_eq!(1, func.params.len());
835 assert!(
837 matches!(func.params.first().unwrap().ty, wit_parser::Type::Id(_)),
838 "expected record param to be Type::Id"
839 );
840 assert_eq!(wit_parser::Type::String, func.result.unwrap());
841
842 Ok(())
843 }
844
845 #[tokio::test]
846 async fn world_level_func_with_record_result() -> anyhow::Result<()> {
847 let tempdir = tempfile::TempDir::new()?;
848 let dep_file = tempdir.path().join("lookup.wasm");
849
850 let dep_wit = r#"package my:lookup@1.0.0;
851
852world lookup {
853 record info {
854 value: string,
855 found: bool,
856 }
857 export lookup: func(key: string) -> info;
858}"#;
859 let dep_wasm = generate_dummy_component(dep_wit, "lookup");
860 tokio::fs::write(&dep_file, &dep_wasm).await?;
861
862 let dep_name = DependencyName::Plain("lookup".to_string().try_into().unwrap());
863 let dep_src = ComponentDependency::Local {
864 path: dep_file,
865 export: None,
866 inherit_configuration: None,
867 };
868 let deps = std::iter::once((&dep_name, &dep_src));
869
870 let wit = extract_wits(deps, ".").await?;
871
872 let resolve = parse_wit(&wit).expect("should have emitted valid WIT");
873
874 let (_rc_pkg_id, rc_pkg) = resolve
875 .packages
876 .iter()
877 .find(|(_, p)| p.name.to_string() == "root:component")
878 .expect("should have had `root:component`");
879
880 let root_world_id = rc_pkg
881 .worlds
882 .get("root")
883 .expect("should have had root world");
884 let root_world = resolve
885 .worlds
886 .get(*root_world_id)
887 .expect("should have had root world at that id");
888
889 let func = root_world
890 .imports
891 .iter()
892 .filter_map(|(_, wi)| as_func(wi))
893 .find(|f| f.name == "lookup")
894 .expect("lookup function does not appear in root imports");
895
896 assert_eq!(1, func.params.len());
897 assert_eq!(wit_parser::Type::String, func.params.first().unwrap().ty);
898 assert!(
900 matches!(func.result, Some(wit_parser::Type::Id(_))),
901 "expected record result to be Type::Id"
902 );
903
904 Ok(())
905 }
906
907 #[tokio::test]
908 async fn world_level_func_with_enum_param() -> anyhow::Result<()> {
909 let tempdir = tempfile::TempDir::new()?;
910 let dep_file = tempdir.path().join("color.wasm");
911
912 let dep_wit = r#"package my:colors@1.0.0;
913
914world colors {
915 enum color {
916 red,
917 green,
918 blue,
919 }
920 export color-name: func(c: color) -> string;
921}"#;
922 let dep_wasm = generate_dummy_component(dep_wit, "colors");
923 tokio::fs::write(&dep_file, &dep_wasm).await?;
924
925 let dep_name = DependencyName::Plain("color-name".to_string().try_into().unwrap());
926 let dep_src = ComponentDependency::Local {
927 path: dep_file,
928 export: None,
929 inherit_configuration: None,
930 };
931 let deps = std::iter::once((&dep_name, &dep_src));
932
933 let wit = extract_wits(deps, ".").await?;
934
935 let resolve = parse_wit(&wit).expect("should have emitted valid WIT");
936
937 let (_rc_pkg_id, rc_pkg) = resolve
938 .packages
939 .iter()
940 .find(|(_, p)| p.name.to_string() == "root:component")
941 .expect("should have had `root:component`");
942
943 let root_world_id = rc_pkg
944 .worlds
945 .get("root")
946 .expect("should have had root world");
947 let root_world = resolve
948 .worlds
949 .get(*root_world_id)
950 .expect("should have had root world at that id");
951
952 let func = root_world
953 .imports
954 .iter()
955 .filter_map(|(_, wi)| as_func(wi))
956 .find(|f| f.name == "color-name")
957 .expect("color-name function does not appear in root imports");
958
959 assert_eq!(1, func.params.len());
960 assert!(
961 matches!(func.params.first().unwrap().ty, wit_parser::Type::Id(_)),
962 "expected enum param to be Type::Id"
963 );
964 assert_eq!(wit_parser::Type::String, func.result.unwrap());
965
966 Ok(())
967 }
968
969 #[tokio::test]
970 async fn we_do_not_mix_up_dep_exports_with_same_named_dep_imports() -> anyhow::Result<()> {
971 let tempdir = tempfile::TempDir::new()?;
972 let dep_file = tempdir.path().join("impexp.wasm");
973
974 let dep_wit = r#"
975 package my:impexp@1.0.0;
976
977 interface zounds {
978 fie: func() -> bool;
979 }
980
981 world tester {
982 export zounds;
983 import clever:clogs/zounds@1.2.3;
984 }"#;
985
986 let dep_imports_another_zounds = r#"
987 package clever:clogs@1.2.3;
988
989 interface zounds {
990 pshaw: func() -> u32;
991 }
992 "#;
993
994 let dep_wasm = generate_dummy_component_dep_has_imports(
995 dep_wit,
996 "tester",
997 &[dep_imports_another_zounds],
998 );
999 tokio::fs::write(&dep_file, &dep_wasm).await?;
1000
1001 let dep_name =
1002 DependencyName::Package("my:impexp/zounds@1.0.0".to_string().try_into().unwrap());
1003 let dep_src = ComponentDependency::Local {
1004 path: dep_file,
1005 export: None,
1006 inherit_configuration: None,
1007 };
1008 let deps = std::iter::once((&dep_name, &dep_src));
1009
1010 let wit = extract_wits(deps, ".").await?;
1011
1012 let resolve = parse_wit(&wit).expect("should have emitted valid WIT");
1013
1014 assert_eq!(3, resolve.packages.len()); let (_rc_pkg_id, rc_pkg) = resolve
1016 .packages
1017 .iter()
1018 .find(|(_, p)| p.name.to_string() == "root:component")
1019 .expect("should have had `root:component`");
1020 let (_mi_pkg_id, _mi_pkg) = resolve
1021 .packages
1022 .iter()
1023 .find(|(_, p)| p.name.to_string() == "my:impexp@1.0.0")
1024 .expect("should have had `my:impexp`");
1025 let (_cc_pkg_id, _cc_pkg) = resolve
1026 .packages
1027 .iter()
1028 .find(|(_, p)| p.name.to_string() == "clever:clogs@1.2.3")
1029 .expect("should have had `clever:clogs`");
1030
1031 assert_eq!(2, rc_pkg.worlds.len()); let root_world_id = rc_pkg
1033 .worlds
1034 .iter()
1035 .find(|w| w.0 == "root")
1036 .expect("should have had `root` world")
1037 .1;
1038
1039 let world = resolve.worlds.get(*root_world_id).unwrap();
1040 assert_eq!(1, world.imports.len());
1041 let (_, the_import) = world.imports.iter().next().unwrap();
1042 let the_import = as_interface(the_import).expect("the import was not an interface");
1043 let the_import_itf = resolve
1044 .interfaces
1045 .get(the_import)
1046 .expect("the import id failed at get");
1047 assert_eq!("zounds", the_import_itf.name.as_ref().unwrap());
1048 let the_import_pkg = resolve
1049 .packages
1050 .get(the_import_itf.package.unwrap())
1051 .expect("the import package failed at get");
1052 assert_eq!("my", the_import_pkg.name.namespace);
1053 assert_eq!("impexp", the_import_pkg.name.name);
1054
1055 Ok(())
1056 }
1057}