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 async fn extract_wits_into(
13 source: impl ExactSizeIterator<Item = (&DependencyName, &ComponentDependency)>,
14 app_root: impl AsRef<Path>,
15 dest_file: impl AsRef<Path>,
16) -> anyhow::Result<()> {
17 if source.len() == 0 {
18 if dest_file.as_ref().is_file() {
19 _ = tokio::fs::remove_file(dest_file).await; }
21 return Ok(());
22 }
23
24 let wit_text = format!(
25 "{}{}",
26 GENERATED_COMMENT,
27 extract_wits(source, app_root).await?
28 );
29
30 tokio::fs::create_dir_all(
31 dest_file
32 .as_ref()
33 .parent()
34 .context("file is root directory")?,
35 )
36 .await?;
37 tokio::fs::write(dest_file, wit_text.as_bytes()).await?;
38
39 Ok(())
40}
41
42enum ImportKind {
43 Function(spin_serde::KebabId),
44 WholePackage,
45 Interface(spin_serde::KebabId),
46}
47
48pub async fn extract_wits(
49 source: impl Iterator<Item = (&DependencyName, &ComponentDependency)>,
50 app_root: impl AsRef<Path>,
51) -> anyhow::Result<String> {
52 let loader = WasmLoader::new(app_root.as_ref().to_owned(), None, None).await?;
53
54 let mut package_wits = indexmap::IndexMap::new();
55
56 let mut aggregating_resolve = wit_parser::Resolve::default();
57 let aggregating_pkg_id =
58 aggregating_resolve.push_str("dummy.wit", "package root:component;\n\nworld root {}")?;
59 let aggregating_world_id =
60 aggregating_resolve.select_world(&[aggregating_pkg_id], Some("root"))?;
61
62 for (index, (dependency_name, dependency)) in source.enumerate() {
64 let import_kind = match dependency_name {
65 DependencyName::Plain(name) => ImportKind::Function(name.clone()),
66 DependencyName::Package(dependency_package_name) => {
67 match &dependency_package_name.interface {
68 Some(itf) => ImportKind::Interface(itf.clone()),
69 None => ImportKind::WholePackage,
70 }
71 }
72 };
73
74 let (wasm_path, export) = loader
75 .load_dependency_content(dependency_name, dependency)
76 .await
77 .with_context(|| format!("failed to load dependency {dependency_name}"))?;
78 let wasm_bytes = tokio::fs::read(&wasm_path).await?;
79
80 let decoded = read_wasm(&wasm_bytes)?;
81 let decoded = match export {
82 None => decoded,
83 Some(export) => {
84 munge_aliased_export(decoded, &export, dependency_name).with_context(|| {
85 format!("failed to map named export {export} to dependency {dependency_name}")
86 })?
87 }
88 };
89 let impo_world = format!("impo-world{index}");
90 let importised = importize(decoded, Some(&impo_world))
91 .with_context(|| format!("failed to map importize dependency {dependency_name}"))?;
92
93 let imports = match &import_kind {
94 ImportKind::WholePackage => all_imports(&importised),
95 ImportKind::Interface(itf) => one_import(&importised, itf.as_ref())?,
96 ImportKind::Function(_) => Default::default(),
97 };
98 let func_import = match &import_kind {
99 ImportKind::Function(f) => one_func_import(&importised, f.as_ref())?,
100 _ => Default::default(),
101 };
102 let type_imports = match &import_kind {
103 ImportKind::Function(_) => world_type_imports(&importised),
104 _ => Default::default(),
105 };
106
107 let root_pkg = importised.package();
112 let useful_pkgs = importised
113 .resolve()
114 .packages
115 .iter()
116 .map(|p| p.0)
117 .filter(|pid| *pid != root_pkg)
118 .collect::<Vec<_>>();
119
120 for p in &useful_pkgs {
121 let pkg_name = importised
122 .resolve()
123 .packages
124 .get(*p)
125 .context("package not found in importised (id lookup failed)")? .name
127 .clone();
128 let output = wit_component::OutputToString::default();
129 let mut printer = wit_component::WitPrinter::new(output);
130 printer.print_package(importised.resolve(), *p, false)?;
131 package_wits.insert(pkg_name, printer.output.to_string());
132 }
133
134 let remap = aggregating_resolve.merge(importised.resolve().clone())?;
137 for iid in imports {
138 let mapped_iid = remap.map_interface(iid, Span::default())?;
139 let wk = wit_parser::WorldKey::Interface(mapped_iid);
140 let world_item = wit_parser::WorldItem::Interface {
141 id: mapped_iid,
142 stability: wit_parser::Stability::Unknown,
143 span: Span::default(),
144 };
145 let previous_world_item = aggregating_resolve
146 .worlds
147 .get_mut(aggregating_world_id)
148 .context("aggregated dependency world doesn't exist")? .imports
150 .insert(wk, world_item);
151 if let Some(previous_world_item) = previous_world_item {
152 debug_assert!({
153 let wit_parser::WorldItem::Interface { id, .. } = previous_world_item else {
154 debug_assert!(
155 false,
156 "previous WorldItem did not match inserted WorldItem: not an interface"
157 );
158 break;
159 };
160 debug_assert_eq!(
161 id, mapped_iid,
162 "replacing WorldItem had a different InterfaceId from replaced one"
163 );
164
165 true
166 });
167 }
168 }
169 if let Some(mut func) = func_import {
170 for param in &mut func.params {
172 if let wit_parser::Type::Id(id) = &mut param.ty {
173 *id = remap.map_type(*id, Span::default())?;
174 }
175 }
176 if let Some(wit_parser::Type::Id(id)) = &mut func.result {
177 *id = remap.map_type(*id, Span::default())?;
178 }
179
180 let aggregating_world = aggregating_resolve
182 .worlds
183 .get_mut(aggregating_world_id)
184 .context("aggregated dependency world doesn't exist")?;
185 for (name, type_id) in &type_imports {
186 let mapped_id = remap.map_type(*type_id, Span::default())?;
187 let wk = wit_parser::WorldKey::Name(name.clone());
188 let world_item = wit_parser::WorldItem::Type {
189 id: mapped_id,
190 span: Span::default(),
191 };
192 aggregating_world.imports.insert(wk, world_item);
193 }
194
195 let wk = wit_parser::WorldKey::Name(func.name.clone());
196 let world_item = wit_parser::WorldItem::Function(func);
197 aggregating_world.imports.insert(wk, world_item);
198 }
199 }
200
201 let world_output = wit_component::OutputToString::default();
203 let mut world_printer = wit_component::WitPrinter::new(world_output);
204 world_printer.print(&aggregating_resolve, aggregating_pkg_id, &[])?;
205
206 let mut buf = String::new();
207
208 buf.push_str(&world_printer.output.to_string());
210
211 for package_wit in package_wits.values() {
213 buf.push_str(package_wit);
214 }
215
216 Ok(buf)
217}
218
219fn munge_aliased_export(
220 decoded: DecodedWasm,
221 export: &str,
222 new_name: &DependencyName,
223) -> anyhow::Result<DecodedWasm> {
224 let export_qname = spin_serde::DependencyPackageName::try_from(export.to_string())?;
225 let Some(export_itf_name) = export_qname.interface.as_ref() else {
226 anyhow::bail!(
227 "the export name should be a qualified interface name - {export_qname} doesn't specify interface"
228 );
229 };
230 let export_pkg_name = wit_parser::PackageName {
231 namespace: export_qname.package.namespace().to_string(),
232 name: export_qname.package.name().to_string(),
233 version: export_qname.version,
234 };
235
236 let DependencyName::Package(new_name) = new_name else {
237 anyhow::bail!(
238 "the dependency name should be a qualified interface name - {new_name} not qualified"
239 );
240 };
241 let Some(new_itf_name) = new_name.interface.as_ref() else {
242 anyhow::bail!(
243 "the dependency name should be a qualified interface name - {new_name} doesn't specify an interface"
244 );
245 };
246 let new_pkg_name = wit_parser::PackageName {
247 namespace: new_name.package.namespace().to_string(),
248 name: new_name.package.name().to_string(),
249 version: new_name.version.clone(),
250 };
251
252 let (mut resolve, decode_id) = match decoded {
253 DecodedWasm::WitPackage(resolve, id) => (resolve, WorldOrPackageId::Package(id)),
254 DecodedWasm::Component(resolve, id) => (resolve, WorldOrPackageId::World(id)),
255 };
256
257 let existing_pkg = resolve
264 .packages
265 .iter()
266 .find(|(_pkg_id, pkg)| pkg.name == new_pkg_name);
267
268 let (inserting_into_pkg_id, inserting_into_pkg) = match existing_pkg {
270 Some(tuple) => tuple,
271 None => {
272 let package_wit = format!("package {new_pkg_name};");
274 let pkg_id = resolve
275 .push_str(
276 std::env::current_dir().context("no current dir")?, &package_wit,
278 )
279 .with_context(|| format!("failed to create import alias package {new_pkg_name}"))?;
280 let pkg = resolve
281 .packages
282 .get(pkg_id)
283 .context("export alias package created but doesn't exist")?; (pkg_id, pkg)
285 }
286 };
287
288 let existing_itf = inserting_into_pkg.interfaces.get(new_itf_name.as_ref());
290 if existing_itf.is_some() {
291 return Ok(decode_id.make_decoded_wasm(resolve));
299 }
300
301 let Some(export_pkg_id) = resolve.package_names.get(&export_pkg_name) else {
304 anyhow::bail!(
305 "export is from a package ({}) that doesn't exist (name lookup failed)",
306 export_pkg_name
307 );
308 };
309 let Some(export_pkg) = resolve.packages.get(*export_pkg_id) else {
310 anyhow::bail!(
311 "export is from a package ({}) that doesn't exist (id lookup failed)",
312 export_pkg_name
313 );
314 };
315 let Some(export_itf_id) = export_pkg.interfaces.get(export_itf_name.as_ref()) else {
316 anyhow::bail!(
317 "export package ({}) doesn't contain interface {} (name lookup failed)",
318 export_pkg_name,
319 export_itf_name
320 );
321 };
322 let Some(export_itf) = resolve.interfaces.get(*export_itf_id) else {
323 anyhow::bail!(
324 "export package ({}) doesn't contain interface {} (id lookup failed)",
325 export_pkg_name,
326 export_itf_name
327 );
328 };
329
330 let mut export_itf = export_itf.clone();
332 export_itf.package = Some(inserting_into_pkg_id);
333 export_itf.name = Some(new_itf_name.to_string());
334
335 let export_itf_id_new = resolve.interfaces.alloc(export_itf);
337 let inserting_into_pkg_mut = resolve
338 .packages
339 .get_mut(inserting_into_pkg_id)
340 .context("package id lookup that succeeded before failed now")?; inserting_into_pkg_mut
342 .interfaces
343 .insert(new_itf_name.to_string(), export_itf_id_new);
344
345 let decoded = decode_id.make_decoded_wasm(resolve);
346
347 Ok(decoded)
348}
349
350enum WorldOrPackageId {
351 Package(wit_parser::PackageId),
352 World(wit_parser::WorldId),
353}
354
355impl WorldOrPackageId {
356 pub fn make_decoded_wasm(&self, resolve: wit_parser::Resolve) -> DecodedWasm {
357 match self {
358 Self::Package(id) => DecodedWasm::WitPackage(resolve, *id),
359 Self::World(id) => DecodedWasm::Component(resolve, *id),
360 }
361 }
362}
363
364fn all_imports(wasm: &DecodedWasm) -> Vec<wit_parser::InterfaceId> {
365 wasm.resolve()
366 .worlds
367 .iter()
368 .flat_map(|(_wid, w)| w.imports.values())
369 .flat_map(as_interface)
370 .collect()
371}
372
373fn as_interface(wi: &wit_parser::WorldItem) -> Option<wit_parser::InterfaceId> {
374 match wi {
375 wit_parser::WorldItem::Interface { id, .. } => Some(*id),
376 _ => None,
377 }
378}
379
380fn as_func(wi: &wit_parser::WorldItem) -> Option<&wit_parser::Function> {
381 match wi {
382 wit_parser::WorldItem::Function(func) => Some(func),
383 _ => None,
384 }
385}
386
387fn one_import(wasm: &DecodedWasm, name: &str) -> anyhow::Result<Vec<wit_parser::InterfaceId>> {
388 let id = wasm
389 .resolve()
390 .interfaces
391 .iter()
392 .find(|i| i.1.name == Some(name.to_string()))
393 .map(|t| t.0)
394 .with_context(|| format!("interface {name} not found in component binary"))?;
395 Ok(vec![id])
396}
397
398fn world_type_imports(wasm: &DecodedWasm) -> Vec<(String, wit_parser::TypeId)> {
399 wasm.resolve()
400 .worlds
401 .iter()
402 .flat_map(|(_wid, w)| {
403 w.imports.iter().filter_map(|(wk, wi)| {
404 if let wit_parser::WorldItem::Type { id, .. } = wi {
405 let name = match wk {
406 wit_parser::WorldKey::Name(n) => n.clone(),
407 wit_parser::WorldKey::Interface(_) => return None,
408 };
409 Some((name, *id))
410 } else {
411 None
412 }
413 })
414 })
415 .collect()
416}
417
418fn one_func_import(wasm: &DecodedWasm, name: &str) -> anyhow::Result<Option<wit_parser::Function>> {
419 let funcs = wasm
420 .resolve()
421 .worlds
422 .iter()
423 .flat_map(|w| {
424 w.1.imports
425 .iter()
426 .flat_map(|(_wk, wi)| as_func(wi))
427 .filter(|f| f.name == name)
428 })
429 .collect::<Vec<_>>();
430
431 if funcs.len() > 1 {
434 anyhow::bail!("Dependency exports more than one function named {name}");
435 }
436 Ok(funcs.first().cloned().cloned())
437}
438
439fn read_wasm(wasm_bytes: &[u8]) -> anyhow::Result<DecodedWasm> {
440 if wasmparser::Parser::is_component(wasm_bytes) {
441 wit_component::decode(wasm_bytes)
442 } else {
443 let (wasm, bindgen) = wit_component::metadata::decode(wasm_bytes)?;
444 if wasm.is_none() {
445 anyhow::bail!(
446 "input is a core wasm module with no `component-type*` \
447 custom sections meaning that there is no WIT information; \
448 is the information not embedded or is this supposed \
449 to be a component?"
450 )
451 }
452 Ok(DecodedWasm::Component(bindgen.resolve, bindgen.world))
453 }
454}
455
456fn importize(decoded: DecodedWasm, out_world_name: Option<&String>) -> anyhow::Result<DecodedWasm> {
457 let (mut resolve, world_id) = match decoded {
458 DecodedWasm::Component(resolve, world) => (resolve, world),
459 DecodedWasm::WitPackage(resolve, id) => {
460 let world = resolve.select_world(&[id], None)?;
461 (resolve, world)
462 }
463 };
464
465 resolve
466 .importize(world_id, out_world_name.cloned())
467 .context("failed to move world exports to imports")?;
468
469 Ok(DecodedWasm::Component(resolve, world_id))
470}
471
472#[cfg(test)]
473mod test {
474 use super::*;
475
476 fn parse_wit(wit: &str) -> anyhow::Result<wit_parser::Resolve> {
477 let mut resolve = wit_parser::Resolve::new();
478 resolve.push_str("dummy.wit", wit)?;
479 Ok(resolve)
480 }
481
482 fn generate_dummy_component(wit: &str, world: &str) -> Vec<u8> {
483 let mut resolve = wit_parser::Resolve::default();
484 let package_id = resolve.push_str("test", wit).expect("should parse WIT");
485 let world_id = resolve
486 .select_world(&[package_id], Some(world))
487 .expect("should select world");
488
489 let mut wasm = wit_component::dummy_module(
490 &resolve,
491 world_id,
492 wit_parser::ManglingAndAbi::Legacy(wit_parser::LiftLowerAbi::Sync),
493 );
494 wit_component::embed_component_metadata(
495 &mut wasm,
496 &resolve,
497 world_id,
498 wit_component::StringEncoding::UTF8,
499 )
500 .expect("should embed component metadata");
501
502 let mut encoder = wit_component::ComponentEncoder::default()
503 .validate(true)
504 .module(&wasm)
505 .expect("should set module");
506 encoder.encode().expect("should encode component")
507 }
508
509 #[tokio::test]
510 async fn if_no_dependencies_then_empty_valid_wit() -> anyhow::Result<()> {
511 let wit = extract_wits(std::iter::empty(), ".").await?;
512
513 let resolve = parse_wit(&wit).expect("should have emitted valid WIT");
514
515 assert_eq!(1, resolve.packages.len());
516 assert_eq!(
517 "root:component",
518 resolve.packages.iter().next().unwrap().1.name.to_string()
519 );
520
521 assert_eq!(0, resolve.interfaces.len());
522
523 assert_eq!(1, resolve.worlds.len());
524
525 let world = resolve.worlds.iter().next().unwrap().1;
526 assert_eq!("root", world.name);
527 assert_eq!(0, world.imports.len());
528
529 Ok(())
530 }
531
532 #[tokio::test]
533 async fn single_dep_wit_extracted() -> anyhow::Result<()> {
534 let tempdir = tempfile::TempDir::new()?;
535 let dep_file = tempdir.path().join("regex.wasm");
536
537 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}";
538 let dep_wasm = generate_dummy_component(dep_wit, "matcher");
539 tokio::fs::write(&dep_file, &dep_wasm).await?;
540
541 let dep_name =
542 DependencyName::Package("my:regex/regex@1.0.0".to_string().try_into().unwrap());
543 let dep_src = ComponentDependency::Local {
544 path: dep_file,
545 export: None,
546 inherit_configuration: None,
547 };
548 let deps = std::iter::once((&dep_name, &dep_src));
549
550 let wit = extract_wits(deps, ".").await?;
551
552 let resolve = parse_wit(&wit).expect("should have emitted valid WIT");
553
554 assert_eq!(2, resolve.packages.len()); let (_rc_pkg_id, rc_pkg) = resolve
556 .packages
557 .iter()
558 .find(|(_, p)| p.name.to_string() == "root:component")
559 .expect("should have had `root:component`");
560 let (_mr_pkg_id, _mr_pkg) = resolve
561 .packages
562 .iter()
563 .find(|(_, p)| p.name.to_string() == "my:regex@1.0.0")
564 .expect("should have had `my:regex`");
565
566 assert_eq!(1, resolve.interfaces.len());
567 assert_eq!(
568 "regex",
569 resolve
570 .interfaces
571 .iter()
572 .next()
573 .unwrap()
574 .1
575 .name
576 .as_ref()
577 .unwrap()
578 );
579 let regex_itf_id = resolve.interfaces.iter().next().unwrap().0;
580
581 assert_eq!(2, rc_pkg.worlds.len()); let root_world_id = rc_pkg
583 .worlds
584 .iter()
585 .find(|w| w.0 == "root")
586 .expect("should have had `root` world")
587 .1;
588
589 let world = resolve.worlds.get(*root_world_id).unwrap();
590 assert_eq!(1, world.imports.len());
591 let expected_import = wit_parser::WorldItem::Interface {
592 id: regex_itf_id,
593 stability: wit_parser::Stability::Unknown,
594 span: Span::default(),
595 };
596 let import = world.imports.values().next().unwrap();
597 assert_eq!(&expected_import, import);
598
599 Ok(())
600 }
601
602 #[tokio::test]
603 async fn world_level_func_extracted() -> anyhow::Result<()> {
604 let tempdir = tempfile::TempDir::new()?;
605 let dep_file = tempdir.path().join("crimes.wasm");
606
607 let dep_wit = "package my:crimes@1.0.0;\n\nworld crimes {\n export is-curse: func(s: string) -> bool;\n}";
608 let dep_wasm = generate_dummy_component(dep_wit, "crimes");
609 tokio::fs::write(&dep_file, &dep_wasm).await?;
610
611 let dep_name = DependencyName::Plain("is-curse".to_string().try_into().unwrap());
612 let dep_src = ComponentDependency::Local {
613 path: dep_file,
614 export: None,
615 inherit_configuration: None,
616 };
617 let deps = std::iter::once((&dep_name, &dep_src));
618
619 let wit = extract_wits(deps, ".").await?;
620
621 let resolve = parse_wit(&wit).expect("should have emitted valid WIT");
622
623 assert_eq!(1, resolve.packages.len()); let (_rc_pkg_id, rc_pkg) = resolve
625 .packages
626 .iter()
627 .find(|(_, p)| p.name.to_string() == "root:component")
628 .expect("should have had `root:component`");
629
630 let root_world_id = rc_pkg
631 .worlds
632 .get("root")
633 .expect("should have had root world");
634 let root_world = resolve
635 .worlds
636 .get(*root_world_id)
637 .expect("should have had root world at that id");
638
639 let func = root_world
640 .imports
641 .iter()
642 .filter_map(|(_, wi)| as_func(wi))
643 .find(|f| f.name == "is-curse")
644 .expect("is-curse function does not appear in root imports");
645
646 assert_eq!(1, func.params.len());
647 assert_eq!(wit_parser::Type::String, func.params.first().unwrap().ty);
648 assert_eq!(wit_parser::Type::Bool, func.result.unwrap());
649
650 Ok(())
651 }
652
653 #[tokio::test]
654 async fn world_level_func_with_record_param() -> anyhow::Result<()> {
655 let tempdir = tempfile::TempDir::new()?;
656 let dep_file = tempdir.path().join("greeter.wasm");
657
658 let dep_wit = r#"package my:greeter@1.0.0;
659
660world greeter {
661 record person {
662 name: string,
663 age: u32,
664 }
665 export greet: func(who: person) -> string;
666}"#;
667 let dep_wasm = generate_dummy_component(dep_wit, "greeter");
668 tokio::fs::write(&dep_file, &dep_wasm).await?;
669
670 let dep_name = DependencyName::Plain("greet".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 let (_rc_pkg_id, rc_pkg) = resolve
683 .packages
684 .iter()
685 .find(|(_, p)| p.name.to_string() == "root:component")
686 .expect("should have had `root:component`");
687
688 let root_world_id = rc_pkg
689 .worlds
690 .get("root")
691 .expect("should have had root world");
692 let root_world = resolve
693 .worlds
694 .get(*root_world_id)
695 .expect("should have had root world at that id");
696
697 let func = root_world
698 .imports
699 .iter()
700 .filter_map(|(_, wi)| as_func(wi))
701 .find(|f| f.name == "greet")
702 .expect("greet function does not appear in root imports");
703
704 assert_eq!(1, func.params.len());
705 assert!(
707 matches!(func.params.first().unwrap().ty, wit_parser::Type::Id(_)),
708 "expected record param to be Type::Id"
709 );
710 assert_eq!(wit_parser::Type::String, func.result.unwrap());
711
712 Ok(())
713 }
714
715 #[tokio::test]
716 async fn world_level_func_with_record_result() -> anyhow::Result<()> {
717 let tempdir = tempfile::TempDir::new()?;
718 let dep_file = tempdir.path().join("lookup.wasm");
719
720 let dep_wit = r#"package my:lookup@1.0.0;
721
722world lookup {
723 record info {
724 value: string,
725 found: bool,
726 }
727 export lookup: func(key: string) -> info;
728}"#;
729 let dep_wasm = generate_dummy_component(dep_wit, "lookup");
730 tokio::fs::write(&dep_file, &dep_wasm).await?;
731
732 let dep_name = DependencyName::Plain("lookup".to_string().try_into().unwrap());
733 let dep_src = ComponentDependency::Local {
734 path: dep_file,
735 export: None,
736 inherit_configuration: None,
737 };
738 let deps = std::iter::once((&dep_name, &dep_src));
739
740 let wit = extract_wits(deps, ".").await?;
741
742 let resolve = parse_wit(&wit).expect("should have emitted valid WIT");
743
744 let (_rc_pkg_id, rc_pkg) = resolve
745 .packages
746 .iter()
747 .find(|(_, p)| p.name.to_string() == "root:component")
748 .expect("should have had `root:component`");
749
750 let root_world_id = rc_pkg
751 .worlds
752 .get("root")
753 .expect("should have had root world");
754 let root_world = resolve
755 .worlds
756 .get(*root_world_id)
757 .expect("should have had root world at that id");
758
759 let func = root_world
760 .imports
761 .iter()
762 .filter_map(|(_, wi)| as_func(wi))
763 .find(|f| f.name == "lookup")
764 .expect("lookup function does not appear in root imports");
765
766 assert_eq!(1, func.params.len());
767 assert_eq!(wit_parser::Type::String, func.params.first().unwrap().ty);
768 assert!(
770 matches!(func.result, Some(wit_parser::Type::Id(_))),
771 "expected record result to be Type::Id"
772 );
773
774 Ok(())
775 }
776
777 #[tokio::test]
778 async fn world_level_func_with_enum_param() -> anyhow::Result<()> {
779 let tempdir = tempfile::TempDir::new()?;
780 let dep_file = tempdir.path().join("color.wasm");
781
782 let dep_wit = r#"package my:colors@1.0.0;
783
784world colors {
785 enum color {
786 red,
787 green,
788 blue,
789 }
790 export color-name: func(c: color) -> string;
791}"#;
792 let dep_wasm = generate_dummy_component(dep_wit, "colors");
793 tokio::fs::write(&dep_file, &dep_wasm).await?;
794
795 let dep_name = DependencyName::Plain("color-name".to_string().try_into().unwrap());
796 let dep_src = ComponentDependency::Local {
797 path: dep_file,
798 export: None,
799 inherit_configuration: None,
800 };
801 let deps = std::iter::once((&dep_name, &dep_src));
802
803 let wit = extract_wits(deps, ".").await?;
804
805 let resolve = parse_wit(&wit).expect("should have emitted valid WIT");
806
807 let (_rc_pkg_id, rc_pkg) = resolve
808 .packages
809 .iter()
810 .find(|(_, p)| p.name.to_string() == "root:component")
811 .expect("should have had `root:component`");
812
813 let root_world_id = rc_pkg
814 .worlds
815 .get("root")
816 .expect("should have had root world");
817 let root_world = resolve
818 .worlds
819 .get(*root_world_id)
820 .expect("should have had root world at that id");
821
822 let func = root_world
823 .imports
824 .iter()
825 .filter_map(|(_, wi)| as_func(wi))
826 .find(|f| f.name == "color-name")
827 .expect("color-name function does not appear in root imports");
828
829 assert_eq!(1, func.params.len());
830 assert!(
831 matches!(func.params.first().unwrap().ty, wit_parser::Type::Id(_)),
832 "expected enum param to be Type::Id"
833 );
834 assert_eq!(wit_parser::Type::String, func.result.unwrap());
835
836 Ok(())
837 }
838}