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