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