tawasuyu

tawasuyu/brahman

Rust 226TOML 67JSON 14HTML 12860 Definitions

feat(core): brahman-card-wit — extractor opcional de contratos WIT

Crate nuevo crates/core/brahman-card-wit que parsea texto WIT con
wit-parser y devuelve un Vec<WitInterface> (de brahman-card),
listo para acoplarse a una ResolvedCard::from_conscious(card, wit).

Ámbito intencional: sólo parsing texto, no toca wasm-tools ni
wit-component. Es opt-in: brahman-card no depende de éste.

API pública:
- parse_wit(source: &str) -> Result<Vec<WitInterface>, WitError>
- parse_wit_file(path: impl AsRef<Path>) -> Result<...>

Cada WitInterface incluye: package, world, imports, exports.
Las interfaces importadas/exportadas (no sólo funciones) se
resuelven por nombre via resolve.interfaces[id].name; las
funciones inline aparecen como WorldKey::Name directo.

Example CLI: brahman-wit-info <ruta.wit> imprime los worlds.

  $ brahman-wit-info shared_wit/protocol.wit
  2 world(s):
    package: brahman:protocol@0.1.0
    world:   module
    imports: types, handshake, lifecycle
    exports: run
    ...

Tests: 4/4 (inline + archivo real + parse error + world vacío).
Workspace: 0 errores.

CHANGELOG.md actualizado con la entrada nueva y la del commit
anterior (7b589b8) que faltaba.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

f4dc01900492ebebac247d2d56988db535784bcf · by Sergio · (5 months ago) · parent 7b589b863 · browse the tree at this commit

Semantic delta · 12

crates/core/brahman-card-wit/examples/brahman-wit-info.rs

crates/core/brahman-card-wit/src/lib.rs

6 files · +292 −2

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 <sha>`.
 
 ## 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<WitInterface>` (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 <ruta.wit>");
+            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 `<modulo>/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<Vec<WitInterface>, 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<Path>) -> Result<Vec<WitInterface>, 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<Vec<WitInterface>, 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<String>
+where
+    I: Iterator<Item = &'a WorldKey>,
+{
+    keys.map(|k| match k {
+        WorldKey::Name(n) => n.clone(),
+        WorldKey::Interface(id) => resolve.interfaces[*id]
+            .name
+            .clone()
+            .unwrap_or_else(|| format!("<interface#{}>", 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::<Vec<_>>()
+        );
+        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());
+    }
+}