diff --git a/crates/wac-types/src/lib.rs b/crates/wac-types/src/lib.rs index baf3b21..35808dc 100644 --- a/crates/wac-types/src/lib.rs +++ b/crates/wac-types/src/lib.rs @@ -7,9 +7,11 @@ mod checker; mod component; mod core; mod package; +mod targets; pub use aggregator::*; pub use checker::*; pub use component::*; pub use core::*; pub use package::*; +pub use targets::*; diff --git a/crates/wac-types/src/targets.rs b/crates/wac-types/src/targets.rs new file mode 100644 index 0000000..8e34378 --- /dev/null +++ b/crates/wac-types/src/targets.rs @@ -0,0 +1,158 @@ +use crate::{ExternKind, ItemKind, SubtypeChecker, Types, WorldId}; +use std::collections::{BTreeMap, BTreeSet}; +use std::fmt; + +/// The result of validating a component against a target world. +pub type TargetValidationResult = Result<(), TargetValidationReport>; + +/// The report of validating a component against a target world. +#[derive(Debug, Default)] +pub struct TargetValidationReport { + /// Imports present in the component but not in the target world. + imports_not_in_target: BTreeSet, + /// Exports not in the component but required by the target world. + /// + /// This is a mapping from the name of the missing export to the expected item kind. + missing_exports: BTreeMap, + /// Mismatched types between the component and the target world. + /// + /// This is a mapping from name of the mismatched item to a tuple of + /// the extern kind of the item and type error. + mismatched_types: BTreeMap, +} + +impl From for TargetValidationResult { + fn from(report: TargetValidationReport) -> TargetValidationResult { + if report.imports_not_in_target.is_empty() + && report.missing_exports.is_empty() + && report.mismatched_types.is_empty() + { + Ok(()) + } else { + Err(report) + } + } +} + +impl fmt::Display for TargetValidationReport { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if !self.imports_not_in_target.is_empty() { + writeln!( + f, + "Imports present in the component but not in the target world:" + )?; + for import in &self.imports_not_in_target { + writeln!(f, " - {}", import)?; + } + } + if !self.missing_exports.is_empty() { + writeln!( + f, + "Exports required by target world but not present in the component:" + )?; + for (name, item_kind) in &self.missing_exports { + writeln!(f, " - {}: {:?}", name, item_kind)?; + } + } + if !self.mismatched_types.is_empty() { + writeln!( + f, + "Type mismatches between the target world and the component:" + )?; + for (name, (kind, error)) in &self.mismatched_types { + writeln!(f, " - {}: {:?} ({})", name, kind, error)?; + } + } + Ok(()) + } +} + +impl std::error::Error for TargetValidationReport {} + +impl TargetValidationReport { + /// Returns the set of imports present in the component but not in the target world. + pub fn imports_not_in_target(&self) -> impl Iterator { + self.imports_not_in_target.iter().map(|s| s.as_str()) + } + + /// Returns the exports not in the component but required by the target world. + pub fn missing_exports(&self) -> impl Iterator { + self.missing_exports.iter().map(|(s, k)| (s.as_str(), k)) + } + + /// Returns the mismatched types between the component and the target world. + pub fn mismatched_types(&self) -> impl Iterator { + self.mismatched_types + .iter() + .map(|(s, (k, e))| (s.as_str(), k, e)) + } +} + +/// Validate whether the component conforms to the given world. +/// +/// # Example +/// +/// ```ignore +/// let mut types = Types::default(); +/// +/// let mut resolve = wit_parser::Resolve::new(); +/// let pkg = resolve.push_dir(path_to_wit_dir)?.0; +/// let wit_bytes = wit_component::encode(&resolve, pkg)?; +/// let wit = Package::from_bytes("wit", None, wit_bytes, &mut types)?; +/// +/// let component_bytes = std::fs::read(path_to_component)?; +/// let component = Package::from_bytes("component", None, component_bytes, &mut types)?; +/// let wit_world = get_wit_world(&types, wit.ty())?; +/// +/// validate_target(&types, wit_world, component.ty())?; +/// ``` +pub fn validate_target( + types: &Types, + wit_world_id: WorldId, + component_world_id: WorldId, +) -> TargetValidationResult { + let component_world = &types[component_world_id]; + let wit_world = &types[wit_world_id]; + // The interfaces imported implicitly through uses. + let implicit_imported_interfaces = wit_world.implicit_imported_interfaces(types); + let mut cache = Default::default(); + let mut checker = SubtypeChecker::new(&mut cache); + + let mut report = TargetValidationReport::default(); + + // The output is allowed to import a subset of the world's imports + checker.invert(); + for (import, item_kind) in component_world.imports.iter() { + let Some(expected) = implicit_imported_interfaces + .get(import.as_str()) + .or_else(|| wit_world.imports.get(import)) + else { + report.imports_not_in_target.insert(import.to_owned()); + continue; + }; + + if let Err(e) = checker.is_subtype(expected.promote(), types, *item_kind, types) { + report + .mismatched_types + .insert(import.to_owned(), (ExternKind::Import, e)); + } + } + + checker.revert(); + + // The output must export every export in the world + for (name, expected) in &wit_world.exports { + let Some(export) = component_world.exports.get(name).copied() else { + report.missing_exports.insert(name.to_owned(), *expected); + continue; + }; + + if let Err(e) = checker.is_subtype(export, types, expected.promote(), types) { + report + .mismatched_types + .insert(name.to_owned(), (ExternKind::Export, e)); + } + } + + report.into() +} diff --git a/src/commands/targets.rs b/src/commands/targets.rs index 2512e6a..820bffb 100644 --- a/src/commands/targets.rs +++ b/src/commands/targets.rs @@ -4,7 +4,7 @@ use std::{ fs, path::{Path, PathBuf}, }; -use wac_types::{ExternKind, ItemKind, Package, SubtypeChecker, Types, WorldId}; +use wac_types::{validate_target, ItemKind, Package, Types, WorldId}; /// Verifies that a given WebAssembly component targets a world. #[derive(Args)] @@ -105,89 +105,3 @@ fn encode_wit_as_component(path: &Path) -> anyhow::Result> { })?; Ok(encoded) } - -/// An error in target validation -#[derive(thiserror::Error, miette::Diagnostic, Debug)] -#[diagnostic(code("component does not match wit world"))] -pub enum Error { - #[error("the target wit does not have an import named `{import}` but the component does")] - /// The import is not in the target world - ImportNotInTarget { - /// The name of the missing target - import: String, - }, - #[error("{kind} `{name}` has a mismatched type for targeted wit world")] - /// An import or export has a mismatched type for the target world. - TargetMismatch { - /// The name of the mismatched item - name: String, - /// The extern kind of the item - kind: ExternKind, - /// The source of the error - #[source] - source: anyhow::Error, - }, - #[error("the targeted wit world requires an export named `{name}` but the component did not export one")] - /// Missing an export for the target world. - MissingTargetExport { - /// The export name. - name: String, - /// The expected item kind. - kind: ItemKind, - }, -} - -/// Validate whether the component conforms to the given world -pub fn validate_target( - types: &Types, - wit_world_id: WorldId, - component_world_id: WorldId, -) -> Result<(), Error> { - let component_world = &types[component_world_id]; - let wit_world = &types[wit_world_id]; - // The interfaces imported implicitly through uses. - let implicit_imported_interfaces = wit_world.implicit_imported_interfaces(types); - let mut cache = Default::default(); - let mut checker = SubtypeChecker::new(&mut cache); - - // The output is allowed to import a subset of the world's imports - checker.invert(); - for (import, item_kind) in component_world.imports.iter() { - let expected = implicit_imported_interfaces - .get(import.as_str()) - .or_else(|| wit_world.imports.get(import)) - .ok_or_else(|| Error::ImportNotInTarget { - import: import.to_owned(), - })?; - - checker - .is_subtype(expected.promote(), types, *item_kind, types) - .map_err(|e| Error::TargetMismatch { - kind: ExternKind::Import, - name: import.to_owned(), - source: e, - })?; - } - - checker.revert(); - - // The output must export every export in the world - for (name, expected) in &wit_world.exports { - let export = component_world.exports.get(name).copied().ok_or_else(|| { - Error::MissingTargetExport { - name: name.clone(), - kind: *expected, - } - })?; - - checker - .is_subtype(export, types, expected.promote(), types) - .map_err(|e| Error::TargetMismatch { - kind: ExternKind::Export, - name: name.clone(), - source: e, - })?; - } - - Ok(()) -}