feat(card): WireCard + extensions — forward-compat sin romper postcard

Restaura el campo extensions de Card que había caído al adoptar postcard
(serde_json::Value usa secuencias/maps de longitud dinámica). La
solución es separar dos formas:

- Card (la rica): para JSON/TOML. Tiene extensions: BTreeMap<String,
  serde_json::Value> con #[serde(flatten, skip_serializing_if = is_empty)].
  Los campos desconocidos del archivo sobreviven el roundtrip.
- WireCard (la slim): para postcard. Mismo schema sin extensions y con
  genesis: Vec<WireCard> recursivo. Postcard-friendly por construcción.

Conversiones From<Card> for WireCard (descarta extensions) y
From<WireCard> for Card (extensiones quedan vacías post-wire). El
contrato es explícito: extensions son anotaciones locales que sobreviven
file I/O pero NO cruzan al Init.

brahman-handshake::Hello.card cambia de Card a WireCard. Client hace
card.into() al enviar; Server hace hello.card.into() para volver a
Card antes de validar/registrar.

Tests:
- 3 nuevos en brahman-card: extensions_preserved_in_json_roundtrip,
  wire_card_roundtrip_strips_extensions, wire_card_postcard_friendly
  (verifica que postcard::to_allocvec(&wire) NO falla — caso que
  rompía con Card.extensions populadas).
- 1 ajuste en handshake/tests/handshake.rs (struct-literal de Hello
  ahora con card: sample_card(...).into()).
- brahman-card: postcard como dev-dep.

Tests acumulados: 35 (card 11, broker 11, handshake codec+transport 2 +
integ 7, card-wit 4, admin 0). 0 errores, 0 warnings (vienen del
commit anterior 9420eae).

CHANGELOG.md actualizado con esta entrada y con el commit 9420eae
("probando" del usuario, limpieza de 17 warnings dead-code).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sergio
2026-05-08 17:33:15 +00:00
parent 9420eae0b6
commit f19ca723b6
8 changed files with 238 additions and 12 deletions
+1 -1
View File
@@ -64,7 +64,7 @@ impl Client {
let hello = Hello {
schema_version: CARD_SCHEMA_VERSION,
protocol_version: brahman_card::PROTOCOL_VERSION.to_string(),
card,
card: card.into(), // Card → WireCard: descarta extensions
wit,
};
write_frame(&mut stream, &Frame::Hello(hello)).await?;
@@ -3,25 +3,26 @@
//! Todos los mensajes que cruzan el wire son variantes de [`Frame`].
use brahman_broker::MatchStrategy;
use brahman_card::{TypeRef, WitInterface};
use brahman_card::{TypeRef, WireCard, WitInterface};
use serde::{Deserialize, Serialize};
use ulid::Ulid;
/// Identificador de sesión emitido por el servidor en `HelloAck`.
pub type SessionId = Ulid;
/// Saludo inicial del módulo. Lleva la Card completa para que el servidor
/// la valide e indexe. Opcionalmente, una `WitInterface` ya extraída — si
/// está presente, el módulo es "consciente" y el server lo registra como
/// `ResolvedCard::from_conscious`; si no, como `from_agnostic`.
/// Saludo inicial del módulo. Lleva la Card en forma `WireCard`
/// (postcard-friendly: sin extensiones JSON arbitrarias). El servidor
/// la convierte a `Card` para uso interno. Opcionalmente, una
/// `WitInterface` ya extraída — si está presente, el módulo es
/// "consciente" y el server lo registra como `ResolvedCard::from_conscious`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Hello {
/// Versión del schema de Card que el cliente sigue.
pub schema_version: u16,
/// Versión del protocolo handshake del cliente.
pub protocol_version: String,
/// Tarjeta de Presentación.
pub card: brahman_card::Card,
/// Tarjeta de Presentación, proyectada al wire.
pub card: WireCard,
/// Interfaz WIT extraída por el cliente (típicamente con
/// `brahman-card-wit`). `None` si el módulo es agnóstico.
#[serde(default)]
+7 -2
View File
@@ -367,7 +367,9 @@ impl Session {
}
let session_id = Ulid::new();
self.register_session(session_id, hello.card, hello.wit).await;
// WireCard → Card: extensiones quedan vacías post-wire (es el contrato).
let card: Card = hello.card.into();
self.register_session(session_id, card, hello.wit).await;
let ack = HelloAck {
server_version: crate::HANDSHAKE_VERSION.to_string(),
@@ -416,7 +418,10 @@ impl Session {
brahman_card::PROTOCOL_VERSION
)));
}
if let Err(e) = hello.card.validate() {
// Validamos contra Card (la rica) — convertir es barato y centraliza
// la lógica de validación en un solo lugar.
let as_card: Card = Card::from(hello.card.clone());
if let Err(e) = as_card.validate() {
return Some(HandshakeError::InvalidCard(e.to_string()));
}
None