use std::{
collections::{HashMap, HashSet},
fmt::Display,
};
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct WitWorld {
pub imports: HashSet<WitInterface>,
pub exports: HashSet<WitInterface>,
}
impl WitWorld {
pub fn includes(&self, interface: &WitInterface) -> bool {
self.imports.iter().any(|i| i.contains(interface))
|| self.exports.iter().any(|e| e.contains(interface))
}
pub fn includes_bidirectional(&self, interface: &WitInterface) -> bool {
let import_match = self.imports.iter().find(|i| {
if let Some(v) = &interface.version
&& let Some(ov) = &i.version
&& v != ov
{
return false;
}
i.namespace == interface.namespace && i.package == interface.package
});
let export_match = self.exports.iter().find(|e| {
if let Some(v) = &interface.version
&& let Some(ov) = &e.version
&& v != ov
{
return false;
}
e.namespace == interface.namespace && e.package == interface.package
});
for i in &interface.interfaces {
if let Some(im) = &import_match
&& im.interfaces.contains(i)
{
continue;
}
if let Some(em) = &export_match
&& em.interfaces.contains(i)
{
continue;
}
return false;
}
true
}
pub fn satisfies(&self, guest: &WitWorld) -> bool {
for required in &guest.imports {
let matched: Vec<_> = self
.exports
.iter()
.filter(|provided| provided.contains(required))
.collect();
if matched.is_empty() {
return false;
}
if matched.len() > 1 {
let versions: HashSet<_> =
matched.iter().filter_map(|m| m.version.as_ref()).collect();
if versions.len() > 1 {
return false; }
}
}
true
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct WitInterface {
pub namespace: String,
pub package: String,
pub interfaces: HashSet<String>,
pub version: Option<semver::Version>,
pub config: HashMap<String, String>,
}
impl WitInterface {
pub fn instance(&self) -> String {
if let Some(v) = &self.version {
format!("{}:{}@{v}", self.namespace, self.package)
} else {
format!("{}:{}", self.namespace, self.package)
}
}
pub fn merge(&mut self, other: &WitInterface) -> bool {
if self.instance() != other.instance() {
return false;
}
self.interfaces.extend(other.interfaces.clone());
self.config.extend(other.config.clone());
true
}
pub fn contains(&self, other: &WitInterface) -> bool {
if self.namespace != other.namespace || self.package != other.package {
return false;
}
if let Some(v) = &self.version
&& let Some(ov) = &other.version
&& v != ov
{
return false;
}
self.interfaces.is_superset(&other.interfaces)
}
}
impl Display for WitInterface {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}", self.namespace, self.package)?;
if !self.interfaces.is_empty() && !self.interfaces.is_empty() {
write!(f, "/")?;
let interfaces: Vec<_> = self.interfaces.clone().into_iter().collect();
write!(f, "{}", interfaces.join(","))?;
}
if let Some(v) = &self.version {
write!(f, "@{}", v)?;
}
Ok(())
}
}
impl std::hash::Hash for WitInterface {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.namespace.hash(state);
self.package.hash(state);
for iface in &self.interfaces {
iface.hash(state);
}
self.version.hash(state);
for (k, v) in &self.config {
k.hash(state);
v.hash(state);
}
}
}
impl From<&str> for WitInterface {
fn from(s: &str) -> Self {
let (main, version) = match s.split_once('@') {
Some((m, v)) => (m, Some(v)),
None => (s, None),
};
let (namespace_package, interface) = match main.split_once('/') {
Some((np, iface)) => (np, Some(iface)),
None => (main, None),
};
let (namespace, package) = match namespace_package.split_once(':') {
Some((ns, pkg)) => (ns, pkg),
None => ("", namespace_package),
};
let interfaces = match interface {
Some(iface) => iface
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect(),
None => HashSet::new(),
};
let version = version.and_then(|v| semver::Version::parse(v).ok());
WitInterface {
namespace: namespace.to_string(),
package: package.to_string(),
interfaces,
version,
config: HashMap::new(),
}
}
}
impl From<String> for WitInterface {
fn from(s: String) -> Self {
WitInterface::from(s.as_str())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
fn create_interface(namespace: &str, package: &str, interfaces: &[&str]) -> WitInterface {
WitInterface {
namespace: namespace.to_string(),
package: package.to_string(),
interfaces: interfaces.iter().map(|s| s.to_string()).collect(),
version: None,
config: HashMap::new(),
}
}
fn create_interface_with_version(
namespace: &str,
package: &str,
interfaces: &[&str],
version: &str,
) -> WitInterface {
WitInterface {
namespace: namespace.to_string(),
package: package.to_string(),
interfaces: interfaces.iter().map(|s| s.to_string()).collect(),
version: Some(semver::Version::parse(version).unwrap()),
config: HashMap::new(),
}
}
#[test]
fn test_contains_basic() {
let interface_a = create_interface("wasi", "logging", &["log", "error", "debug"]);
let interface_b = create_interface("wasi", "logging", &["log", "error"]);
let interface_c = create_interface("wasi", "logging", &["log", "trace"]);
assert!(interface_a.contains(&interface_b));
assert!(!interface_b.contains(&interface_a));
assert!(!interface_a.contains(&interface_c));
assert!(interface_a.contains(&interface_a));
}
#[test]
fn test_contains_namespace_and_package_matching() {
let wit1 = WitInterface::from("wasi:blobstore");
let wit2 = WitInterface::from("custom:blobstore");
assert!(!wit1.contains(&wit2));
let wit3 = WitInterface::from("wasi:blobstore");
let wit4 = WitInterface::from("wasi:keyvalue");
assert!(!wit3.contains(&wit4));
let wit5 = WitInterface::from("wasi:blobstore");
let wit6 = WitInterface::from("wasi:blobstore");
assert!(wit5.contains(&wit6));
let wit7 = WitInterface::from("blobstore/types");
let wit8 = WitInterface::from("blobstore/types");
assert!(wit7.contains(&wit8));
let wit9 = WitInterface::from("wasi:blobstore/types");
let wit10 = WitInterface::from("blobstore/types");
assert!(!wit9.contains(&wit10));
}
#[test]
fn test_contains_interface_subsets() {
let wit1 = WitInterface::from("wasi:blobstore/types,container,blobstore");
let wit2 = WitInterface::from("wasi:blobstore/types,container");
assert!(wit1.contains(&wit2));
let wit3 = WitInterface::from("wasi:blobstore/types");
let wit4 = WitInterface::from("wasi:blobstore/types,container");
assert!(!wit3.contains(&wit4));
let wit5 = WitInterface::from("wasi:blobstore/types,container");
let wit6 = WitInterface::from("wasi:blobstore");
assert!(wit5.contains(&wit6));
let wit7 = WitInterface::from("wasi:cli/stdin,stdout");
let wit8 = WitInterface::from("wasi:cli/stdout,stderr");
assert!(!wit7.contains(&wit8));
let wit9 = WitInterface::from("wasi:cli/environment,exit,stdin,stdout");
let wit10 = WitInterface::from("wasi:cli/environment");
assert!(wit9.contains(&wit10));
}
#[test]
fn test_contains_version_handling() {
let wit1 = WitInterface::from("wasi:blobstore/types@0.2.0");
let wit2 = WitInterface::from("wasi:blobstore/types@0.2.0");
assert!(wit1.contains(&wit2));
let wit3 = WitInterface::from("wasi:blobstore/types@0.2.0");
let wit4 = WitInterface::from("wasi:blobstore/types@0.3.0");
assert!(!wit3.contains(&wit4));
let wit5 = WitInterface::from("wasi:blobstore/types@0.2.0");
let wit6 = WitInterface::from("wasi:blobstore/types");
assert!(wit5.contains(&wit6));
let wit7 = WitInterface::from("wasi:blobstore/types");
let wit8 = WitInterface::from("wasi:blobstore/types@0.2.0");
assert!(wit7.contains(&wit8));
let wit9 = WitInterface::from("wasi:http/types,incoming-handler,outgoing-handler@0.2.0");
let wit10 = WitInterface::from("wasi:http/types,incoming-handler@0.2.0");
assert!(wit9.contains(&wit10));
}
#[test]
fn test_contains_with_version() {
let interface_a = create_interface_with_version("wasi", "http", &["handler"], "0.2.0");
let interface_b = create_interface_with_version("wasi", "http", &["handler"], "0.2.0");
let interface_c = create_interface_with_version("wasi", "http", &["handler"], "0.3.0");
assert!(interface_a.contains(&interface_b));
assert!(!interface_a.contains(&interface_c));
}
#[test]
fn test_contains_config_ignored() {
let mut wit1 = WitInterface::from("wasi:blobstore/types");
wit1.config.insert("key".to_string(), "value1".to_string());
let mut wit2 = WitInterface::from("wasi:blobstore/types");
wit2.config.insert("key".to_string(), "value2".to_string());
assert!(wit1.contains(&wit2));
}
#[test]
fn test_world_includes() {
let required_interface = create_interface("wasi", "keyvalue", &["get"]);
let broader_interface = create_interface("wasi", "keyvalue", &["get", "set"]);
let different_interface = create_interface("wasi", "logging", &["log"]);
let world1 = WitWorld {
imports: [required_interface.clone()].iter().cloned().collect(),
exports: HashSet::new(),
};
assert!(world1.includes(&required_interface));
let world2 = WitWorld {
imports: [broader_interface.clone()].iter().cloned().collect(),
exports: HashSet::new(),
};
assert!(world2.includes(&required_interface));
assert!(!world1.includes(&broader_interface));
let world3 = WitWorld {
imports: HashSet::new(),
exports: [broader_interface.clone()].iter().cloned().collect(),
};
assert!(world3.includes(&required_interface));
let world4 = WitWorld {
imports: [different_interface].iter().cloned().collect(),
exports: HashSet::new(),
};
assert!(!world4.includes(&required_interface));
}
#[test]
fn test_world_satisfies() {
let guest_world = WitWorld {
imports: [
create_interface("wasi", "logging", &["log"]),
create_interface("wasi", "keyvalue", &["get", "exists"]),
]
.iter()
.cloned()
.collect(),
exports: HashSet::new(),
};
let host_world_exact = WitWorld {
imports: HashSet::new(),
exports: [
create_interface("wasi", "logging", &["log"]),
create_interface("wasi", "keyvalue", &["get", "exists"]),
]
.iter()
.cloned()
.collect(),
};
assert!(host_world_exact.satisfies(&guest_world));
let host_world_superset = WitWorld {
imports: HashSet::new(),
exports: [
create_interface("wasi", "logging", &["log", "error"]),
create_interface("wasi", "keyvalue", &["get", "exists", "set", "delete"]),
]
.iter()
.cloned()
.collect(),
};
assert!(host_world_superset.satisfies(&guest_world));
let host_world_missing = WitWorld {
imports: HashSet::new(),
exports: [create_interface("wasi", "logging", &["log"])]
.iter()
.cloned()
.collect(),
};
assert!(!host_world_missing.satisfies(&guest_world));
let host_world_subset = WitWorld {
imports: HashSet::new(),
exports: [
create_interface("wasi", "logging", &["log"]),
create_interface("wasi", "keyvalue", &["get"]), ]
.iter()
.cloned()
.collect(),
};
assert!(!host_world_subset.satisfies(&guest_world));
}
#[test]
fn test_parse_basic_formats() {
let wit1 = WitInterface::from("wasi:blobstore");
assert_eq!(wit1.namespace, "wasi");
assert_eq!(wit1.package, "blobstore");
assert!(wit1.interfaces.is_empty());
assert!(wit1.version.is_none());
let wit2 = WitInterface::from("wasi:http/incoming-handler");
assert_eq!(wit2.namespace, "wasi");
assert_eq!(wit2.package, "http");
assert_eq!(wit2.interfaces.len(), 1);
assert!(wit2.interfaces.contains("incoming-handler"));
let wit3 = WitInterface::from("wasi:http/incoming-handler,outgoing-handler,types");
assert_eq!(wit3.interfaces.len(), 3);
assert!(wit3.interfaces.contains("incoming-handler"));
assert!(wit3.interfaces.contains("outgoing-handler"));
assert!(wit3.interfaces.contains("types"));
let wit4 = WitInterface::from("mypackage");
assert_eq!(wit4.namespace, "");
assert_eq!(wit4.package, "mypackage");
assert!(wit4.interfaces.is_empty());
let wit5 = WitInterface::from("blobstore/types");
assert_eq!(wit5.namespace, "");
assert_eq!(wit5.package, "blobstore");
assert!(wit5.interfaces.contains("types"));
}
#[test]
fn test_parse_with_versions() {
let wit1 = WitInterface::from("wasi:blobstore/types@0.2.0");
assert_eq!(wit1.version, Some(semver::Version::parse("0.2.0").unwrap()));
let wit2 = WitInterface::from("wasi:keyvalue/store,atomics,batch@0.2.0-draft");
assert_eq!(wit2.interfaces.len(), 3);
assert_eq!(
wit2.version,
Some(semver::Version::parse("0.2.0-draft").unwrap())
);
let wit3 = WitInterface::from("mypackage/interface1,interface2@1.0.0");
assert_eq!(wit3.namespace, "");
assert_eq!(wit3.version, Some(semver::Version::parse("1.0.0").unwrap()));
let wit4 = WitInterface::from("mypackage@1.0.0");
assert!(wit4.interfaces.is_empty());
assert_eq!(wit4.version, Some(semver::Version::parse("1.0.0").unwrap()));
let wit5 = WitInterface::from("wasi:logging/logging@0.1.0-draft");
assert_eq!(
wit5.version,
Some(semver::Version::parse("0.1.0-draft").unwrap())
);
let wit6 = WitInterface::from("wasi:cli/environment@0.2.0-rc.2024-12-05");
assert_eq!(
wit6.version,
Some(semver::Version::parse("0.2.0-rc.2024-12-05").unwrap())
);
let wit7 = WitInterface::from("wasi:blobstore/types@invalid-version");
assert!(wit7.version.is_none());
}
#[test]
fn test_parse_edge_cases() {
let wit1 = WitInterface::from("wasi:http/incoming-handler, outgoing-handler , types");
assert_eq!(wit1.interfaces.len(), 3);
assert!(wit1.interfaces.contains("incoming-handler"));
assert!(wit1.interfaces.contains("outgoing-handler"));
assert!(wit1.interfaces.contains("types"));
let wit2 = WitInterface::from("wasi:http/incoming-handler,");
assert_eq!(wit2.interfaces.len(), 1);
assert!(wit2.interfaces.contains("incoming-handler"));
let wit3 = WitInterface::from("wasi:http/,incoming-handler");
assert_eq!(wit3.interfaces.len(), 1);
assert!(wit3.interfaces.contains("incoming-handler"));
let wit4 = WitInterface::from("wasi:http/incoming-handler,,outgoing-handler");
assert_eq!(wit4.interfaces.len(), 2);
let wit5 = WitInterface::from("foo:bar:baz/interface");
assert_eq!(wit5.namespace, "foo");
assert_eq!(wit5.package, "bar:baz");
assert!(wit5.interfaces.contains("interface"));
}
#[test]
fn test_parse_from_string() {
let iface: WitInterface = "wasi:http/incoming-handler@0.2.0".into();
assert_eq!(iface.namespace, "wasi");
assert_eq!(iface.package, "http");
assert_eq!(iface.interfaces.len(), 1);
assert!(iface.interfaces.contains("incoming-handler"));
assert_eq!(
iface.version,
Some(semver::Version::parse("0.2.0").unwrap())
);
let iface2: WitInterface = "wasmcloud:messaging".into();
assert_eq!(iface2.namespace, "wasmcloud");
assert_eq!(iface2.package, "messaging");
assert!(iface2.interfaces.is_empty());
assert_eq!(iface2.version, None);
let iface3: WitInterface = "wasi:keyvalue/store,atomic@0.1.0".into();
assert_eq!(iface3.namespace, "wasi");
assert_eq!(iface3.package, "keyvalue");
assert_eq!(iface3.interfaces.len(), 2);
assert!(iface3.interfaces.contains("store"));
assert!(iface3.interfaces.contains("atomic"));
}
#[test]
fn test_display() {
let iface = create_interface("wasi", "http", &["incoming-handler"]);
assert_eq!(format!("{}", iface), "wasi:http/incoming-handler");
let iface_with_version =
create_interface_with_version("wasi", "http", &["incoming-handler"], "0.2.0");
assert_eq!(
format!("{}", iface_with_version),
"wasi:http/incoming-handler@0.2.0"
);
let iface_no_interfaces = create_interface("wasi", "logging", &[]);
assert_eq!(format!("{}", iface_no_interfaces), "wasi:logging");
}
}