feat: card standalone — primitiva de identidad soberana (hoja, autocontenida)
Tarjeta de Presentación canónica: identidad arje + flujos tipados brahman, content-addressed. Hoja pura sin deps internas — la base sobre la que se montan red (chasqui/minga) y escritorio. cargo check pasa (3 crates). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
# card — identidad agnóstica de transporte
|
||||
|
||||
Contrato de identidad y membresía **independiente del transporte**: claves
|
||||
Ed25519, `EspinaId` (hash de la clave pública), firmas y handshake de membresía
|
||||
de una "espina" (red privada). El mismo contrato lo implementan dos transportes
|
||||
distintos: `card-net` (libp2p) y `wawa-akasha` (protocolo propio de wawa).
|
||||
|
||||
## Subcrates
|
||||
|
||||
- **`card-core`** — el contrato agnóstico real: par Ed25519 → `EspinaId`,
|
||||
`Card` firmada, `handshake` de membresía (un miembro presenta su tarjeta
|
||||
firmada; el anfitrión la verifica contra la raíz de confianza de la espina).
|
||||
Sólo bytes firmados — no sabe de libp2p ni Akasha.
|
||||
- **`card-net`** — espina dorsal P2P sobre **libp2p**: discovery (mDNS +
|
||||
Kademlia DHT), gossipsub, NAT traversal (Circuit Relay v2 + DCUtR + AutoNAT).
|
||||
Implementa el contrato de `card-core` sobre libp2p. Lo consume `khipu`.
|
||||
- **`card-wit`** — **[DORMIDO]** binding WIT/wasm del contrato. Se reactiva
|
||||
cuando `card` cruce a apps WASM; hoy el contrato real es `card-core`.
|
||||
|
||||
## Estado (2026-05-31)
|
||||
|
||||
### Hecho
|
||||
- `card-core`: identidad Ed25519 + `EspinaId` + handshake de membresía (contrato
|
||||
agnóstico declarado como la fuente de verdad).
|
||||
- `card-net`: discovery (mDNS+DHT), gossipsub y NAT traversal completo
|
||||
(Relay v2 + DCUtR + AutoNAT); discovery de personas por `DhtKey::Persona`.
|
||||
|
||||
### Pendiente
|
||||
- `card-wit`: dormido — binding WASM pendiente de reactivación.
|
||||
- Espejo del contrato sobre `wawa-akasha` (transporte wawa) aún por cablear.
|
||||
- Endurecer revocación / rotación de membresía de espina.
|
||||
|
||||
## Lugar en el repo
|
||||
|
||||
`shared/card` — contrato de identidad. `card-net` lo lleva a libp2p (khipu);
|
||||
`agora` cubre firma/confianza de más alto nivel.
|
||||
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "card-core"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
publish.workspace = true
|
||||
description = "Brahman — Tarjeta de Presentación canónica (identidad arje + flujos tipados brahman)."
|
||||
|
||||
[dependencies]
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
ulid = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
postcard = { workspace = true }
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "card-net"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
publish.workspace = true
|
||||
description = "Brahman — capa de transporte P2P compartida (libp2p TCP+Noise+Yamux+Kad+Identify+Stream). Cualquier protocolo (handshake brahman, sync minga, futuros) puede registrar su StreamProtocol y abrir/aceptar streams sobre la malla común."
|
||||
|
||||
[dependencies]
|
||||
futures = { workspace = true }
|
||||
libp2p = { workspace = true }
|
||||
libp2p-stream = { workspace = true }
|
||||
libp2p-allow-block-list = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
blake3 = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true }
|
||||
tokio-util = { workspace = true }
|
||||
@@ -0,0 +1,167 @@
|
||||
//! Claves namespaced del DHT compartido — la **primitiva unificadora** de
|
||||
//! Brahman (Capa 1).
|
||||
//!
|
||||
//! El ecosistema brahman corre UN solo Kademlia (en este crate, `card-net`).
|
||||
//! Para que distintos dominios — código indexado (minga), Cards
|
||||
//! (card-discovery), Personas (ágora), servicios — coexistan sin colisión,
|
||||
//! cada clave lleva un byte de `kind` como prefijo. La representación en wire
|
||||
//! es de longitud fija: `[kind_tag] ++ blake3(id)` = 33 bytes.
|
||||
//!
|
||||
//! Vive en `card-net` (y no en un dominio concreto) precisamente porque es el
|
||||
//! namespace COMÚN: minga, agora y card-discovery la comparten. `minga-dht`
|
||||
//! la re-exporta por compatibilidad histórica.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Tipo de registro — el namespace de una clave en el DHT.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum RecordKind {
|
||||
/// Bloque de código indexado (minga).
|
||||
Code,
|
||||
/// `Card` brahman (módulo, ente, etc.).
|
||||
Card,
|
||||
/// `Persona` de ágora (identidad humana federada).
|
||||
Persona,
|
||||
/// Endpoint de servicio.
|
||||
Service,
|
||||
/// Dominio definido por el consumidor.
|
||||
Custom(u8),
|
||||
}
|
||||
|
||||
impl RecordKind {
|
||||
/// Byte de etiqueta. `Custom(n)` ocupa `0x80 | n` (top bit) para no
|
||||
/// chocar nunca con los kinds estándar (`0x00..`).
|
||||
pub fn tag(&self) -> u8 {
|
||||
match self {
|
||||
RecordKind::Code => 0x01,
|
||||
RecordKind::Card => 0x02,
|
||||
RecordKind::Persona => 0x03,
|
||||
RecordKind::Service => 0x04,
|
||||
RecordKind::Custom(n) => 0x80 | (n & 0x7f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Longitud fija de la clave en wire: 1 byte de kind + 32 de hash.
|
||||
pub const DHT_KEY_LEN: usize = 33;
|
||||
|
||||
/// Clave de DHT namespaced. Dos formas de construcción:
|
||||
///
|
||||
/// - Con un `id` legible (`new`, `code`, `card`, `persona`) — el wire
|
||||
/// hashea el id con blake3. Útil cuando el consumidor publica
|
||||
/// identidades simbólicas (nombres de módulos, slugs de personas).
|
||||
/// - Con `for_hash` — el wire usa los 32 bytes del hash directamente.
|
||||
/// Útil cuando el id YA es un blake3 (como en minga, que indexa
|
||||
/// contenido por su α-hash, o ágora, cuyo `IdentityId` ya es
|
||||
/// `blake3(pubkey)`) — evita una segunda pasada de blake3.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct DhtKey {
|
||||
kind: RecordKind,
|
||||
/// Representación canónica de los 32 bytes que forman la "id" en wire.
|
||||
/// Si se construyó por nombre simbólico, son `blake3(id_string)`.
|
||||
/// Si se construyó por hash directo, son el hash tal cual.
|
||||
body: [u8; 32],
|
||||
/// `id` legible — para `Display`. `None` si se construyó por hash.
|
||||
label: Option<String>,
|
||||
}
|
||||
|
||||
impl DhtKey {
|
||||
pub fn new(kind: RecordKind, id: impl Into<String>) -> Self {
|
||||
let id = id.into();
|
||||
let body = *blake3::hash(id.as_bytes()).as_bytes();
|
||||
Self {
|
||||
kind,
|
||||
body,
|
||||
label: Some(id),
|
||||
}
|
||||
}
|
||||
|
||||
/// Clave para un bloque de código (id legible).
|
||||
pub fn code(id: impl Into<String>) -> Self {
|
||||
Self::new(RecordKind::Code, id)
|
||||
}
|
||||
|
||||
/// Clave para una Card.
|
||||
pub fn card(id: impl Into<String>) -> Self {
|
||||
Self::new(RecordKind::Card, id)
|
||||
}
|
||||
|
||||
/// Clave para una Persona.
|
||||
pub fn persona(id: impl Into<String>) -> Self {
|
||||
Self::new(RecordKind::Persona, id)
|
||||
}
|
||||
|
||||
/// Clave a partir de un hash ya computado (32 bytes). El wire usa
|
||||
/// esos bytes directamente, **sin re-hashear**.
|
||||
pub fn for_hash(kind: RecordKind, hash: [u8; 32]) -> Self {
|
||||
Self {
|
||||
kind,
|
||||
body: hash,
|
||||
label: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn kind(&self) -> RecordKind {
|
||||
self.kind
|
||||
}
|
||||
|
||||
pub fn id(&self) -> Option<&str> {
|
||||
self.label.as_deref()
|
||||
}
|
||||
|
||||
/// Representación en wire: `[kind_tag] ++ body`, 33 bytes.
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
let mut out = Vec::with_capacity(DHT_KEY_LEN);
|
||||
out.push(self.kind.tag());
|
||||
out.extend_from_slice(&self.body);
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn wire_key_has_fixed_length() {
|
||||
assert_eq!(DhtKey::card("modulo-x").to_bytes().len(), DHT_KEY_LEN);
|
||||
assert_eq!(DhtKey::code("fn-hash").to_bytes().len(), DHT_KEY_LEN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_id_different_kind_does_not_collide() {
|
||||
let a = DhtKey::card("foo").to_bytes();
|
||||
let b = DhtKey::code("foo").to_bytes();
|
||||
let c = DhtKey::persona("foo").to_bytes();
|
||||
assert_ne!(a, b);
|
||||
assert_ne!(b, c);
|
||||
assert_ne!(a, c);
|
||||
// El hash del id es el mismo; sólo difiere el byte de kind.
|
||||
assert_eq!(a[1..], b[1..]);
|
||||
assert_ne!(a[0], b[0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_kind_and_id_is_stable() {
|
||||
assert_eq!(DhtKey::card("x").to_bytes(), DhtKey::card("x").to_bytes());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_hash_no_rehashea() {
|
||||
// for_hash debe usar los bytes tal cual: body = hash, no blake3(hash).
|
||||
let h = [7u8; 32];
|
||||
let k = DhtKey::for_hash(RecordKind::Persona, h);
|
||||
let wire = k.to_bytes();
|
||||
assert_eq!(wire[0], RecordKind::Persona.tag());
|
||||
assert_eq!(&wire[1..], &h);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_kind_never_collides_with_standard() {
|
||||
for std in [RecordKind::Code, RecordKind::Card, RecordKind::Persona, RecordKind::Service] {
|
||||
for n in 0..=127u8 {
|
||||
assert_ne!(std.tag(), RecordKind::Custom(n).tag());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,518 @@
|
||||
//! `brahman-net` — capa P2P compartida de la red Brahman.
|
||||
//!
|
||||
//! Provee un nodo libp2p genérico que cualquier protocolo de la
|
||||
//! familia (handshake brahman remoto, sync minga, futuros) puede
|
||||
//! reusar. La idea: una sola malla, múltiples sub-protocolos
|
||||
//! multiplexados por `StreamProtocol`.
|
||||
//!
|
||||
//! ## Stack
|
||||
//!
|
||||
//! - **TCP + Noise + Yamux**: transporte autenticado y multiplexado.
|
||||
//! - **`stream::Behaviour`**: streams bidireccionales por
|
||||
//! `StreamProtocol`. Cada protocolo (`/brahman/handshake/1.0.0`,
|
||||
//! `/minga/sync/1.0.0`, …) se registra independientemente vía el
|
||||
//! `stream::Control` que `BrahmanNet` expone.
|
||||
//! - **`kad::Behaviour<MemoryStore>`**: Kademlia DHT en modo Server
|
||||
//! para discovery (peers cercanos + content providers).
|
||||
//! - **`identify::Behaviour`**: cada peer anuncia sus listen-addrs
|
||||
//! reales; las inyectamos automáticamente al routing table de Kad.
|
||||
//!
|
||||
//! ## Modelo
|
||||
//!
|
||||
//! El swarm corre en una task tokio dedicada. La interfaz pública son:
|
||||
//! 1. **Comandos** (canal mpsc): `dial`, `listen`, `add_dht_peer`,
|
||||
//! `find_closest_peers`, `start_providing`, `find_providers`.
|
||||
//! 2. **`stream::Control`** (acceso directo): para abrir/aceptar
|
||||
//! streams de un protocolo concreto. Cada protocolo se ocupa de
|
||||
//! su propia lógica sobre el stream resultante.
|
||||
//!
|
||||
//! La separación entre comandos y control permite que la lógica de
|
||||
//! red (DHT, dial, listen) y la lógica de protocolos (handshake/sync)
|
||||
//! evolucionen independientes — el protocolo no necesita conocer al
|
||||
//! swarm, sólo pide streams.
|
||||
//!
|
||||
//! ## Identidad
|
||||
//!
|
||||
//! Por defecto se genera una keypair Ed25519 efímera. Para identidad
|
||||
//! persistente (la misma `peer_id` across reboots), pasar la keypair
|
||||
//! con [`BrahmanNet::with_keypair`]. Esa misma keypair puede ser la
|
||||
//! base para firmas de Cards (cuando se implemente trust remoto).
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![warn(rust_2018_idioms)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use futures::StreamExt;
|
||||
use libp2p::{
|
||||
autonat, dcutr, identify, identity, kad, mdns, noise, relay,
|
||||
swarm::{behaviour::toggle::Toggle, NetworkBehaviour, SwarmEvent},
|
||||
tcp, yamux, Swarm, SwarmBuilder,
|
||||
};
|
||||
use libp2p_allow_block_list::{self as allow_block_list, BlockedPeers};
|
||||
use libp2p_stream as stream;
|
||||
use tokio::sync::{mpsc, oneshot, Mutex};
|
||||
|
||||
pub mod key;
|
||||
pub use key::{DhtKey, RecordKind, DHT_KEY_LEN};
|
||||
|
||||
pub use libp2p::{
|
||||
identity::{Keypair, PublicKey},
|
||||
multiaddr::Protocol,
|
||||
Multiaddr, PeerId, Stream, StreamProtocol,
|
||||
};
|
||||
pub use libp2p_stream::OpenStreamError;
|
||||
|
||||
const IDENTIFY_PROTOCOL: &str = "/brahman-net/0.1.0";
|
||||
const IDLE_CONNECTION_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
|
||||
#[derive(NetworkBehaviour)]
|
||||
struct BrahmanBehaviour {
|
||||
/// Block-list a nivel de swarm: peers en este behaviour son
|
||||
/// rechazados ANTES del handshake Noise. Más eficiente que
|
||||
/// rechazar al nivel del handshake brahman (ahorra round-trip
|
||||
/// TCP+Noise por intento denegado). Sincronizado con la
|
||||
/// `PeerPolicy.deny` vía `block_peer`/`unblock_peer` exposed
|
||||
/// en `BrahmanNet`.
|
||||
block_list: allow_block_list::Behaviour<BlockedPeers>,
|
||||
stream: stream::Behaviour,
|
||||
kad: kad::Behaviour<kad::store::MemoryStore>,
|
||||
identify: identify::Behaviour,
|
||||
/// Relay server (Circuit Relay v2): un nodo alcanzable presta su
|
||||
/// conexión para que dos pares detrás de NAT se contacten. Pasivo —
|
||||
/// sólo actúa ante reservas/solicitudes de circuito.
|
||||
relay: relay::Behaviour,
|
||||
/// Relay client: permite reservar un circuito en un relay y ser
|
||||
/// alcanzable vía `…/p2p/<relay>/p2p-circuit/p2p/<self>`.
|
||||
relay_client: relay::client::Behaviour,
|
||||
/// DCUtR: tras conectar por relay, intenta promover a conexión
|
||||
/// directa (hole-punching) coordinando por el circuito.
|
||||
dcutr: dcutr::Behaviour,
|
||||
/// AutoNAT: confirma qué direcciones externas son realmente alcanzables
|
||||
/// pidiéndole a otros peers que nos disquen de vuelta. Sustituye el
|
||||
/// "confiar a ciegas en el observed_addr de identify" por direcciones
|
||||
/// verificadas — sólo esas se anuncian (y entran en reservas de relay).
|
||||
autonat: autonat::Behaviour,
|
||||
/// mDNS: descubrimiento de pares en la MISMA LAN sin bootstrap ni IP
|
||||
/// conocida (multicast 224.0.0.251). Los pares descubiertos se inyectan
|
||||
/// a la routing table de Kad (igual que las listen-addrs de identify), así
|
||||
/// el DHT y los protocolos de stream funcionan zero-config en LAN.
|
||||
/// `Toggle`: si el socket multicast no se puede abrir (sandbox, red sin
|
||||
/// multicast), queda deshabilitado y el nodo sigue andando igual.
|
||||
mdns: Toggle<mdns::tokio::Behaviour>,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum NodeError {
|
||||
#[error("transport build failed: {0}")]
|
||||
Build(String),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Command {
|
||||
Dial(Multiaddr),
|
||||
Listen(Multiaddr),
|
||||
AddDhtPeer(PeerId, Multiaddr),
|
||||
FindClosestPeers(PeerId, oneshot::Sender<Vec<DiscoveredPeer>>),
|
||||
StartProviding(Vec<u8>),
|
||||
StopProviding(Vec<u8>),
|
||||
GetProviders(Vec<u8>, oneshot::Sender<Vec<PeerId>>),
|
||||
BlockPeer(PeerId),
|
||||
UnblockPeer(PeerId),
|
||||
}
|
||||
|
||||
/// Peer descubierto vía DHT: identidad + direcciones conocidas.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DiscoveredPeer {
|
||||
pub peer_id: PeerId,
|
||||
pub addrs: Vec<Multiaddr>,
|
||||
}
|
||||
|
||||
/// Nodo Brahman en la malla P2P. Maneja el swarm libp2p y expone
|
||||
/// API uniforme para listen/dial/DHT/streams.
|
||||
pub struct BrahmanNet {
|
||||
/// Identidad libp2p de este nodo. Estable mientras viva la
|
||||
/// keypair (efímera por default; persistente si pasaste una
|
||||
/// vía [`with_keypair`]).
|
||||
pub peer_id: PeerId,
|
||||
/// Keypair compartida (Arc para compartir con consumers que
|
||||
/// necesitan firmar mensajes con la misma identidad — p. ej.
|
||||
/// `card_handshake::network::connect_libp2p` que firma el
|
||||
/// Hello). NO se expone públicamente; usar [`Self::keypair`].
|
||||
keypair: Arc<Keypair>,
|
||||
cmd_tx: mpsc::UnboundedSender<Command>,
|
||||
listen_rx: Mutex<mpsc::UnboundedReceiver<Multiaddr>>,
|
||||
/// Control para abrir y aceptar streams. Cada protocolo
|
||||
/// (handshake brahman, sync minga, etc.) llama
|
||||
/// `control.accept(StreamProtocol::new("/foo/1.0.0"))` para
|
||||
/// recibir streams entrantes, o `control.open_stream(peer, proto)`
|
||||
/// para abrirlos. Multiplexado y demultiplexado lo hace libp2p.
|
||||
pub control: stream::Control,
|
||||
}
|
||||
|
||||
impl BrahmanNet {
|
||||
/// Crea un nodo con keypair Ed25519 generada al vuelo (peer_id
|
||||
/// efímero — cambia en cada arranque).
|
||||
pub fn new() -> Result<Self, NodeError> {
|
||||
Self::with_keypair(identity::Keypair::generate_ed25519())
|
||||
}
|
||||
|
||||
/// Crea un nodo con una keypair libp2p específica. Usá esto para
|
||||
/// `peer_id` estable (por ejemplo si tu identidad se persiste a
|
||||
/// disco, o si la derivás de la identidad criptográfica del
|
||||
/// módulo).
|
||||
///
|
||||
/// Sólo Ed25519 se soporta — la `keypair` se duplica internamente
|
||||
/// vía clone del `ed25519::Keypair` para que tanto el swarm
|
||||
/// (Noise auth) como el caller (firma de Cards) compartan la
|
||||
/// misma identidad sin la fricción de que `identity::Keypair` no
|
||||
/// implemente `Clone`.
|
||||
pub fn with_keypair(keypair: identity::Keypair) -> Result<Self, NodeError> {
|
||||
let ed_kp = keypair
|
||||
.try_into_ed25519()
|
||||
.map_err(|_| NodeError::Build("brahman-net sólo soporta keypairs Ed25519".into()))?;
|
||||
let kp_for_swarm = identity::Keypair::from(ed_kp.clone());
|
||||
let kp_for_storage = Arc::new(identity::Keypair::from(ed_kp));
|
||||
let peer_id = kp_for_swarm.public().to_peer_id();
|
||||
|
||||
let mut swarm: Swarm<BrahmanBehaviour> = SwarmBuilder::with_existing_identity(kp_for_swarm)
|
||||
.with_tokio()
|
||||
.with_tcp(
|
||||
tcp::Config::default(),
|
||||
noise::Config::new,
|
||||
yamux::Config::default,
|
||||
)
|
||||
.map_err(|e| NodeError::Build(format!("{e}")))?
|
||||
// Inyecta el transporte de relay-client: habilita marcar y
|
||||
// escuchar direcciones `…/p2p-circuit`. Provee el
|
||||
// `relay_client` behaviour al closure de abajo.
|
||||
.with_relay_client(noise::Config::new, yamux::Config::default)
|
||||
.map_err(|e| NodeError::Build(format!("{e}")))?
|
||||
.with_behaviour(|key, relay_client| {
|
||||
let local = key.public().to_peer_id();
|
||||
let mut kad =
|
||||
kad::Behaviour::new(local, kad::store::MemoryStore::new(local));
|
||||
// Modo Server: respondemos a queries del DHT. Auto
|
||||
// requiere detectar reachability; para entornos
|
||||
// controlados (localhost, redes privadas) Server es
|
||||
// lo correcto.
|
||||
kad.set_mode(Some(kad::Mode::Server));
|
||||
let identify = identify::Behaviour::new(
|
||||
identify::Config::new(IDENTIFY_PROTOCOL.to_string(), key.public())
|
||||
.with_agent_version(format!("brahman-net/{}", env!("CARGO_PKG_VERSION"))),
|
||||
);
|
||||
// mDNS resiliente: si el socket multicast no abre, seguimos sin
|
||||
// descubrimiento LAN en vez de fallar el nodo entero.
|
||||
let depurar = std::env::var("BRAHMAN_DEBUG").is_ok();
|
||||
let mdns = match mdns::tokio::Behaviour::new(mdns::Config::default(), local) {
|
||||
Ok(b) => {
|
||||
if depurar {
|
||||
eprintln!("brahman: mDNS ACTIVO (descubrimiento LAN)");
|
||||
}
|
||||
Toggle::from(Some(b))
|
||||
}
|
||||
Err(e) => {
|
||||
if depurar {
|
||||
eprintln!("brahman: mDNS DESACTIVADO ({e})");
|
||||
}
|
||||
Toggle::from(None)
|
||||
}
|
||||
};
|
||||
BrahmanBehaviour {
|
||||
block_list: allow_block_list::Behaviour::default(),
|
||||
stream: stream::Behaviour::new(),
|
||||
kad,
|
||||
identify,
|
||||
relay: relay::Behaviour::new(local, Default::default()),
|
||||
relay_client,
|
||||
dcutr: dcutr::Behaviour::new(local),
|
||||
mdns,
|
||||
autonat: autonat::Behaviour::new(
|
||||
local,
|
||||
autonat::Config {
|
||||
// Confirmamos también IPs privadas/loopback: la
|
||||
// malla Brahman vive tanto en LAN como en WAN, no
|
||||
// sólo en IPs globales (default `true` las
|
||||
// ignoraría y nada se confirmaría en LAN).
|
||||
only_global_ips: false,
|
||||
// Sondeo pronto tras arrancar (default ~15 s es
|
||||
// demasiado para el flujo de reservas de relay).
|
||||
boot_delay: Duration::from_secs(2),
|
||||
retry_interval: Duration::from_secs(5),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
}
|
||||
})
|
||||
.map_err(|e| NodeError::Build(format!("{e}")))?
|
||||
.with_swarm_config(|c| c.with_idle_connection_timeout(IDLE_CONNECTION_TIMEOUT))
|
||||
.build();
|
||||
|
||||
let control = swarm.behaviour().stream.new_control();
|
||||
|
||||
let (cmd_tx, mut cmd_rx) = mpsc::unbounded_channel::<Command>();
|
||||
let (listen_tx, listen_rx) = mpsc::unbounded_channel::<Multiaddr>();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut pending_finds: HashMap<
|
||||
kad::QueryId,
|
||||
oneshot::Sender<Vec<DiscoveredPeer>>,
|
||||
> = HashMap::new();
|
||||
let mut pending_providers: HashMap<
|
||||
kad::QueryId,
|
||||
(Vec<PeerId>, oneshot::Sender<Vec<PeerId>>),
|
||||
> = HashMap::new();
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
Some(cmd) = cmd_rx.recv() => {
|
||||
match cmd {
|
||||
Command::Dial(addr) => {
|
||||
let _ = swarm.dial(addr);
|
||||
}
|
||||
Command::Listen(addr) => {
|
||||
let _ = swarm.listen_on(addr);
|
||||
}
|
||||
Command::AddDhtPeer(peer, addr) => {
|
||||
swarm.behaviour_mut().kad.add_address(&peer, addr);
|
||||
}
|
||||
Command::FindClosestPeers(target, tx) => {
|
||||
let qid = swarm.behaviour_mut().kad.get_closest_peers(target);
|
||||
pending_finds.insert(qid, tx);
|
||||
}
|
||||
Command::StartProviding(key) => {
|
||||
// Best-effort: si falla (sin peers cercanos para
|
||||
// replicar), seguirá viviendo en el local store
|
||||
// y se servirá vía get_providers de quien tenga
|
||||
// conexión con nosotros.
|
||||
let _ = swarm.behaviour_mut().kad.start_providing(key.into());
|
||||
}
|
||||
Command::StopProviding(key) => {
|
||||
// Quitamos el record local del provider store.
|
||||
// Los peers cercanos eventualmente expiran su
|
||||
// copia replicada por TTL natural (~24h en
|
||||
// libp2p kad default); para retiro inmediato
|
||||
// habría que enviar un republish con sentinel,
|
||||
// pero kad no expone esa primitiva. Aceptable
|
||||
// para el caso "el provider local desapareció":
|
||||
// queries que pasen por nosotros dejan de
|
||||
// listarnos al instante.
|
||||
swarm.behaviour_mut().kad.stop_providing(&key.into());
|
||||
}
|
||||
Command::GetProviders(key, tx) => {
|
||||
let qid = swarm.behaviour_mut().kad.get_providers(key.into());
|
||||
pending_providers.insert(qid, (Vec::new(), tx));
|
||||
}
|
||||
Command::BlockPeer(peer) => {
|
||||
swarm.behaviour_mut().block_list.block_peer(peer);
|
||||
}
|
||||
Command::UnblockPeer(peer) => {
|
||||
swarm.behaviour_mut().block_list.unblock_peer(peer);
|
||||
}
|
||||
}
|
||||
}
|
||||
event = swarm.select_next_some() => {
|
||||
match event {
|
||||
SwarmEvent::NewListenAddr { address, .. } => {
|
||||
let _ = listen_tx.send(address);
|
||||
}
|
||||
// Identify nos dice las listen-addrs reales del
|
||||
// peer. Las inyectamos a Kad para poblar el
|
||||
// routing table sin necesidad de add_dht_peer
|
||||
// manual — la propagación pasa a ser automática.
|
||||
SwarmEvent::Behaviour(BrahmanBehaviourEvent::Identify(
|
||||
identify::Event::Received { peer_id, info, .. }
|
||||
)) => {
|
||||
// El observed_addr que nos reporta identify se
|
||||
// emite como CANDIDATO a externa; AutoNAT lo
|
||||
// prueba pidiendo dial-backs y, si es
|
||||
// alcanzable, dispara StatusChanged(Public) —
|
||||
// ahí recién lo confirmamos (abajo). Las
|
||||
// listen-addrs del peer pueblan la routing
|
||||
// table de Kad.
|
||||
for addr in info.listen_addrs {
|
||||
swarm.behaviour_mut().kad.add_address(&peer_id, addr);
|
||||
}
|
||||
}
|
||||
// mDNS descubrió pares en la LAN: inyectamos sus
|
||||
// direcciones a Kad (igual que identify). Con eso el
|
||||
// DHT y los streams funcionan sin bootstrap manual.
|
||||
SwarmEvent::Behaviour(BrahmanBehaviourEvent::Mdns(
|
||||
mdns::Event::Discovered(list)
|
||||
)) => {
|
||||
for (peer_id, addr) in list {
|
||||
if std::env::var("BRAHMAN_DEBUG").is_ok() {
|
||||
eprintln!("brahman: mDNS descubrió {peer_id} en {addr}");
|
||||
}
|
||||
swarm.behaviour_mut().kad.add_address(&peer_id, addr);
|
||||
}
|
||||
}
|
||||
// AutoNAT confirmó (vía dial-back de otros peers)
|
||||
// que somos alcanzables en `addr`: recién entonces
|
||||
// la anunciamos como externa — la usa el relay
|
||||
// server en las reservas y la ven los pares.
|
||||
SwarmEvent::Behaviour(BrahmanBehaviourEvent::Autonat(
|
||||
autonat::Event::StatusChanged { new: autonat::NatStatus::Public(addr), .. }
|
||||
)) => {
|
||||
swarm.add_external_address(addr);
|
||||
}
|
||||
SwarmEvent::Behaviour(BrahmanBehaviourEvent::Kad(
|
||||
kad::Event::OutboundQueryProgressed { id, result, step, .. }
|
||||
)) => {
|
||||
match result {
|
||||
kad::QueryResult::GetClosestPeers(Ok(ok)) if step.last => {
|
||||
if let Some(tx) = pending_finds.remove(&id) {
|
||||
let infos = ok.peers.into_iter()
|
||||
.map(|p| DiscoveredPeer {
|
||||
peer_id: p.peer_id,
|
||||
addrs: p.addrs,
|
||||
})
|
||||
.collect();
|
||||
let _ = tx.send(infos);
|
||||
}
|
||||
}
|
||||
kad::QueryResult::GetClosestPeers(Err(_)) if step.last => {
|
||||
if let Some(tx) = pending_finds.remove(&id) {
|
||||
let _ = tx.send(Vec::new());
|
||||
}
|
||||
}
|
||||
kad::QueryResult::GetProviders(Ok(ok)) => {
|
||||
if let Some((collected, _)) =
|
||||
pending_providers.get_mut(&id)
|
||||
{
|
||||
if let kad::GetProvidersOk::FoundProviders {
|
||||
providers, ..
|
||||
} = ok
|
||||
{
|
||||
for p in providers {
|
||||
if !collected.contains(&p) {
|
||||
collected.push(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if step.last {
|
||||
if let Some((providers, tx)) =
|
||||
pending_providers.remove(&id)
|
||||
{
|
||||
let _ = tx.send(providers);
|
||||
}
|
||||
}
|
||||
}
|
||||
kad::QueryResult::GetProviders(Err(_)) if step.last => {
|
||||
if let Some((providers, tx)) =
|
||||
pending_providers.remove(&id)
|
||||
{
|
||||
let _ = tx.send(providers);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
peer_id,
|
||||
keypair: kp_for_storage,
|
||||
cmd_tx,
|
||||
listen_rx: Mutex::new(listen_rx),
|
||||
control,
|
||||
})
|
||||
}
|
||||
|
||||
/// Acceso a la keypair de identidad del nodo. Usar para firmar
|
||||
/// payloads que viajan asociados al `peer_id` (handshake brahman
|
||||
/// firmado, futuros sub-protocolos con autenticación). El `Arc`
|
||||
/// permite compartir sin copia — la keypair libp2p no es `Clone`.
|
||||
pub fn keypair(&self) -> Arc<Keypair> {
|
||||
self.keypair.clone()
|
||||
}
|
||||
|
||||
/// Bloquea conexiones desde/hacia `peer` a nivel del swarm.
|
||||
/// Conexiones existentes se cierran y nuevos intentos son
|
||||
/// rechazados ANTES del Noise handshake — más eficiente que
|
||||
/// rechazar al nivel del handshake brahman (ahorra round-trip
|
||||
/// TCP+Noise por intento). Idempotente.
|
||||
pub fn block_peer(&self, peer: PeerId) {
|
||||
let _ = self.cmd_tx.send(Command::BlockPeer(peer));
|
||||
}
|
||||
|
||||
/// Quita a `peer` de la block-list del swarm. Conexiones futuras
|
||||
/// son aceptadas con normalidad. Idempotente.
|
||||
pub fn unblock_peer(&self, peer: PeerId) {
|
||||
let _ = self.cmd_tx.send(Command::UnblockPeer(peer));
|
||||
}
|
||||
|
||||
/// Empieza a escuchar en `addr`. Bloquea hasta que el listener
|
||||
/// publique su dirección real (Multiaddr resuelta — útil cuando
|
||||
/// pediste `/ip4/0.0.0.0/tcp/0` y querés saber qué puerto te tocó).
|
||||
pub async fn listen(&self, addr: Multiaddr) -> Multiaddr {
|
||||
self.cmd_tx
|
||||
.send(Command::Listen(addr))
|
||||
.expect("swarm task alive");
|
||||
let mut rx = self.listen_rx.lock().await;
|
||||
rx.recv().await.expect("listen address arrives")
|
||||
}
|
||||
|
||||
/// Inicia conexión con un peer en `addr`. No-op si ya hay
|
||||
/// conexión. Best-effort — fallos se loggean al swarm pero no se
|
||||
/// propagan al caller (consistente con libp2p).
|
||||
pub fn dial(&self, addr: Multiaddr) {
|
||||
let _ = self.cmd_tx.send(Command::Dial(addr));
|
||||
}
|
||||
|
||||
/// Añade un peer al routing table de Kademlia. Punto de entrada
|
||||
/// para bootstrap: tras esto, el nodo puede dirigir queries DHT
|
||||
/// a través de este peer.
|
||||
pub fn add_dht_peer(&self, peer: PeerId, addr: Multiaddr) {
|
||||
let _ = self.cmd_tx.send(Command::AddDhtPeer(peer, addr));
|
||||
}
|
||||
|
||||
/// Consulta el DHT por los peers más cercanos al `target` PeerId.
|
||||
/// Devuelve la lista resuelta (vacía si la query falla o si no
|
||||
/// hay peers conocidos). Bloquea hasta que la query completa.
|
||||
pub async fn find_closest_peers(&self, target: PeerId) -> Vec<DiscoveredPeer> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let _ = self
|
||||
.cmd_tx
|
||||
.send(Command::FindClosestPeers(target, tx));
|
||||
rx.await.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Anuncia en el DHT que este peer tiene el contenido identificado
|
||||
/// por `key`. Otros peers pueden luego descubrirlo vía
|
||||
/// [`find_providers`](Self::find_providers). Best-effort: si la
|
||||
/// replicación falla inicialmente, el record vive en el store
|
||||
/// local hasta que llegue conexión.
|
||||
pub fn start_providing(&self, key: &[u8]) {
|
||||
let _ = self.cmd_tx.send(Command::StartProviding(key.to_vec()));
|
||||
}
|
||||
|
||||
/// Retira el anuncio previo de [`start_providing`] para `key`.
|
||||
/// El record local se borra al instante (queries que lleguen a
|
||||
/// nosotros dejan de listarnos). Los records replicados en peers
|
||||
/// remotos viven hasta su TTL — kad no expone primitiva para
|
||||
/// retracción inmediata cross-peer. Aceptable: simétrico al
|
||||
/// caso "el provider apareció" (también propagación eventual).
|
||||
pub fn stop_providing(&self, key: &[u8]) {
|
||||
let _ = self.cmd_tx.send(Command::StopProviding(key.to_vec()));
|
||||
}
|
||||
|
||||
/// Consulta el DHT por peers que han anunciado proveer `key`.
|
||||
/// Devuelve la lista de `PeerId`s que se reportan como providers.
|
||||
/// Lista vacía si nadie anuncia.
|
||||
pub async fn find_providers(&self, key: &[u8]) -> Vec<PeerId> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let _ = self
|
||||
.cmd_tx
|
||||
.send(Command::GetProviders(key.to_vec(), tx));
|
||||
rx.await.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "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]
|
||||
card-core = { path = "../card-core" }
|
||||
wit-parser = "0.230"
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
|
||||
[[example]]
|
||||
name = "brahman-wit-info"
|
||||
path = "examples/brahman-wit-info.rs"
|
||||
@@ -0,0 +1,34 @@
|
||||
# brahman-card-wit
|
||||
|
||||
> **DORMIDO (2026-05-30).** Capa 3 de Brahman. Ver `/BRAHMAN.md`.
|
||||
|
||||
Parser opcional de contratos WIT (`.wit` texto → `Vec<card_core::WitInterface>`, uno por `world`),
|
||||
sobre `wit-parser` (sin `wasm-tools`/`wit-component`).
|
||||
|
||||
## Estado: relegado, no borrado
|
||||
|
||||
La visión original de Brahman incluía **módulos agnósticos descritos por interfaz WIT** (eventualmente WASM).
|
||||
Esa capa **nunca se ejecutó**:
|
||||
|
||||
- No existe ningún archivo `.wit` en el workspace.
|
||||
- Ningún crate de producción depende de este crate — sólo `examples/brahman-wit-info.rs` y la
|
||||
**dev-dependency** de `card-sidecar`.
|
||||
|
||||
Se conserva (funciona, 210 LOC, reversible) por si algún día aparecen `.wit` reales. **No asumir que está
|
||||
en ninguna ruta de build.**
|
||||
|
||||
## El contrato agnóstico real y vigente
|
||||
|
||||
```
|
||||
shared/card (formato Card) + card-handshake (handshake nativo Rust) + DhtKey (namespacing en la DHT)
|
||||
```
|
||||
|
||||
El tipo de metadata `WitInterface` vive en **`card-core`** (no aquí) y **sí** lo usa el broker
|
||||
(`chasqui-broker`) para matching estructural — es metadata opcional viva. Lo único dormido es *este parser*
|
||||
de archivos `.wit` inexistentes.
|
||||
|
||||
## Si se decide revivir WIT
|
||||
|
||||
Requeriría: (1) que el Init lea `<modulo>/wit/protocol.wit` en el descubrimiento y construya
|
||||
`ResolvedCard::from_conscious(card, wit)`; (2) que algún módulo de producción publique un `.wit`.
|
||||
Hasta entonces, este crate es tooling latente (`cargo run -p card-wit --example brahman-wit-info -- <archivo.wit>`).
|
||||
@@ -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 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
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
//! `brahman-card-wit` — extractor de contratos WIT.
|
||||
//!
|
||||
//! **DORMIDO (2026-05-30).** No existe ningún archivo `.wit` en el workspace
|
||||
//! y ningún crate de producción depende de éste (sólo `examples/` y la
|
||||
//! dev-dependency de `card-sidecar`). Es la "Capa 3" de Brahman (ver `/BRAHMAN.md`):
|
||||
//! la idea de módulos agnósticos descritos por interfaz WIT nunca se ejecutó.
|
||||
//! El **contrato agnóstico real y vigente** es `shared/card` (formato `Card`) +
|
||||
//! el handshake nativo Rust de `card-handshake` + el namespacing de `DhtKey`.
|
||||
//! Se conserva como herramienta opcional por si algún día aparecen `.wit`
|
||||
//! reales; no asumir que está en ninguna ruta de build.
|
||||
//!
|
||||
//! Nota: el tipo de metadata [`WitInterface`] vive en `card-core` y SÍ lo usa
|
||||
//! el broker para matching estructural — eso es metadata opcional viva. Lo
|
||||
//! dormido es este *parser* de archivos `.wit` inexistentes.
|
||||
//!
|
||||
//! 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 [`card_core::Card`]
|
||||
//! cuando se construye una [`card_core::ResolvedCard`].
|
||||
//!
|
||||
//! Casos de uso (hipotéticos hasta que existan `.wit`):
|
||||
//!
|
||||
//! - 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 card_core::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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user