⎇ b08d5cbe0a6222f5b1479f40e68346b5d38246a8
crates/protocol/brahman-card-wit/src/lib.rs
Historia
Quién tocó qué
crudo
refactor(monorepo): reorganización lógica + renames + SDDs + split CHANGELOG
1//! `brahman-card-wit` — extractor de contratos WIT.
2//!
3//! Crate **opcional** (no es dep de `brahman-card`). Parsea texto WIT
4//! mediante [`wit-parser`] y devuelve una lista de [`WitInterface`]
5//! (uno por `world`) lista para acoplarse a una [`brahman_card::Card`]
6//! cuando se construye una [`brahman_card::ResolvedCard`].
7//!
8//! Casos de uso:
9//!
10//! - El Init lee `<modulo>/wit/protocol.wit` durante el descubrimiento
11//! y lo combina con la Card del módulo para obtener una
12//! `ResolvedCard::from_conscious(card, wit)`.
13//! - Tooling (`brahman-wit-info`) inspecciona un `.wit` y muestra
14//! sus mundos, exports e imports.
15//!
16//! No depende de `wasm-tools`/`wit-component` — sólo del parser texto.
17
18#![forbid(unsafe_code)]
19#![warn(rust_2018_idioms)]
20
21use std::path::{Path, PathBuf};
22
23use brahman_card::WitInterface;
24use thiserror::Error;
25use wit_parser::{Resolve, WorldKey};
26
27#[derive(Debug, Error)]
28pub enum WitError {
29 #[error("parse: {0}")]
30 Parse(String),
31 #[error("E/S: {0}")]
32 Io(#[from] std::io::Error),
33}
34
35/// Parsea WIT desde una string. Devuelve un `WitInterface` por cada
36/// `world` declarado.
37pub fn parse_wit(source: &str) -> Result<Vec<WitInterface>, WitError> {
38 parse_with_path(source, Path::new("inline.wit"))
39}
40
41/// Parsea WIT desde un archivo. Útil para `module/wit/protocol.wit`.
42pub fn parse_wit_file(path: impl AsRef<Path>) -> Result<Vec<WitInterface>, WitError> {
43 let p = path.as_ref();
44 let source = std::fs::read_to_string(p)?;
45 parse_with_path(&source, p)
46}
47
48fn parse_with_path(source: &str, path: &Path) -> Result<Vec<WitInterface>, WitError> {
49 let mut resolve = Resolve::new();
50 let path_buf: PathBuf = path.to_path_buf();
51 resolve
52 .push_str(&path_buf, source)
53 .map_err(|e| WitError::Parse(e.to_string()))?;
54
55 let mut out = Vec::new();
56 for (_pkg_id, pkg) in resolve.packages.iter() {
57 let pkg_name = pkg.name.to_string();
58 for (_name, &world_id) in &pkg.worlds {
59 let world = &resolve.worlds[world_id];
60 let exports = collect_keys(world.exports.iter().map(|(k, _)| k), &resolve);
61 let imports = collect_keys(world.imports.iter().map(|(k, _)| k), &resolve);
62 out.push(WitInterface {
63 package: pkg_name.clone(),
64 world: world.name.clone(),
65 exports,
66 imports,
67 });
68 }
69 }
70 Ok(out)
71}
72
73fn collect_keys<'a, I>(keys: I, resolve: &Resolve) -> Vec<String>
74where
75 I: Iterator<Item = &'a WorldKey>,
76{
77 keys.map(|k| match k {
78 WorldKey::Name(n) => n.clone(),
79 WorldKey::Interface(id) => resolve.interfaces[*id]
80 .name
81 .clone()
82 .unwrap_or_else(|| format!("<interface#{}>", id.index())),
83 })
84 .collect()
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90
91 const SAMPLE: &str = r#"
92package brahman:test@0.1.0;
93
94interface handshake {
95 hello: func() -> result<_, string>;
96}
97
98interface lifecycle {
99 report: func();
100}
101
102world module {
103 import handshake;
104 import lifecycle;
105 export run: func() -> result<_, string>;
106}
107"#;
108
109 #[test]
110 fn parses_inline_wit() {
111 let worlds = parse_wit(SAMPLE).unwrap();
112 assert_eq!(worlds.len(), 1, "esperaba un único world");
113 let w = &worlds[0];
114 assert!(w.package.starts_with("brahman:test"));
115 assert_eq!(w.world, "module");
116 assert!(
117 w.imports.iter().any(|i| i == "handshake"),
118 "imports={:?}",
119 w.imports
120 );
121 assert!(
122 w.imports.iter().any(|i| i == "lifecycle"),
123 "imports={:?}",
124 w.imports
125 );
126 assert!(
127 w.exports.iter().any(|e| e == "run"),
128 "exports={:?}",
129 w.exports
130 );
131 }
132
133 #[test]
134 fn parses_shared_protocol() {
135 let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../../../shared_wit/protocol.wit");
136 let worlds = parse_wit_file(path).unwrap();
137 assert!(
138 worlds.iter().any(|w| w.world == "module"),
139 "no encontró world 'module' en {:?}",
140 worlds.iter().map(|w| &w.world).collect::<Vec<_>>()
141 );
142 assert!(
143 worlds.iter().any(|w| w.world == "admin-host"),
144 "no encontró world 'admin-host'"
145 );
146 }
147
148 #[test]
149 fn parse_error_on_garbage() {
150 let bad = "this is not wit at all { } } ;;;;";
151 assert!(matches!(parse_wit(bad), Err(WitError::Parse(_))));
152 }
153
154 #[test]
155 fn empty_world_handled() {
156 let src = r#"
157package brahman:empty@0.1.0;
158world hollow {}
159"#;
160 let worlds = parse_wit(src).unwrap();
161 assert_eq!(worlds.len(), 1);
162 assert!(worlds[0].exports.is_empty());
163 assert!(worlds[0].imports.is_empty());
164 }
165}