1use std::{
19 collections::{HashMap, HashSet},
20 fmt::Display,
21};
22
23#[derive(Clone, Debug, Default, PartialEq, Eq)]
29pub struct WitWorld {
30 pub imports: HashSet<WitInterface>,
32 pub exports: HashSet<WitInterface>,
34}
35
36impl WitWorld {
37 pub fn includes(&self, interface: &WitInterface) -> bool {
42 self.imports.iter().any(|i| i.contains(interface))
43 || self.exports.iter().any(|e| e.contains(interface))
44 }
45
46 pub fn includes_bidirectional(&self, interface: &WitInterface) -> bool {
50 let import_match = self.imports.iter().find(|i| {
51 if let Some(v) = &interface.version
52 && let Some(ov) = &i.version
53 && v != ov
54 {
55 return false;
56 }
57 i.namespace == interface.namespace && i.package == interface.package
58 });
59
60 let export_match = self.exports.iter().find(|e| {
61 if let Some(v) = &interface.version
63 && let Some(ov) = &e.version
64 && v != ov
65 {
66 return false;
67 }
68 e.namespace == interface.namespace && e.package == interface.package
69 });
70
71 for i in &interface.interfaces {
73 if let Some(im) = &import_match
75 && im.interfaces.contains(i)
76 {
77 continue;
78 }
79 if let Some(em) = &export_match
81 && em.interfaces.contains(i)
82 {
83 continue;
84 }
85
86 return false;
87 }
88
89 true
90 }
91
92 pub fn satisfies(&self, guest: &WitWorld) -> bool {
105 for required in &guest.imports {
107 let matched: Vec<_> = self
109 .exports
110 .iter()
111 .filter(|provided| provided.contains(required))
112 .collect();
113
114 if matched.is_empty() {
116 return false;
117 }
118
119 if matched.len() > 1 {
121 let versions: HashSet<_> =
123 matched.iter().filter_map(|m| m.version.as_ref()).collect();
124 if versions.len() > 1 {
125 return false; }
127 }
128 }
129
130 true
131 }
132}
133
134#[derive(Clone, Debug, PartialEq, Eq)]
145pub struct WitInterface {
146 pub namespace: String,
148 pub package: String,
150 pub interfaces: HashSet<String>,
152 pub version: Option<semver::Version>,
156 pub config: HashMap<String, String>,
158}
159
160impl WitInterface {
161 pub fn instance(&self) -> String {
164 if let Some(v) = &self.version {
165 format!("{}:{}@{v}", self.namespace, self.package)
166 } else {
167 format!("{}:{}", self.namespace, self.package)
168 }
169 }
170
171 pub fn merge(&mut self, other: &WitInterface) -> bool {
174 if self.instance() != other.instance() {
175 return false;
176 }
177
178 self.interfaces.extend(other.interfaces.clone());
179 self.config.extend(other.config.clone());
180 true
181 }
182
183 pub fn contains(&self, other: &WitInterface) -> bool {
197 if self.namespace != other.namespace || self.package != other.package {
199 return false;
200 }
201
202 if let Some(v) = &self.version
204 && let Some(ov) = &other.version
205 && v != ov
206 {
207 return false;
208 }
209
210 self.interfaces.is_superset(&other.interfaces)
211 }
212}
213
214impl Display for WitInterface {
215 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
216 write!(f, "{}:{}", self.namespace, self.package)?;
217 if !self.interfaces.is_empty() && !self.interfaces.is_empty() {
218 write!(f, "/")?;
219 let interfaces: Vec<_> = self.interfaces.clone().into_iter().collect();
220 write!(f, "{}", interfaces.join(","))?;
221 }
222 if let Some(v) = &self.version {
223 write!(f, "@{}", v)?;
224 }
225 Ok(())
226 }
227}
228
229impl std::hash::Hash for WitInterface {
230 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
231 self.namespace.hash(state);
232 self.package.hash(state);
233 for iface in &self.interfaces {
234 iface.hash(state);
235 }
236 self.version.hash(state);
237 for (k, v) in &self.config {
238 k.hash(state);
239 v.hash(state);
240 }
241 }
242}
243
244impl From<&str> for WitInterface {
245 fn from(s: &str) -> Self {
246 let (main, version) = match s.split_once('@') {
251 Some((m, v)) => (m, Some(v)),
252 None => (s, None),
253 };
254 let (namespace_package, interface) = match main.split_once('/') {
255 Some((np, iface)) => (np, Some(iface)),
256 None => (main, None),
257 };
258 let (namespace, package) = match namespace_package.split_once(':') {
259 Some((ns, pkg)) => (ns, pkg),
260 None => ("", namespace_package),
261 };
262 let interfaces = match interface {
263 Some(iface) => iface
264 .split(',')
265 .map(|s| s.trim().to_string())
266 .filter(|s| !s.is_empty())
267 .collect(),
268 None => HashSet::new(),
269 };
270 let version = version.and_then(|v| semver::Version::parse(v).ok());
271
272 WitInterface {
273 namespace: namespace.to_string(),
274 package: package.to_string(),
275 interfaces,
276 version,
277 config: HashMap::new(),
278 }
279 }
280}
281
282impl From<String> for WitInterface {
283 fn from(s: String) -> Self {
284 WitInterface::from(s.as_str())
285 }
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291 use std::collections::HashSet;
292
293 fn create_interface(namespace: &str, package: &str, interfaces: &[&str]) -> WitInterface {
294 WitInterface {
295 namespace: namespace.to_string(),
296 package: package.to_string(),
297 interfaces: interfaces.iter().map(|s| s.to_string()).collect(),
298 version: None,
299 config: HashMap::new(),
300 }
301 }
302
303 fn create_interface_with_version(
304 namespace: &str,
305 package: &str,
306 interfaces: &[&str],
307 version: &str,
308 ) -> WitInterface {
309 WitInterface {
310 namespace: namespace.to_string(),
311 package: package.to_string(),
312 interfaces: interfaces.iter().map(|s| s.to_string()).collect(),
313 version: Some(semver::Version::parse(version).unwrap()),
314 config: HashMap::new(),
315 }
316 }
317
318 #[test]
319 fn test_contains_basic() {
320 let interface_a = create_interface("wasi", "logging", &["log", "error", "debug"]);
321 let interface_b = create_interface("wasi", "logging", &["log", "error"]);
322 let interface_c = create_interface("wasi", "logging", &["log", "trace"]);
323
324 assert!(interface_a.contains(&interface_b));
326 assert!(!interface_b.contains(&interface_a));
328 assert!(!interface_a.contains(&interface_c));
330 assert!(interface_a.contains(&interface_a));
332 }
333
334 #[test]
335 fn test_contains_namespace_and_package_matching() {
336 let wit1 = WitInterface::from("wasi:blobstore");
338 let wit2 = WitInterface::from("custom:blobstore");
339 assert!(!wit1.contains(&wit2));
340
341 let wit3 = WitInterface::from("wasi:blobstore");
343 let wit4 = WitInterface::from("wasi:keyvalue");
344 assert!(!wit3.contains(&wit4));
345
346 let wit5 = WitInterface::from("wasi:blobstore");
348 let wit6 = WitInterface::from("wasi:blobstore");
349 assert!(wit5.contains(&wit6));
350
351 let wit7 = WitInterface::from("blobstore/types");
353 let wit8 = WitInterface::from("blobstore/types");
354 assert!(wit7.contains(&wit8));
355
356 let wit9 = WitInterface::from("wasi:blobstore/types");
358 let wit10 = WitInterface::from("blobstore/types");
359 assert!(!wit9.contains(&wit10));
360 }
361
362 #[test]
363 fn test_contains_interface_subsets() {
364 let wit1 = WitInterface::from("wasi:blobstore/types,container,blobstore");
366 let wit2 = WitInterface::from("wasi:blobstore/types,container");
367 assert!(wit1.contains(&wit2));
368
369 let wit3 = WitInterface::from("wasi:blobstore/types");
371 let wit4 = WitInterface::from("wasi:blobstore/types,container");
372 assert!(!wit3.contains(&wit4));
373
374 let wit5 = WitInterface::from("wasi:blobstore/types,container");
376 let wit6 = WitInterface::from("wasi:blobstore");
377 assert!(wit5.contains(&wit6));
378
379 let wit7 = WitInterface::from("wasi:cli/stdin,stdout");
381 let wit8 = WitInterface::from("wasi:cli/stdout,stderr");
382 assert!(!wit7.contains(&wit8));
383
384 let wit9 = WitInterface::from("wasi:cli/environment,exit,stdin,stdout");
386 let wit10 = WitInterface::from("wasi:cli/environment");
387 assert!(wit9.contains(&wit10));
388 }
389
390 #[test]
391 fn test_contains_version_handling() {
392 let wit1 = WitInterface::from("wasi:blobstore/types@0.2.0");
394 let wit2 = WitInterface::from("wasi:blobstore/types@0.2.0");
395 assert!(wit1.contains(&wit2));
396
397 let wit3 = WitInterface::from("wasi:blobstore/types@0.2.0");
399 let wit4 = WitInterface::from("wasi:blobstore/types@0.3.0");
400 assert!(!wit3.contains(&wit4));
401
402 let wit5 = WitInterface::from("wasi:blobstore/types@0.2.0");
404 let wit6 = WitInterface::from("wasi:blobstore/types");
405 assert!(wit5.contains(&wit6));
406
407 let wit7 = WitInterface::from("wasi:blobstore/types");
409 let wit8 = WitInterface::from("wasi:blobstore/types@0.2.0");
410 assert!(wit7.contains(&wit8));
411
412 let wit9 = WitInterface::from("wasi:http/types,incoming-handler,outgoing-handler@0.2.0");
414 let wit10 = WitInterface::from("wasi:http/types,incoming-handler@0.2.0");
415 assert!(wit9.contains(&wit10));
416 }
417
418 #[test]
419 fn test_contains_with_version() {
420 let interface_a = create_interface_with_version("wasi", "http", &["handler"], "0.2.0");
421 let interface_b = create_interface_with_version("wasi", "http", &["handler"], "0.2.0");
422 let interface_c = create_interface_with_version("wasi", "http", &["handler"], "0.3.0");
423
424 assert!(interface_a.contains(&interface_b));
426 assert!(!interface_a.contains(&interface_c));
428 }
429
430 #[test]
431 fn test_contains_config_ignored() {
432 let mut wit1 = WitInterface::from("wasi:blobstore/types");
434 wit1.config.insert("key".to_string(), "value1".to_string());
435
436 let mut wit2 = WitInterface::from("wasi:blobstore/types");
437 wit2.config.insert("key".to_string(), "value2".to_string());
438
439 assert!(wit1.contains(&wit2));
441 }
442
443 #[test]
444 fn test_world_includes() {
445 let required_interface = create_interface("wasi", "keyvalue", &["get"]);
446 let broader_interface = create_interface("wasi", "keyvalue", &["get", "set"]);
447 let different_interface = create_interface("wasi", "logging", &["log"]);
448
449 let world1 = WitWorld {
451 imports: [required_interface.clone()].iter().cloned().collect(),
452 exports: HashSet::new(),
453 };
454 assert!(world1.includes(&required_interface));
455
456 let world2 = WitWorld {
458 imports: [broader_interface.clone()].iter().cloned().collect(),
459 exports: HashSet::new(),
460 };
461 assert!(world2.includes(&required_interface));
462 assert!(!world1.includes(&broader_interface));
463
464 let world3 = WitWorld {
466 imports: HashSet::new(),
467 exports: [broader_interface.clone()].iter().cloned().collect(),
468 };
469 assert!(world3.includes(&required_interface));
470
471 let world4 = WitWorld {
473 imports: [different_interface].iter().cloned().collect(),
474 exports: HashSet::new(),
475 };
476 assert!(!world4.includes(&required_interface));
477 }
478
479 #[test]
480 fn test_world_satisfies() {
481 let guest_world = WitWorld {
483 imports: [
484 create_interface("wasi", "logging", &["log"]),
485 create_interface("wasi", "keyvalue", &["get", "exists"]),
486 ]
487 .iter()
488 .cloned()
489 .collect(),
490 exports: HashSet::new(),
491 };
492
493 let host_world_exact = WitWorld {
495 imports: HashSet::new(),
496 exports: [
497 create_interface("wasi", "logging", &["log"]),
498 create_interface("wasi", "keyvalue", &["get", "exists"]),
499 ]
500 .iter()
501 .cloned()
502 .collect(),
503 };
504 assert!(host_world_exact.satisfies(&guest_world));
505
506 let host_world_superset = WitWorld {
508 imports: HashSet::new(),
509 exports: [
510 create_interface("wasi", "logging", &["log", "error"]),
511 create_interface("wasi", "keyvalue", &["get", "exists", "set", "delete"]),
512 ]
513 .iter()
514 .cloned()
515 .collect(),
516 };
517 assert!(host_world_superset.satisfies(&guest_world));
518
519 let host_world_missing = WitWorld {
521 imports: HashSet::new(),
522 exports: [create_interface("wasi", "logging", &["log"])]
523 .iter()
524 .cloned()
525 .collect(),
526 };
527 assert!(!host_world_missing.satisfies(&guest_world));
528
529 let host_world_subset = WitWorld {
531 imports: HashSet::new(),
532 exports: [
533 create_interface("wasi", "logging", &["log"]),
534 create_interface("wasi", "keyvalue", &["get"]), ]
536 .iter()
537 .cloned()
538 .collect(),
539 };
540 assert!(!host_world_subset.satisfies(&guest_world));
541 }
542
543 #[test]
544 fn test_parse_basic_formats() {
545 let wit1 = WitInterface::from("wasi:blobstore");
547 assert_eq!(wit1.namespace, "wasi");
548 assert_eq!(wit1.package, "blobstore");
549 assert!(wit1.interfaces.is_empty());
550 assert!(wit1.version.is_none());
551
552 let wit2 = WitInterface::from("wasi:http/incoming-handler");
554 assert_eq!(wit2.namespace, "wasi");
555 assert_eq!(wit2.package, "http");
556 assert_eq!(wit2.interfaces.len(), 1);
557 assert!(wit2.interfaces.contains("incoming-handler"));
558
559 let wit3 = WitInterface::from("wasi:http/incoming-handler,outgoing-handler,types");
561 assert_eq!(wit3.interfaces.len(), 3);
562 assert!(wit3.interfaces.contains("incoming-handler"));
563 assert!(wit3.interfaces.contains("outgoing-handler"));
564 assert!(wit3.interfaces.contains("types"));
565
566 let wit4 = WitInterface::from("mypackage");
568 assert_eq!(wit4.namespace, "");
569 assert_eq!(wit4.package, "mypackage");
570 assert!(wit4.interfaces.is_empty());
571
572 let wit5 = WitInterface::from("blobstore/types");
574 assert_eq!(wit5.namespace, "");
575 assert_eq!(wit5.package, "blobstore");
576 assert!(wit5.interfaces.contains("types"));
577 }
578
579 #[test]
580 fn test_parse_with_versions() {
581 let wit1 = WitInterface::from("wasi:blobstore/types@0.2.0");
583 assert_eq!(wit1.version, Some(semver::Version::parse("0.2.0").unwrap()));
584
585 let wit2 = WitInterface::from("wasi:keyvalue/store,atomics,batch@0.2.0-draft");
587 assert_eq!(wit2.interfaces.len(), 3);
588 assert_eq!(
589 wit2.version,
590 Some(semver::Version::parse("0.2.0-draft").unwrap())
591 );
592
593 let wit3 = WitInterface::from("mypackage/interface1,interface2@1.0.0");
595 assert_eq!(wit3.namespace, "");
596 assert_eq!(wit3.version, Some(semver::Version::parse("1.0.0").unwrap()));
597
598 let wit4 = WitInterface::from("mypackage@1.0.0");
600 assert!(wit4.interfaces.is_empty());
601 assert_eq!(wit4.version, Some(semver::Version::parse("1.0.0").unwrap()));
602
603 let wit5 = WitInterface::from("wasi:logging/logging@0.1.0-draft");
605 assert_eq!(
606 wit5.version,
607 Some(semver::Version::parse("0.1.0-draft").unwrap())
608 );
609
610 let wit6 = WitInterface::from("wasi:cli/environment@0.2.0-rc.2024-12-05");
612 assert_eq!(
613 wit6.version,
614 Some(semver::Version::parse("0.2.0-rc.2024-12-05").unwrap())
615 );
616
617 let wit7 = WitInterface::from("wasi:blobstore/types@invalid-version");
619 assert!(wit7.version.is_none());
620 }
621
622 #[test]
623 fn test_parse_edge_cases() {
624 let wit1 = WitInterface::from("wasi:http/incoming-handler, outgoing-handler , types");
626 assert_eq!(wit1.interfaces.len(), 3);
627 assert!(wit1.interfaces.contains("incoming-handler"));
628 assert!(wit1.interfaces.contains("outgoing-handler"));
629 assert!(wit1.interfaces.contains("types"));
630
631 let wit2 = WitInterface::from("wasi:http/incoming-handler,");
633 assert_eq!(wit2.interfaces.len(), 1);
634 assert!(wit2.interfaces.contains("incoming-handler"));
635
636 let wit3 = WitInterface::from("wasi:http/,incoming-handler");
638 assert_eq!(wit3.interfaces.len(), 1);
639 assert!(wit3.interfaces.contains("incoming-handler"));
640
641 let wit4 = WitInterface::from("wasi:http/incoming-handler,,outgoing-handler");
643 assert_eq!(wit4.interfaces.len(), 2);
644
645 let wit5 = WitInterface::from("foo:bar:baz/interface");
647 assert_eq!(wit5.namespace, "foo");
648 assert_eq!(wit5.package, "bar:baz");
649 assert!(wit5.interfaces.contains("interface"));
650 }
651
652 #[test]
653 fn test_parse_from_string() {
654 let iface: WitInterface = "wasi:http/incoming-handler@0.2.0".into();
655 assert_eq!(iface.namespace, "wasi");
656 assert_eq!(iface.package, "http");
657 assert_eq!(iface.interfaces.len(), 1);
658 assert!(iface.interfaces.contains("incoming-handler"));
659 assert_eq!(
660 iface.version,
661 Some(semver::Version::parse("0.2.0").unwrap())
662 );
663
664 let iface2: WitInterface = "wasmcloud:messaging".into();
665 assert_eq!(iface2.namespace, "wasmcloud");
666 assert_eq!(iface2.package, "messaging");
667 assert!(iface2.interfaces.is_empty());
668 assert_eq!(iface2.version, None);
669
670 let iface3: WitInterface = "wasi:keyvalue/store,atomic@0.1.0".into();
671 assert_eq!(iface3.namespace, "wasi");
672 assert_eq!(iface3.package, "keyvalue");
673 assert_eq!(iface3.interfaces.len(), 2);
674 assert!(iface3.interfaces.contains("store"));
675 assert!(iface3.interfaces.contains("atomic"));
676 }
677
678 #[test]
679 fn test_display() {
680 let iface = create_interface("wasi", "http", &["incoming-handler"]);
681 assert_eq!(format!("{}", iface), "wasi:http/incoming-handler");
682
683 let iface_with_version =
684 create_interface_with_version("wasi", "http", &["incoming-handler"], "0.2.0");
685 assert_eq!(
686 format!("{}", iface_with_version),
687 "wasi:http/incoming-handler@0.2.0"
688 );
689
690 let iface_no_interfaces = create_interface("wasi", "logging", &[]);
691 assert_eq!(format!("{}", iface_no_interfaces), "wasi:logging");
692 }
693}