tawasuyu

tawasuyu/brahman

Rust 307TOML 110JSON 14Markdown 113965 Definitions

⎇ 09501911b7ec497215773d2cb7192a2e6ce17e02 crates/core/brahman-cards/src/lib.rs Rust · 244 lines · 9.3 KB History Who touched what raw

feat(brahman-cards): brazo unificado V1 — readers JSON + estructura canónica 09501911b · Sergio ·

1//! `brahman-cards` — brazo unificado de Cards.
2//!
3//! Brahman maneja varios formatos legítimos de "Card" (la unidad
4//! declarativa que describe identidad, datos, módulos, widgets, ...).
5//! Cada formato vive en su propio crate de origen y conserva su shape
6//! público; lo que este crate aporta es **un único punto de entrada**
7//! que sabe interpretar cada uno de ellos y proyectarlos a una sola
8//! estructura interna canónica [`Card`].
9//!
10//! Diseño:
11//!
12//! ```text
13//! ┌─────────────┐  ┌──────────────┐  ┌─────────────┐
14//! │ Ente JSON   │  │ Monad JSON   │  │ UiModule    │ … futuro
15//! │ (brahman-   │  │ (nouser-     │  │ (nakui-ui-  │
16//! │  card)      │  │  card)       │  │  schema)    │
17//! └─────┬───────┘  └──────┬───────┘  └──────┬──────┘
18//!       │                 │                 │
19//!       └────────┬────────┴────────┬────────┘
20//!                │  brahman-cards  │
21//!                │   (este crate)  │
22//!                └────────┬────────┘
23//!                         │
24//!                  ┌──────▼──────┐
25//!                  │   `Card`    │ ← único tipo canónico
26//!                  │   wrapper   │   que consumen UI runtime,
27//!                  │   común +   │   storage, DHT, wire.
28//!                  │   variant   │
29//!                  │   body      │
30//!                  └─────────────┘
31//! ```
32//!
33//! Los formatos NO se disuelven. Si en el futuro hay que soportar un
34//! formato simplificado nuevo, se agrega un reader acá y nadie aguas
35//! abajo se entera — siguen recibiendo `Card`.
36//!
37//! V1 (este commit) sólo soporta inputs JSON. La extensión a Nickel
38//! (con templates de defaults vía merge nativo de Nickel) llega en un
39//! commit separado para aislar la dependencia `nickel-lang-core`.
40
41#![forbid(unsafe_code)]
42
43use std::collections::BTreeMap;
44use std::path::Path;
45
46use serde::{Deserialize, Serialize};
47use serde_json::Value;
48use thiserror::Error;
49
50pub use brahman_card::Card as EnteCard;
51pub use nakui_ui_schema::Module as UiModuleSpec;
52pub use nouser_card::MonadManifest;
53
54/// Estructura canónica única que consumen los downstream del sistema
55/// (UI runtime, storage, DHT, wire). Cada formato input se proyecta
56/// a ésta vía un reader del brazo.
57///
58/// El wrapper común agrupa lo que TODOS los formatos comparten
59/// (identidad legible + extensiones forward-compat); el body preserva
60/// el typing rico de cada dominio sin colapsarlos.
61// PartialEq se omite porque algunos body variants vienen de crates
62// que no lo implementan (MonadManifest, nakui_ui_schema::Module).
63// Si downstream necesita igualdad, comparar via JSON round-trip o
64// agregar PartialEq en los crates origen.
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct Card {
67    /// Identificador opaco. String en el wrapper para no obligar a
68    /// los formatos a un mismo tipo concreto (Ente/Monad usan ULID,
69    /// UiModule usa slug human-friendly como `"sales_engine"`).
70    /// Cada reader documenta qué formato exige.
71    pub id: String,
72
73    /// Versión del schema canónico de este wrapper. Bump = romper
74    /// compat de los consumers downstream. Distinto de los
75    /// `schema_version` internos de cada body variant, que siguen
76    /// su propio versioning.
77    pub schema_version: u16,
78
79    /// Ancestro del que esta Card desciende (si aplica). Significado
80    /// específico al body variant (Ente: lineage del proceso; Monad:
81    /// split/merge de Mónada padre; UiModule: típicamente None).
82    #[serde(default)]
83    pub lineage: Option<String>,
84
85    /// Etiqueta humana legible. Cada reader la deriva del campo
86    /// equivalente del input (label/title/etc.).
87    pub label: String,
88
89    /// Campos no reconocidos del input se preservan acá. Permite
90    /// forward-compat: leer un input con campos nuevos no rompe la
91    /// carga, y volver a serializar conserva el extra.
92    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
93    pub extensions: BTreeMap<String, Value>,
94
95    /// Cuerpo tipado por dominio. La elección del variant es
96    /// responsabilidad del reader (basada en el input shape).
97    pub body: CardBody,
98}
99
100/// Versión actual del schema canónico de [`Card`]. Bump cuando cambie
101/// la shape del wrapper o las invariantes que comparten todos los
102/// variants.
103pub const CARD_SCHEMA_VERSION: u16 = 1;
104
105/// Variantes tipadas del body de [`Card`]. Una por dominio.
106///
107/// **Convención de extensión**: agregar un variant nuevo aquí + un
108/// reader que produzca ese variant. Los consumers que sólo manejen
109/// algunos variants pueden hacer `match { Ente(..) => ..., _ => skip }`.
110#[derive(Debug, Clone, Serialize, Deserialize)]
111#[serde(tag = "kind", rename_all = "snake_case")]
112pub enum CardBody {
113    /// Entidad runtime con proceso/payload/supervision (lo que era
114    /// `brahman_card::Card` directo).
115    Ente(EnteCard),
116
117    /// Agrupación semántica de archivos (Mónada de Nouser). No tiene
118    /// proceso; describe membership + signals semánticas (centroid,
119    /// keywords, lens).
120    Monad(MonadManifest),
121
122    /// Descriptor de módulo de UI: entities + views + menu + actions.
123    /// Lo que hoy lee la metainterface de Nakui desde
124    /// `examples/nakui-modules/<id>/module.json`.
125    UiModule(UiModuleSpec),
126}
127
128impl CardBody {
129    /// Etiqueta corta del variant — útil para mensajes de error y
130    /// dispatch en la UI sin necesitar match exhaustivo.
131    pub fn kind_name(&self) -> &'static str {
132        match self {
133            CardBody::Ente(_) => "ente",
134            CardBody::Monad(_) => "monad",
135            CardBody::UiModule(_) => "ui_module",
136        }
137    }
138}
139
140/// Errores de carga del brazo.
141#[derive(Debug, Error)]
142pub enum CardLoadError {
143    #[error("io: {0}")]
144    Io(#[from] std::io::Error),
145
146    #[error("parse JSON: {0}")]
147    JsonParse(#[from] serde_json::Error),
148
149    #[error("ningún reader registrado matcheó el input (shape no reconocido)")]
150    NoMatchingReader,
151
152    #[error("reader '{reader}' falló: {message}")]
153    ReaderFailed { reader: &'static str, message: String },
154
155    #[error("formato no soportado: extensión '{ext}'. Soportadas: {supported:?}")]
156    UnsupportedExtension {
157        ext: String,
158        supported: Vec<&'static str>,
159    },
160}
161
162/// Trait de reader. Cada formato implementa una instancia.
163///
164/// El dispatcher del brazo (`load_card`) prueba los readers en el
165/// orden registrado y se queda con el primero cuyo `can_read`
166/// devuelve `true`. Por eso el orden importa: poner los más
167/// específicos antes que los más laxos.
168pub trait CardReader: Send + Sync {
169    /// Nombre del reader, para mensajes de error.
170    fn name(&self) -> &'static str;
171
172    /// Dado un JSON Value (el input ya parseado a serde Value),
173    /// decide si este reader puede manejarlo. Heurística estructural
174    /// — el shape del input identifica el formato, no flags
175    /// explícitos (los inputs legacy no los tienen).
176    fn can_read(&self, input: &Value) -> bool;
177
178    /// Produce el [`Card`] canónico. Sólo se llama si `can_read`
179    /// devolvió `true`.
180    fn read(&self, input: Value) -> Result<Card, CardLoadError>;
181}
182
183mod readers;
184pub use readers::{EnteJsonReader, MonadJsonReader, UiModuleJsonReader};
185
186/// Construye el set default de readers para inputs JSON. El orden
187/// es deliberado: el más específico (UiModule, que tiene `entities`
188/// y `views` simultáneamente) antes que el más laxo. Si dos readers
189/// matchean, gana el primero.
190pub fn default_readers() -> Vec<Box<dyn CardReader>> {
191    vec![
192        Box::new(UiModuleJsonReader),
193        Box::new(MonadJsonReader),
194        Box::new(EnteJsonReader),
195    ]
196}
197
198/// Carga un Card desde una ruta. Detecta formato por extensión, y
199/// dentro de JSON detecta el shape probando los readers default en
200/// orden.
201///
202/// Para custom reader sets, usar [`load_card_with`].
203pub fn load_card(path: impl AsRef<Path>) -> Result<Card, CardLoadError> {
204    load_card_with(path, &default_readers())
205}
206
207/// Variante de [`load_card`] con readers custom. Útil para tests o
208/// para apps que quieren restringir formatos soportados.
209pub fn load_card_with(
210    path: impl AsRef<Path>,
211    readers: &[Box<dyn CardReader>],
212) -> Result<Card, CardLoadError> {
213    let path = path.as_ref();
214    let ext = path
215        .extension()
216        .and_then(|e| e.to_str())
217        .unwrap_or("")
218        .to_ascii_lowercase();
219    match ext.as_str() {
220        "json" => {
221            let bytes = std::fs::read(path)?;
222            let value: Value = serde_json::from_slice(&bytes)?;
223            dispatch_to_reader(value, readers)
224        }
225        other => Err(CardLoadError::UnsupportedExtension {
226            ext: other.to_string(),
227            supported: vec!["json"],
228        }),
229    }
230}
231
232/// Recorre los readers en orden, se queda con el primero que matchea
233/// y delega la conversión.
234fn dispatch_to_reader(
235    input: Value,
236    readers: &[Box<dyn CardReader>],
237) -> Result<Card, CardLoadError> {
238    for r in readers {
239        if r.can_read(&input) {
240            return r.read(input);
241        }
242    }
243    Err(CardLoadError::NoMatchingReader)
244}