⎇ 09501911b7ec497215773d2cb7192a2e6ce17e02
crates/core/brahman-cards/src/readers.rs
Historia
Quién tocó qué
crudo
feat(brahman-cards): brazo unificado V1 — readers JSON + estructura canónica
1//! Readers V1: tres formatos JSON ya existentes en el monorepo.
2//!
3//! Cada reader implementa:
4//! - `can_read`: heurística estructural para decidir si el JSON es
5//! suyo. No requiere flag explícito en el input — los inputs legacy
6//! no los tienen.
7//! - `read`: deserializa el JSON al tipo del crate origen (sin tocarlo)
8//! y lo envuelve en [`Card`] derivando los campos del wrapper.
9//!
10//! Convenciones para derivar el wrapper:
11//! - `id`: del campo `id` del input (cada formato lo expone). Si es
12//! ULID se serializa a string canónico.
13//! - `label`: del campo `label`.
14//! - `lineage`: del campo `lineage` cuando existe (Ente/Monad).
15//! - `extensions`: campos JSON desconocidos respecto a la struct del
16//! crate origen. Hoy lo mantenemos vacío (los crates origen ya
17//! tienen sus propios `extensions` internos via `#[serde(flatten)]`)
18//! — no duplicamos. Si en el futuro queremos mover el "extras" del
19//! crate origen al wrapper, esta es la palanca.
20
21use serde_json::Value;
22
23use crate::{Card, CardBody, CardLoadError, CardReader, EnteCard, MonadManifest, UiModuleSpec, CARD_SCHEMA_VERSION};
24
25// ============================================================================
26// Ente (brahman-card)
27// ============================================================================
28
29/// Reader para el shape JSON de [`brahman_card::Card`].
30///
31/// Heurística de detección: el input tiene `payload` Y `supervision`
32/// — son los campos requeridos del schema Ente que ningún otro
33/// formato del monorepo tiene.
34pub struct EnteJsonReader;
35
36impl CardReader for EnteJsonReader {
37 fn name(&self) -> &'static str {
38 "ente-json"
39 }
40
41 fn can_read(&self, input: &Value) -> bool {
42 let obj = match input.as_object() {
43 Some(o) => o,
44 None => return false,
45 };
46 obj.contains_key("payload") && obj.contains_key("supervision")
47 }
48
49 fn read(&self, input: Value) -> Result<Card, CardLoadError> {
50 let id = pull_string(&input, "id").unwrap_or_default();
51 let label = pull_string(&input, "label").unwrap_or_default();
52 let lineage = pull_string(&input, "lineage");
53
54 let ente: EnteCard =
55 serde_json::from_value(input).map_err(|e| CardLoadError::ReaderFailed {
56 reader: "ente-json",
57 message: e.to_string(),
58 })?;
59
60 Ok(Card {
61 id,
62 schema_version: CARD_SCHEMA_VERSION,
63 lineage,
64 label,
65 extensions: Default::default(),
66 body: CardBody::Ente(ente),
67 })
68 }
69}
70
71// ============================================================================
72// Monad (nouser-card)
73// ============================================================================
74
75/// Reader para el shape JSON de [`nouser_card::MonadManifest`].
76///
77/// Heurística: tiene `members` (BTreeSet<FileId>) Y `cardinality`
78/// (u32). La combinación es exclusiva del MonadManifest.
79pub struct MonadJsonReader;
80
81impl CardReader for MonadJsonReader {
82 fn name(&self) -> &'static str {
83 "monad-json"
84 }
85
86 fn can_read(&self, input: &Value) -> bool {
87 let obj = match input.as_object() {
88 Some(o) => o,
89 None => return false,
90 };
91 obj.contains_key("members") && obj.contains_key("cardinality")
92 }
93
94 fn read(&self, input: Value) -> Result<Card, CardLoadError> {
95 let id = pull_string(&input, "id").unwrap_or_default();
96 let label = pull_string(&input, "label").unwrap_or_default();
97 let lineage = pull_string(&input, "lineage");
98
99 let monad: MonadManifest =
100 serde_json::from_value(input).map_err(|e| CardLoadError::ReaderFailed {
101 reader: "monad-json",
102 message: e.to_string(),
103 })?;
104
105 Ok(Card {
106 id,
107 schema_version: CARD_SCHEMA_VERSION,
108 lineage,
109 label,
110 extensions: Default::default(),
111 body: CardBody::Monad(monad),
112 })
113 }
114}
115
116// ============================================================================
117// UiModule (nakui-ui-schema)
118// ============================================================================
119
120/// Reader para el shape JSON de los `module.json` de la UI Nakui
121/// ([`nakui_ui_schema::Module`]).
122///
123/// Heurística: tiene `entities` Y `views` Y `menu`. Es el shape más
124/// específico del repo, así que va primero en el orden default — si
125/// matchea, ningún otro reader debería intentar.
126pub struct UiModuleJsonReader;
127
128impl CardReader for UiModuleJsonReader {
129 fn name(&self) -> &'static str {
130 "ui-module-json"
131 }
132
133 fn can_read(&self, input: &Value) -> bool {
134 let obj = match input.as_object() {
135 Some(o) => o,
136 None => return false,
137 };
138 obj.contains_key("entities") && obj.contains_key("views") && obj.contains_key("menu")
139 }
140
141 fn read(&self, input: Value) -> Result<Card, CardLoadError> {
142 let id = pull_string(&input, "id").unwrap_or_default();
143 let label = pull_string(&input, "label").unwrap_or_default();
144 // UiModule no tiene lineage en su schema, queda None.
145 let module: UiModuleSpec =
146 serde_json::from_value(input).map_err(|e| CardLoadError::ReaderFailed {
147 reader: "ui-module-json",
148 message: e.to_string(),
149 })?;
150
151 Ok(Card {
152 id,
153 schema_version: CARD_SCHEMA_VERSION,
154 lineage: None,
155 label,
156 extensions: Default::default(),
157 body: CardBody::UiModule(module),
158 })
159 }
160}
161
162// ============================================================================
163// Helpers
164// ============================================================================
165
166fn pull_string(v: &Value, key: &str) -> Option<String> {
167 v.get(key)?.as_str().map(|s| s.to_string())
168}