Skip to main content

spin_dependency_wit/
lib.rs

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; // Not an error if tidying fails
20        }
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    // TODO: figure out what to do if we import two itfs from same dep
63    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        // Capture WITs for all packages used in the importised thing.
108        // Things like WASI packages may be depended on by multiple packages
109        // so we index on the package name to avoid emitting them twice.
110
111        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)")? // shouldn't happen
126                .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        // Now add the imports to the aggregating component import world
135
136        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")? // shouldn't happen
149                .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            // Remap type IDs in the function to reference the aggregating resolve
171            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            // Add world-level type definitions that the function depends on
181            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    // Text for the root package and world(s)
202    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    // Print the root package and the world(s) with the imports
209    buf.push_str(&world_printer.output.to_string());
210
211    // Print each package
212    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    // Two scenarios:
258    // 1. The new name is in a package that is already in the Resolve
259    //    1a. The package already contains an interface with the right name
260    //    1b. The package does not already contain an interface with the right name
261    // 2. The new name is in a package that is NOT already in the Resolve
262
263    let existing_pkg = resolve
264        .packages
265        .iter()
266        .find(|(_pkg_id, pkg)| pkg.name == new_pkg_name);
267
268    // We address the first level by creating the new-name package if it doesn't exist
269    let (inserting_into_pkg_id, inserting_into_pkg) = match existing_pkg {
270        Some(tuple) => tuple,
271        None => {
272            // insert the needed package
273            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")?, // unused
277                    &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")?; // shouldn't happen
284            (pkg_id, pkg)
285        }
286    };
287
288    // Second level asks if the new-name package already contains the interface
289    let existing_itf = inserting_into_pkg.interfaces.get(new_itf_name.as_ref());
290    if existing_itf.is_some() {
291        // This makes the questionable assumption that the matchingly interface already
292        // in the package is the same as the export. E.g. given "a:b/i" = { export = "c:d/i" }
293        // where the dep contains an `a:b` package with an interface named `i`, we will generate
294        // from that rather than emitting the `c:d/i` WIT for `a:b/i`. We could be smarter about
295        // this but we don't know if other things depend on `a:b/i` so replacing it could result
296        // in a bad WIT. If this becomes a problem we could maybe try emitting it as something
297        // like `a:b/i-from-c-d` but I'd prefer to cross that bridge when we come to it.
298        return Ok(decode_id.make_decoded_wasm(resolve));
299    }
300
301    // The new-name package does not contain the interface: we need to slurp the EXPORTED itf into the `inserting_into`
302    // package under the NEW (importing) interface name
303    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    // Create the new-name interface by cloning the export interface
331    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    // Add the new-name interface to the resolve and to the new-name package
336    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")?; // we re-lookup to get around a "mutable borrow at the same time as immutable borrow" woe
341    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    // This shouldn't happen because we are using the compiled Wasm so there should
432    // be only one world in play.  But belt and braces.
433    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()); // root:component and my:regex
555        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()); // root and synthetic "impo*" wart
582        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()); // root:component - world-level funcs don't retain their package when importised
624        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        // The param should be a user-defined type (record)
706        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        // The result should be a user-defined type (record)
769        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}