diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a5c165..579033b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,25 @@ ratio/diff ver `git show `. ## 2026-05-08 +### feat(core): brahman-card-wit — extractor opcional de contratos WIT +- Crate nuevo `crates/core/brahman-card-wit` con `wit-parser = "0.230"`. +- API: `parse_wit(source)` y `parse_wit_file(path)` devuelven + `Vec` (uno por `world` declarado). +- Interfaces importadas/exportadas (no sólo funciones) se resuelven + por nombre via `resolve.interfaces[id].name`. +- Example `crates/core/brahman-card-wit/examples/brahman-wit-info.rs` + CLI: `brahman-wit-info shared_wit/protocol.wit` → lista paquete, + worlds, imports y exports. +- 4 tests: inline, archivo real (`shared_wit/protocol.wit`), parse + error, world vacío. +- Validado contra `protocol.wit`: detecta worlds `module` y + `admin-host` con sus imports/exports correctos. + +### `7b589b8` chore: agrega CHANGELOG.md retroactivo +- `CHANGELOG.md` en la raíz con los 11 commits previos documentados + acción por acción. A partir de este punto, cada cambio sustantivo + actualiza también este archivo en el mismo commit. + ### `8a83a26` feat(handshake): notificación push de matches - Frame `MatchEvent { kind: Available | Lost, ... }` añadido al protocolo. - `Session::run_post_handshake` usa `tokio::select!` para multiplexar diff --git a/Cargo.lock b/Cargo.lock index a8a61bd..33cd755 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1176,6 +1176,16 @@ dependencies = [ "ulid", ] +[[package]] +name = "brahman-card-wit" +version = "0.1.0" +dependencies = [ + "anyhow", + "brahman-card", + "thiserror 2.0.18", + "wit-parser 0.230.0", +] + [[package]] name = "brahman-handshake" version = "0.1.0" @@ -10277,6 +10287,17 @@ dependencies = [ "indexmap 2.14.0", ] +[[package]] +name = "wasmparser" +version = "0.230.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808198a69b5a0535583370a51d459baa14261dfab04800c4864ee9e1a14346ed" +dependencies = [ + "bitflags 2.11.1", + "indexmap 2.14.0", + "semver", +] + [[package]] name = "wasmparser" version = "0.244.0" @@ -11123,7 +11144,7 @@ checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" dependencies = [ "anyhow", "heck 0.5.0", - "wit-parser", + "wit-parser 0.244.0", ] [[package]] @@ -11173,7 +11194,25 @@ dependencies = [ "wasm-encoder 0.244.0", "wasm-metadata", "wasmparser 0.244.0", - "wit-parser", + "wit-parser 0.244.0", +] + +[[package]] +name = "wit-parser" +version = "0.230.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "679fde5556495f98079a8e6b9ef8c887f731addaffa3d48194075c1dd5cd611b" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.14.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.230.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 2be928c..a894516 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ # core/ — Init y compat (arje absorbido) # ============================================================ "crates/core/brahman-card", + "crates/core/brahman-card-wit", "crates/core/brahman-handshake", "crates/core/brahman-broker", "crates/core/brahman-admin", diff --git a/crates/core/brahman-card-wit/Cargo.toml b/crates/core/brahman-card-wit/Cargo.toml new file mode 100644 index 0000000..f57bcf4 --- /dev/null +++ b/crates/core/brahman-card-wit/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "brahman-card-wit" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +publish.workspace = true +description = "Brahman — extractor opcional: parsea contratos WIT y devuelve `WitInterface` listo para acoplar a una `Card`." + +[dependencies] +brahman-card = { path = "../brahman-card" } +wit-parser = "0.230" +thiserror = { workspace = true } + +[dev-dependencies] +anyhow = { workspace = true } + +[[example]] +name = "brahman-wit-info" +path = "examples/brahman-wit-info.rs" diff --git a/crates/core/brahman-card-wit/examples/brahman-wit-info.rs b/crates/core/brahman-card-wit/examples/brahman-wit-info.rs new file mode 100644 index 0000000..bb8910f --- /dev/null +++ b/crates/core/brahman-card-wit/examples/brahman-wit-info.rs @@ -0,0 +1,45 @@ +//! `brahman-wit-info` — inspecciona un archivo WIT y lista sus worlds. +//! +//! Uso: +//! ```sh +//! cargo run -p brahman-card-wit --example brahman-wit-info -- shared_wit/protocol.wit +//! ``` + +use std::process::ExitCode; + +fn main() -> ExitCode { + let path = match std::env::args().nth(1) { + Some(p) => p, + None => { + eprintln!("uso: brahman-wit-info "); + return ExitCode::from(2); + } + }; + + let worlds = match brahman_card_wit::parse_wit_file(&path) { + Ok(w) => w, + Err(e) => { + eprintln!("error parseando {path}: {e}"); + return ExitCode::from(1); + } + }; + + if worlds.is_empty() { + println!("(ningún world declarado)"); + return ExitCode::SUCCESS; + } + + println!("{} world(s):", worlds.len()); + for w in &worlds { + println!(); + println!(" package: {}", w.package); + println!(" world: {}", w.world); + if !w.imports.is_empty() { + println!(" imports: {}", w.imports.join(", ")); + } + if !w.exports.is_empty() { + println!(" exports: {}", w.exports.join(", ")); + } + } + ExitCode::SUCCESS +} diff --git a/crates/core/brahman-card-wit/src/lib.rs b/crates/core/brahman-card-wit/src/lib.rs new file mode 100644 index 0000000..291b415 --- /dev/null +++ b/crates/core/brahman-card-wit/src/lib.rs @@ -0,0 +1,165 @@ +//! `brahman-card-wit` — extractor de contratos WIT. +//! +//! Crate **opcional** (no es dep de `brahman-card`). Parsea texto WIT +//! mediante [`wit-parser`] y devuelve una lista de [`WitInterface`] +//! (uno por `world`) lista para acoplarse a una [`brahman_card::Card`] +//! cuando se construye una [`brahman_card::ResolvedCard`]. +//! +//! Casos de uso: +//! +//! - El Init lee `/wit/protocol.wit` durante el descubrimiento +//! y lo combina con la Card del módulo para obtener una +//! `ResolvedCard::from_conscious(card, wit)`. +//! - Tooling (`brahman-wit-info`) inspecciona un `.wit` y muestra +//! sus mundos, exports e imports. +//! +//! No depende de `wasm-tools`/`wit-component` — sólo del parser texto. + +#![forbid(unsafe_code)] +#![warn(rust_2018_idioms)] + +use std::path::{Path, PathBuf}; + +use brahman_card::WitInterface; +use thiserror::Error; +use wit_parser::{Resolve, WorldKey}; + +#[derive(Debug, Error)] +pub enum WitError { + #[error("parse: {0}")] + Parse(String), + #[error("E/S: {0}")] + Io(#[from] std::io::Error), +} + +/// Parsea WIT desde una string. Devuelve un `WitInterface` por cada +/// `world` declarado. +pub fn parse_wit(source: &str) -> Result, WitError> { + parse_with_path(source, Path::new("inline.wit")) +} + +/// Parsea WIT desde un archivo. Útil para `module/wit/protocol.wit`. +pub fn parse_wit_file(path: impl AsRef) -> Result, WitError> { + let p = path.as_ref(); + let source = std::fs::read_to_string(p)?; + parse_with_path(&source, p) +} + +fn parse_with_path(source: &str, path: &Path) -> Result, WitError> { + let mut resolve = Resolve::new(); + let path_buf: PathBuf = path.to_path_buf(); + resolve + .push_str(&path_buf, source) + .map_err(|e| WitError::Parse(e.to_string()))?; + + let mut out = Vec::new(); + for (_pkg_id, pkg) in resolve.packages.iter() { + let pkg_name = pkg.name.to_string(); + for (_name, &world_id) in &pkg.worlds { + let world = &resolve.worlds[world_id]; + let exports = collect_keys(world.exports.iter().map(|(k, _)| k), &resolve); + let imports = collect_keys(world.imports.iter().map(|(k, _)| k), &resolve); + out.push(WitInterface { + package: pkg_name.clone(), + world: world.name.clone(), + exports, + imports, + }); + } + } + Ok(out) +} + +fn collect_keys<'a, I>(keys: I, resolve: &Resolve) -> Vec +where + I: Iterator, +{ + keys.map(|k| match k { + WorldKey::Name(n) => n.clone(), + WorldKey::Interface(id) => resolve.interfaces[*id] + .name + .clone() + .unwrap_or_else(|| format!("", id.index())), + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + const SAMPLE: &str = r#" +package brahman:test@0.1.0; + +interface handshake { + hello: func() -> result<_, string>; +} + +interface lifecycle { + report: func(); +} + +world module { + import handshake; + import lifecycle; + export run: func() -> result<_, string>; +} +"#; + + #[test] + fn parses_inline_wit() { + let worlds = parse_wit(SAMPLE).unwrap(); + assert_eq!(worlds.len(), 1, "esperaba un único world"); + let w = &worlds[0]; + assert!(w.package.starts_with("brahman:test")); + assert_eq!(w.world, "module"); + assert!( + w.imports.iter().any(|i| i == "handshake"), + "imports={:?}", + w.imports + ); + assert!( + w.imports.iter().any(|i| i == "lifecycle"), + "imports={:?}", + w.imports + ); + assert!( + w.exports.iter().any(|e| e == "run"), + "exports={:?}", + w.exports + ); + } + + #[test] + fn parses_shared_protocol() { + let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../../../shared_wit/protocol.wit"); + let worlds = parse_wit_file(path).unwrap(); + assert!( + worlds.iter().any(|w| w.world == "module"), + "no encontró world 'module' en {:?}", + worlds.iter().map(|w| &w.world).collect::>() + ); + assert!( + worlds.iter().any(|w| w.world == "admin-host"), + "no encontró world 'admin-host'" + ); + } + + #[test] + fn parse_error_on_garbage() { + let bad = "this is not wit at all { } } ;;;;"; + assert!(matches!(parse_wit(bad), Err(WitError::Parse(_)))); + } + + #[test] + fn empty_world_handled() { + let src = r#" +package brahman:empty@0.1.0; +world hollow {} +"#; + let worlds = parse_wit(src).unwrap(); + assert_eq!(worlds.len(), 1); + assert!(worlds[0].exports.is_empty()); + assert!(worlds[0].imports.is_empty()); + } +}