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
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; // Not an error if tidying fails
18        }
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    // TODO: figure out what to do if we import two itfs from same dep
57    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        // Capture WITs for all packages used in the importised thing.
102        // Things like WASI packages may be depended on by multiple packages
103        // so we index on the package name to avoid emitting them twice.
104
105        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)")? // shouldn't happen
120                .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        // Now add the imports to the aggregating component import world
129
130        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")? // shouldn't happen
143                .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            // Remap type IDs in the function to reference the aggregating resolve
165            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            // Add world-level type definitions that the function depends on
175            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    // Text for the root package and world(s)
196    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    // Print the root package and the world(s) with the imports
203    buf.push_str(&world_printer.output.to_string());
204
205    // Print each package
206    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    // Two scenarios:
252    // 1. The new name is in a package that is already in the Resolve
253    //    1a. The package already contains an interface with the right name
254    //    1b. The package does not already contain an interface with the right name
255    // 2. The new name is in a package that is NOT already in the Resolve
256
257    let existing_pkg = resolve
258        .packages
259        .iter()
260        .find(|(_pkg_id, pkg)| pkg.name == new_pkg_name);
261
262    // We address the first level by creating the new-name package if it doesn't exist
263    let (inserting_into_pkg_id, inserting_into_pkg) = match existing_pkg {
264        Some(tuple) => tuple,
265        None => {
266            // insert the needed package
267            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")?, // unused
271                    &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")?; // shouldn't happen
278            (pkg_id, pkg)
279        }
280    };
281
282    // Second level asks if the new-name package already contains the interface
283    let existing_itf = inserting_into_pkg.interfaces.get(new_itf_name.as_ref());
284    if existing_itf.is_some() {
285        // This makes the questionable assumption that the matchingly interface already
286        // in the package is the same as the export. E.g. given "a:b/i" = { export = "c:d/i" }
287        // where the dep contains an `a:b` package with an interface named `i`, we will generate
288        // from that rather than emitting the `c:d/i` WIT for `a:b/i`. We could be smarter about
289        // this but we don't know if other things depend on `a:b/i` so replacing it could result
290        // in a bad WIT. If this becomes a problem we could maybe try emitting it as something
291        // like `a:b/i-from-c-d` but I'd prefer to cross that bridge when we come to it.
292        return Ok(decode_id.make_decoded_wasm(resolve));
293    }
294
295    // The new-name package does not contain the interface: we need to slurp the EXPORTED itf into the `inserting_into`
296    // package under the NEW (importing) interface name
297    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    // Create the new-name interface by cloning the export interface
325    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    // Add the new-name interface to the resolve and to the new-name package
330    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")?; // we re-lookup to get around a "mutable borrow at the same time as immutable borrow" woe
335    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    // This shouldn't happen because we are using the compiled Wasm so there should
426    // be only one world in play.  But belt and braces.
427    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()); // root:component and my:regex
549        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()); // root and synthetic "impo*" wart
576        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()); // root:component - world-level funcs don't retain their package when importised
618        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        // The param should be a user-defined type (record)
700        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        // The result should be a user-defined type (record)
763        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}