feat(brahman-cards): Nickel reader + templates con merge nativo (V2)

El brazo ahora acepta `.ncl`: evalúa via nickel-lang 2.0, exporta a
JSON, dispatcha por los readers JSON estándar. Templates funcionan
con import + & merge nativos de Nickel — el brazo no inventa
mecánica paralela.

- Dep nickel-lang = "2.0.0" (interfaz estable).
- Nuevo módulo nickel_eval con eval_nickel_file(path) -> Value y
  errores tipados (Io/Eval/Export/JsonReparse). Mensaje de Nickel
  como texto plano sin ANSI.
- load_card_with añade arm "ncl" simétrico al "json".
- CardLoadError::Nickel propaga el error limpio.
- Imports resueltos: parent dir del input + env
  BRAHMAN_CARDS_TEMPLATES_DIR (registry global, opcional).
- Convención obligatoria documentada: fields override-ables del
  template usan `| default` (sin eso Nickel rechaza el merge).

9 tests nuevos: eval directo, dispatch a UiModule/Ente, template
merge con id+label override, registry via env, error wrapping,
contract violation en eval-time (`id | String = 42`), shape
desconocida.

22 tests totales en brahman-cards (13 JSON V1 + 9 Nickel V2).
Workspace build verde.

NO hace: migrar consumers, set canonical de templates, KCL→Nickel
— todos para commits siguientes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sergio
2026-05-09 23:25:57 +00:00
parent 09501911b7
commit 2a4443790c
6 changed files with 1185 additions and 27 deletions
+95
View File
@@ -6,6 +6,101 @@ ratio/diff ver `git show <sha>`.
## 2026-05-09
### feat(brahman-cards): Nickel reader + templates con merge nativo (V2)
Sigue al V1 (readers JSON). Ahora el brazo acepta inputs `.ncl`:
los evalúa via `nickel-lang` 2.0, exporta a JSON, y dispatcha por
los mismos readers JSON estándar. Un `.ncl` puede producir
cualquier `CardBody` siempre que su shape sea reconocida. Los
templates funcionan con los `import` + `&` merge nativos de
Nickel — el brazo no inventa una mecánica paralela.
Cambios:
- **Dep `nickel-lang = "2.0.0"`** (interfaz estable, no
`nickel-lang-core` que es internal/inestable). Compila clean
pero suma ~1 min al build cold del crate.
- **Nuevo módulo `nickel_eval.rs`** con `eval_nickel_file(path) ->
Result<Value, NickelEvalError>`. Errores tipados:
`Io`, `Eval`, `Export`, `JsonReparse` — el mensaje de Nickel se
formatea como texto plano (sin ANSI) para que sea legible en
logs y toasts.
- **`load_card_with` añade `"ncl"`**: lee archivo → eval Nickel →
exporta a JSON → parsea de vuelta a Value → dispatch a los
readers JSON. Pipeline simétrico a `"json"`.
- **`CardLoadError::Nickel(NickelEvalError)`**: el error de
Nickel se propaga limpio al error público del brazo.
- **Resolución de imports**:
- El parent dir del input se agrega como import path → `import
"./template.ncl"` resuelve sin config.
- El env `BRAHMAN_CARDS_TEMPLATES_DIR` (constante exportada
`BRAHMAN_CARDS_TEMPLATES_ENV`) agrega un registry global →
`import "ui_module_minimal.ncl"` desde cualquier ubicación.
- No hay magic resolución por kind. El autor del Card decide
qué template importa.
**Convención obligatoria de templates** (documentada en
`nickel_eval.rs`): las fields que el usuario va a sobrescribir
deben marcarse `| default` (o `| optional`). Sin ese marker
Nickel rechaza el merge de strings/numbers no-iguales con la
misma prioridad. Patrón canónico:
```nickel
# template ui_module_basic.ncl
{
id | String | default = "TEMPLATE_ID",
label | String | default = "TEMPLATE_LABEL",
...
}
# uso concreto
let base = import "ui_module_basic.ncl" in
base & { id = "my_id", label = "Mi Label" }
```
9 tests nuevos en `tests/nickel.rs`:
- `eval_nickel_file_returns_value_for_valid_input` — happy path.
- `eval_nickel_file_surfaces_evaluation_error` — variant `Eval`
con path + message.
- `load_card_dispatches_ncl_to_ui_module_variant` — pipeline
e2e a UiModule.
- `load_card_dispatches_ncl_to_ente_variant` — pipeline e2e a
Ente.
- `template_merge_overrides_id_and_label_only` — el caso del
user: template + override de id+label, resto del template
intacto.
- `template_resolves_via_env_registry` — uso del env como
registry global.
- `load_card_wraps_nickel_error_in_card_load_error` — wrap
limpio del error.
- `nickel_contract_violation_caught_at_eval_time` — value-add
concreto: `id | String = 42` falla en eval, no en deserialize
ni aguas abajo.
- `ncl_evaluating_to_unknown_shape_returns_no_matching_reader`
— sanity de coherencia con dispatcher JSON.
22 tests en total en `brahman-cards` (13 JSON V1 + 9 Nickel V2).
Workspace build verde tras la dep nueva.
**Lo que NO hace V2** (sigue pendiente):
- No migra consumers — `nakui-ui` sigue cargando con
`nakui_ui_schema::load_modules_from_dir`. La migración a
`brahman_cards::load_card` queda para después.
- No define un set canonical de templates en el repo (algo
como `templates/ente_basic.ncl`, `templates/ui_module_minimal.ncl`).
Eso emerge cuando aparezcan los primeros casos de uso reales
donde dos cards comparten estructura.
- No hace cross-validation entre template + override (ej:
detectar que un override saca un campo required del template).
Nickel ya lo hace via contracts si el template tiene un schema.
- No expone una API streaming (load N cards en paralelo). El
use case actual es one-shot al boot.
**Pendientes para próximos commits** (orden):
1. Migrar consumers (`nakui-ui` consume `brahman_cards::load_card`).
2. Yahweh refactor: lift del MetaUi runtime a `crates/modules/ui_engine/`.
3. KCL → Nickel: kcl_wrapper reemplazado por evaluación de Nickel
contracts; los 3 schemas .k de nakui modules pasan a .ncl.
4. card.k eliminado (es REFERENCE ONLY documentado).
### feat(brahman-cards): brazo unificado V1 — readers JSON + estructura canónica
**Pivote arquitectónico** decidido en charla: Brahman maneja varios
formatos legítimos de "Card" (cada formato vive en su crate origen y
Generated
+562 -26
View File
@@ -106,6 +106,12 @@ dependencies = [
"memchr",
]
[[package]]
name = "aliasable"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
[[package]]
name = "aligned"
version = "0.4.3"
@@ -270,12 +276,24 @@ dependencies = [
"password-hash",
]
[[package]]
name = "arraydeque"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
[[package]]
name = "arrayref"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
[[package]]
name = "arrayvec"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
[[package]]
name = "arrayvec"
version = "0.7.6"
@@ -315,7 +333,16 @@ version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8824ecca2e851cec16968d54a01dd372ef8f95b244fb84b84e70128be347c3c6"
dependencies = [
"term",
"term 0.7.0",
]
[[package]]
name = "ascii-canvas"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef1e3e699d84ab1b0911a1010c5c106aa34ae89aeac103be5ce0c3859db1e891"
dependencies = [
"term 1.2.1",
]
[[package]]
@@ -800,7 +827,7 @@ dependencies = [
"aligned",
"anyhow",
"arg_enum_proc_macro",
"arrayvec",
"arrayvec 0.7.6",
"log",
"num-rational",
"num-traits",
@@ -818,7 +845,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8"
dependencies = [
"anyhow",
"arrayvec",
"arrayvec 0.7.6",
"log",
"nom 8.0.0",
"num-rational",
@@ -831,7 +858,7 @@ version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7178fe5f7d460b13895ebb9dcb28a3a6216d2df2574a0806cb51b555d297f38"
dependencies = [
"arrayvec",
"arrayvec 0.7.6",
]
[[package]]
@@ -887,6 +914,12 @@ dependencies = [
"zeroize",
]
[[package]]
name = "beef"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1"
[[package]]
name = "bincode"
version = "1.3.3"
@@ -964,6 +997,12 @@ version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
[[package]]
name = "bitmaps"
version = "3.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d084b0137aaa901caf9f1e8b21daa6aa24d41cd806e111335541eff9683bd6"
[[package]]
name = "bitstream-io"
version = "4.10.0"
@@ -995,7 +1034,7 @@ dependencies = [
"ash-window",
"bitflags 2.11.1",
"bytemuck",
"codespan-reporting",
"codespan-reporting 0.12.0",
"glow",
"gpu-alloc",
"gpu-alloc-ash",
@@ -1059,7 +1098,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce"
dependencies = [
"arrayref",
"arrayvec",
"arrayvec 0.7.6",
"cc",
"cfg-if",
"constant_time_eq",
@@ -1200,6 +1239,7 @@ version = "0.1.0"
dependencies = [
"brahman-card",
"nakui-ui-schema",
"nickel-lang",
"nouser-card",
"serde",
"serde_json",
@@ -1432,7 +1472,7 @@ dependencies = [
"cedar-policy-core",
"cedar-policy-validator",
"itertools 0.10.5",
"lalrpop-util",
"lalrpop-util 0.20.2",
"ref-cast",
"serde",
"serde_json",
@@ -1449,8 +1489,8 @@ dependencies = [
"either",
"ipnet",
"itertools 0.10.5",
"lalrpop",
"lalrpop-util",
"lalrpop 0.20.2",
"lalrpop-util 0.20.2",
"lazy_static",
"miette",
"regex",
@@ -1706,6 +1746,16 @@ dependencies = [
"objc",
]
[[package]]
name = "codespan"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "583f52b0658b321b25fd6b209b6c76cf058f433071297de64e5980c3d9aad937"
dependencies = [
"codespan-reporting 0.13.1",
"serde",
]
[[package]]
name = "codespan-reporting"
version = "0.12.0"
@@ -1717,6 +1767,17 @@ dependencies = [
"unicode-width 0.2.2",
]
[[package]]
name = "codespan-reporting"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681"
dependencies = [
"serde",
"termcolor",
"unicode-width 0.2.2",
]
[[package]]
name = "color_quant"
version = "1.1.0"
@@ -3264,6 +3325,12 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "fixedbitset"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
[[package]]
name = "flate2"
version = "1.1.9"
@@ -4053,7 +4120,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4f3bedd573fafafa13d1200b356c588cf094fb2786e3684bb3f5ea59b549fa9"
dependencies = [
"arrayvec",
"arrayvec 0.7.6",
"log",
"rayon",
]
@@ -4773,6 +4840,15 @@ version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285"
[[package]]
name = "imbl-sized-chunks"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f4241005618a62f8d57b2febd02510fb96e0137304728543dfc5fd6f052c22d"
dependencies = [
"bitmaps",
]
[[package]]
name = "imgref"
version = "1.12.1"
@@ -4815,6 +4891,15 @@ dependencies = [
"web-time",
]
[[package]]
name = "indoc"
version = "2.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
dependencies = [
"rustversion",
]
[[package]]
name = "inotify"
version = "0.9.6"
@@ -4994,6 +5079,15 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "json_scanner"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe0a2dc336065c75719cffd3c6c929e0ec4ed85b92b8248a7bbd999acb0e419c"
dependencies = [
"memchr",
]
[[package]]
name = "jsonwebtoken"
version = "9.3.1"
@@ -5009,6 +5103,15 @@ dependencies = [
"simple_asn1",
]
[[package]]
name = "keccak"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653"
dependencies = [
"cpufeatures 0.2.17",
]
[[package]]
name = "khronos-egl"
version = "6.0.0"
@@ -5045,7 +5148,7 @@ version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c62026ae44756f8a599ba21140f350303d4f08dcdcc71b5ad9c9bb8128c13c62"
dependencies = [
"arrayvec",
"arrayvec 0.7.6",
"euclid",
"smallvec",
]
@@ -5065,22 +5168,44 @@ version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55cb077ad656299f160924eb2912aa147d7339ea7d69e1b5517326fdcec3c1ca"
dependencies = [
"ascii-canvas",
"ascii-canvas 3.0.0",
"bit-set 0.5.3",
"ena",
"itertools 0.11.0",
"lalrpop-util",
"petgraph",
"lalrpop-util 0.20.2",
"petgraph 0.6.5",
"pico-args",
"regex",
"regex-syntax",
"string_cache",
"term",
"term 0.7.0",
"tiny-keccak",
"unicode-xid",
"walkdir",
]
[[package]]
name = "lalrpop"
version = "0.22.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba4ebbd48ce411c1d10fb35185f5a51a7bfa3d8b24b4e330d30c9e3a34129501"
dependencies = [
"ascii-canvas 4.0.0",
"bit-set 0.8.0",
"ena",
"itertools 0.14.0",
"lalrpop-util 0.22.2",
"petgraph 0.7.1",
"pico-args",
"regex",
"regex-syntax",
"sha3",
"string_cache",
"term 1.2.1",
"unicode-xid",
"walkdir",
]
[[package]]
name = "lalrpop-util"
version = "0.20.2"
@@ -5090,6 +5215,16 @@ dependencies = [
"regex-automata",
]
[[package]]
name = "lalrpop-util"
version = "0.22.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5baa5e9ff84f1aefd264e6869907646538a52147a755d494517a8007fb48733"
dependencies = [
"regex-automata",
"rustversion",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
@@ -5593,6 +5728,40 @@ dependencies = [
"value-bag",
]
[[package]]
name = "logos"
version = "0.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff472f899b4ec2d99161c51f60ff7075eeb3097069a36050d8037a6325eb8154"
dependencies = [
"logos-derive",
]
[[package]]
name = "logos-codegen"
version = "0.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "192a3a2b90b0c05b27a0b2c43eecdb7c415e29243acc3f89cc8247a5b693045c"
dependencies = [
"beef",
"fnv",
"lazy_static",
"proc-macro2",
"quote",
"regex-syntax",
"rustc_version",
"syn 2.0.117",
]
[[package]]
name = "logos-derive"
version = "0.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "605d9697bcd5ef3a42d38efc51541aa3d6a4a25f7ab6d1ed0da5ac632a26b470"
dependencies = [
"logos-codegen",
]
[[package]]
name = "loop9"
version = "0.1.5"
@@ -5643,7 +5812,7 @@ version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4336502e29e32af93cf2dad2214ed6003c17ceb5bd499df77b1de663b9042b92"
dependencies = [
"arrayvec",
"arrayvec 0.7.6",
"euclid",
"num-traits",
]
@@ -5691,6 +5860,68 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "670fdfda89751bc4a84ac13eaa63e205cf0fd22b4c9a5fbfa085b63c1f1d3a30"
[[package]]
name = "malachite"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec410515e231332b14cd986a475d1c3323bcfa4c7efc038bfa1d5b410b1c57e4"
dependencies = [
"malachite-base",
"malachite-float",
"malachite-nz",
"malachite-q",
]
[[package]]
name = "malachite-base"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c738d3789301e957a8f7519318fcbb1b92bb95863b28f6938ae5a05be6259f34"
dependencies = [
"hashbrown 0.15.5",
"itertools 0.14.0",
"libm",
"ryu",
]
[[package]]
name = "malachite-float"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9446a966be7f1708c10badd6690d3094b5ad62d3accdcf2154740656d7650cfa"
dependencies = [
"itertools 0.14.0",
"malachite-base",
"malachite-nz",
"malachite-q",
"serde",
]
[[package]]
name = "malachite-nz"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1707c9a1fa36ce21749b35972bfad17bbf34cf5a7c96897c0491da321e387d3b"
dependencies = [
"itertools 0.14.0",
"libm",
"malachite-base",
"serde",
"wide",
]
[[package]]
name = "malachite-q"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d764801aa4e96bbb69b389dcd03b50075345131cd63ca2e380bca71cc37a3675"
dependencies = [
"itertools 0.14.0",
"malachite-base",
"malachite-nz",
"serde",
]
[[package]]
name = "malloc_buf"
version = "0.0.6"
@@ -6110,11 +6341,11 @@ version = "25.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b977c445f26e49757f9aca3631c3b8b836942cb278d69a92e7b80d3b24da632"
dependencies = [
"arrayvec",
"arrayvec 0.7.6",
"bit-set 0.8.0",
"bitflags 2.11.1",
"cfg_aliases",
"codespan-reporting",
"codespan-reporting 0.12.0",
"half",
"hashbrown 0.15.5",
"hexf-parse",
@@ -6135,7 +6366,7 @@ version = "0.1.0"
dependencies = [
"brahman-card",
"brahman-sidecar",
"petgraph",
"petgraph 0.6.5",
"rhai",
"serde",
"serde_json",
@@ -6315,6 +6546,102 @@ dependencies = [
"smallvec",
]
[[package]]
name = "nickel-lang"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec629a17e21b7e192dfc715a9567fb4f773a6d0c92424f0ec659c6633f31b087"
dependencies = [
"codespan-reporting 0.13.1",
"indexmap 2.14.0",
"malachite",
"nickel-lang-core",
"nickel-lang-vector",
"serde",
"serde_json",
"serde_yaml",
"toml 0.9.12+spec-1.1.0",
]
[[package]]
name = "nickel-lang-core"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51647f09e6e385c140226867c62292f23c31241ee2b4986f3f71a40d48e88a60"
dependencies = [
"base64 0.22.1",
"bumpalo",
"codespan",
"codespan-reporting 0.13.1",
"colorchoice",
"indexmap 2.14.0",
"indoc",
"json_scanner",
"lalrpop 0.22.2",
"lalrpop-util 0.22.2",
"logos",
"malachite",
"malachite-q",
"md-5",
"nickel-lang-parser",
"nickel-lang-vector",
"once_cell",
"ouroboros",
"paste",
"pretty",
"regex",
"saphyr-parser",
"serde",
"serde_json",
"serde_yaml",
"sha-1",
"sha2",
"simple-counter",
"smallvec",
"strip-ansi-escapes",
"strsim",
"toml 0.9.12+spec-1.1.0",
"toml_edit 0.23.10+spec-1.0.0",
"typed-arena",
"unicode-segmentation",
]
[[package]]
name = "nickel-lang-parser"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20d8810705f1a243b1996c83fe9f173d8fb33f1cc97ea5ff7486cc6ef8981f81"
dependencies = [
"bumpalo",
"codespan",
"codespan-reporting 0.13.1",
"indexmap 2.14.0",
"lalrpop 0.22.2",
"lalrpop-util 0.22.2",
"logos",
"malachite",
"nickel-lang-vector",
"ouroboros",
"pretty",
"regex",
"saphyr-parser",
"serde",
"serde_json",
"simple-counter",
"toml_edit 0.23.10+spec-1.0.0",
"typed-arena",
]
[[package]]
name = "nickel-lang-vector"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "870c323d81061fc47db4aa7346f6f3492f3e4bb8bd5dd8b7c85ac9d0c9709f0c"
dependencies = [
"imbl-sized-chunks",
"serde",
]
[[package]]
name = "nix"
version = "0.29.0"
@@ -6989,6 +7316,30 @@ dependencies = [
"ureq",
]
[[package]]
name = "ouroboros"
version = "0.18.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e0f050db9c44b97a94723127e6be766ac5c340c48f2c4bb3ffa11713744be59"
dependencies = [
"aliasable",
"ouroboros_macro",
"static_assertions",
]
[[package]]
name = "ouroboros_macro"
version = "0.18.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c7028bdd3d43083f6d8d4d5187680d0d3560d54df4cc9d752005268b41e64d0"
dependencies = [
"heck 0.4.1",
"proc-macro2",
"proc-macro2-diagnostics",
"quote",
"syn 2.0.117",
]
[[package]]
name = "parking"
version = "2.2.1"
@@ -7147,7 +7498,17 @@ version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db"
dependencies = [
"fixedbitset",
"fixedbitset 0.4.2",
"indexmap 2.14.0",
]
[[package]]
name = "petgraph"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772"
dependencies = [
"fixedbitset 0.5.7",
"indexmap 2.14.0",
]
@@ -7430,6 +7791,17 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]]
name = "pretty"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d22152487193190344590e4f30e219cf3fe140d9e7a3fdb683d82aa2c5f4156"
dependencies = [
"arrayvec 0.5.2",
"typed-arena",
"unicode-width 0.2.2",
]
[[package]]
name = "prettyplease"
version = "0.2.37"
@@ -7480,6 +7852,19 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "proc-macro2-diagnostics"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
"version_check",
"yansi",
]
[[package]]
name = "profiling"
version = "1.0.18"
@@ -7811,7 +8196,7 @@ dependencies = [
"aligned-vec",
"arbitrary",
"arg_enum_proc_macro",
"arrayvec",
"arrayvec 0.7.6",
"av-scenechange",
"av1-grain",
"bitstream-io",
@@ -8449,7 +8834,7 @@ version = "1.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c5108e3d4d903e21aac27f12ba5377b6b34f9f44b325e4894c7924169d06995"
dependencies = [
"arrayvec",
"arrayvec 0.7.6",
"borsh",
"bytes",
"num-traits",
@@ -8640,6 +9025,15 @@ version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "safe_arch"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323"
dependencies = [
"bytemuck",
]
[[package]]
name = "salsa20"
version = "0.10.2"
@@ -8658,6 +9052,16 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "saphyr-parser"
version = "0.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fb771b59f6b1985d1406325ec28f97cfb14256abcec4fdfb37b36a1766d6af7"
dependencies = [
"arraydeque",
"hashlink 0.10.0",
]
[[package]]
name = "schannel"
version = "0.1.29"
@@ -8970,6 +9374,30 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "serde_yaml"
version = "0.9.34+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
dependencies = [
"indexmap 2.14.0",
"itoa",
"ryu",
"serde",
"unsafe-libyaml",
]
[[package]]
name = "sha-1"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c"
dependencies = [
"cfg-if",
"cpufeatures 0.2.17",
"digest",
]
[[package]]
name = "sha1"
version = "0.10.6"
@@ -8998,6 +9426,16 @@ dependencies = [
"digest",
]
[[package]]
name = "sha3"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874"
dependencies = [
"digest",
"keccak",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"
@@ -9053,6 +9491,12 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]]
name = "simple-counter"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bb57743b52ea059937169c0061d70298fe2df1d2c988b44caae79dd979d9b49"
[[package]]
name = "simple_asn1"
version = "0.6.4"
@@ -9388,6 +9832,15 @@ dependencies = [
"quote",
]
[[package]]
name = "strip-ansi-escapes"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025"
dependencies = [
"vte",
]
[[package]]
name = "strsim"
version = "0.11.1"
@@ -9449,7 +9902,7 @@ version = "2.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3429154a8b5a98ca39100ba45ef49ae046fb1d0869dff78d78a2670b1b278982"
dependencies = [
"arrayvec",
"arrayvec 0.7.6",
"async-channel 2.5.0",
"bincode",
"chrono",
@@ -9814,7 +10267,7 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a13e5d13f79d558b5d353a98072ca8ca0e99da429467804de959aa8c83c9a004"
dependencies = [
"arrayvec",
"arrayvec 0.7.6",
"grid",
"serde",
"slotmap",
@@ -9896,6 +10349,15 @@ dependencies = [
"winapi",
]
[[package]]
name = "term"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8c27177b12a6399ffc08b98f76f7c9a1f4fe9fc967c784c5a071fa8d93cf7e1"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "termcolor"
version = "1.4.1"
@@ -10024,7 +10486,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab"
dependencies = [
"arrayref",
"arrayvec",
"arrayvec 0.7.6",
"bytemuck",
"cfg-if",
"log",
@@ -10188,6 +10650,21 @@ dependencies = [
"toml_edit 0.22.27",
]
[[package]]
name = "toml"
version = "0.9.12+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863"
dependencies = [
"indexmap 2.14.0",
"serde_core",
"serde_spanned 1.1.1",
"toml_datetime 0.7.5+spec-1.1.0",
"toml_parser",
"toml_writer",
"winnow 0.7.15",
]
[[package]]
name = "toml"
version = "1.1.2+spec-1.1.0"
@@ -10212,6 +10689,15 @@ dependencies = [
"serde",
]
[[package]]
name = "toml_datetime"
version = "0.7.5+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
dependencies = [
"serde_core",
]
[[package]]
name = "toml_datetime"
version = "1.1.1+spec-1.1.0"
@@ -10235,6 +10721,19 @@ dependencies = [
"winnow 0.7.15",
]
[[package]]
name = "toml_edit"
version = "0.23.10+spec-1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269"
dependencies = [
"indexmap 2.14.0",
"toml_datetime 0.7.5+spec-1.1.0",
"toml_parser",
"toml_writer",
"winnow 0.7.15",
]
[[package]]
name = "toml_edit"
version = "0.25.11+spec-1.1.0"
@@ -10481,6 +10980,12 @@ dependencies = [
"core_maths",
]
[[package]]
name = "typed-arena"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a"
[[package]]
name = "typeid"
version = "1.0.3"
@@ -10667,6 +11172,12 @@ dependencies = [
"subtle",
]
[[package]]
name = "unsafe-libyaml"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]]
name = "unsigned-varint"
version = "0.7.2"
@@ -10879,6 +11390,15 @@ dependencies = [
"libc",
]
[[package]]
name = "vte"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077"
dependencies = [
"memchr",
]
[[package]]
name = "waker-fn"
version = "1.2.0"
@@ -11035,7 +11555,7 @@ version = "0.40.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a19af97fcb96045dd1d6b4d23e2b4abdbbe81723dbc5c9f016eb52145b320063"
dependencies = [
"arrayvec",
"arrayvec 0.7.6",
"multi-stash",
"smallvec",
"spin",
@@ -11317,6 +11837,16 @@ dependencies = [
"winsafe",
]
[[package]]
name = "wide"
version = "0.7.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03"
dependencies = [
"bytemuck",
"safe_arch",
]
[[package]]
name = "widestring"
version = "1.2.1"
@@ -12461,6 +12991,12 @@ dependencies = [
"web-time",
]
[[package]]
name = "yansi"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
[[package]]
name = "yasna"
version = "0.5.2"
+1
View File
@@ -16,6 +16,7 @@ ulid = { workspace = true }
brahman-card = { path = "../brahman-card" }
nouser-card = { path = "../../modules/nouser/card" }
nakui-ui-schema = { path = "../../modules/nakui/ui-schema" }
nickel-lang = "2.0.0"
[dev-dependencies]
serde_json = { workspace = true }
+15 -1
View File
@@ -157,6 +157,9 @@ pub enum CardLoadError {
ext: String,
supported: Vec<&'static str>,
},
#[error("evaluación Nickel: {0}")]
Nickel(#[from] NickelEvalError),
}
/// Trait de reader. Cada formato implementa una instancia.
@@ -180,7 +183,9 @@ pub trait CardReader: Send + Sync {
fn read(&self, input: Value) -> Result<Card, CardLoadError>;
}
mod nickel_eval;
mod readers;
pub use nickel_eval::{eval_nickel_file, NickelEvalError, BRAHMAN_CARDS_TEMPLATES_ENV};
pub use readers::{EnteJsonReader, MonadJsonReader, UiModuleJsonReader};
/// Construye el set default de readers para inputs JSON. El orden
@@ -222,9 +227,18 @@ pub fn load_card_with(
let value: Value = serde_json::from_slice(&bytes)?;
dispatch_to_reader(value, readers)
}
"ncl" => {
// Nickel pipeline: leer archivo → evaluar deeply → exportar
// a JSON → parsear como Value → dispatch a los readers JSON
// estándar. Templates funcionan via los `import` nativos de
// Nickel; el evaluator resuelve relativo al input y al
// `BRAHMAN_CARDS_TEMPLATES_DIR` env (si está set).
let value = eval_nickel_file(path)?;
dispatch_to_reader(value, readers)
}
other => Err(CardLoadError::UnsupportedExtension {
ext: other.to_string(),
supported: vec!["json"],
supported: vec!["json", "ncl"],
}),
}
}
@@ -0,0 +1,141 @@
//! Evaluador Nickel para inputs `.ncl`.
//!
//! El brazo de Cards lee Nickel como **fuente** y produce JSON como
//! **representación intermedia** que después dispatcha por los readers
//! estándar. Esto significa que un `.ncl` puede producir cualquier
//! variant del [`super::CardBody`] siempre que evalúe a una shape JSON
//! que alguno de los readers reconozca.
//!
//! # Templates
//!
//! Nickel soporta `import "..."` y el operador `&` de merge nativo. Un
//! Card "concreto" puede ser un template + override:
//!
//! ```nickel
//! let base = import "ente_basic.ncl" in
//! base & { id = "01ARZ...", label = "mi-ente" }
//! ```
//!
//! **Convención obligatoria del template**: las fields que el usuario
//! va a sobrescribir tienen que estar marcadas `| default` (o
//! `| optional`). Nickel rechaza el merge de dos strings/numbers
//! distintos con la misma prioridad — el `| default` baja la prioridad
//! del template y deja que el override del user gane:
//!
//! ```nickel
//! # template ui_module_basic.ncl
//! {
//! id | String | default = "TEMPLATE_ID",
//! label | String | default = "TEMPLATE_LABEL",
//! # ...
//! }
//! ```
//!
//! Resolución de imports (en orden):
//! 1. Relativo al directorio del archivo input (default de Nickel).
//! 2. `BRAHMAN_CARDS_TEMPLATES_DIR` (env). Permite tener un
//! registry global de templates accesible por nombre desnudo:
//! `import "ui_module_basic.ncl"`.
//!
//! No agregamos magic resolución por kind — el autor decide qué
//! template importa explícitamente.
use std::ffi::OsString;
use std::path::Path;
use serde_json::Value;
use thiserror::Error;
/// Variable de entorno opcional. Si está set, su path se agrega al
/// search path de imports de Nickel después del parent dir del input,
/// permitiendo `import "<nombre>.ncl"` desde cualquier ubicación.
pub const BRAHMAN_CARDS_TEMPLATES_ENV: &str = "BRAHMAN_CARDS_TEMPLATES_DIR";
/// Errores específicos del pipeline Nickel. Wrap del error de Nickel
/// formateado como texto plano (sin ANSI) + el path del input para
/// contexto.
#[derive(Debug, Error)]
pub enum NickelEvalError {
#[error("io leyendo {path}: {source}")]
Io {
path: String,
#[source]
source: std::io::Error,
},
#[error("evaluación de '{path}' falló:\n{message}")]
Eval { path: String, message: String },
#[error("export a JSON de '{path}' falló:\n{message}")]
Export { path: String, message: String },
#[error("JSON exportado por Nickel no parsea de vuelta: {source}")]
JsonReparse {
#[source]
source: serde_json::Error,
},
}
/// Lee `path` (debe ser un `.ncl` válido), lo evalúa profundamente vía
/// `nickel-lang` y devuelve el resultado como `serde_json::Value`
/// listo para dispatch a un reader JSON.
///
/// El parent dir del input se agrega como import path para que
/// imports relativos tipo `import "./template.ncl"` funcionen sin
/// configuración extra. Si `BRAHMAN_CARDS_TEMPLATES_DIR` está set,
/// también se agrega.
pub fn eval_nickel_file(path: &Path) -> Result<Value, NickelEvalError> {
let path_display = path.display().to_string();
let source = std::fs::read_to_string(path).map_err(|e| NickelEvalError::Io {
path: path_display.clone(),
source: e,
})?;
let mut import_paths: Vec<OsString> = Vec::new();
if let Some(parent) = path.parent() {
if !parent.as_os_str().is_empty() {
import_paths.push(parent.into());
}
}
if let Ok(reg) = std::env::var(BRAHMAN_CARDS_TEMPLATES_ENV) {
if !reg.is_empty() {
import_paths.push(reg.into());
}
}
let mut ctx = nickel_lang::Context::new()
.with_added_import_paths(import_paths)
.with_source_name(path_display.clone());
let expr = ctx
.eval_deep_for_export(&source)
.map_err(|e| NickelEvalError::Eval {
path: path_display.clone(),
message: format_nickel_error(&e),
})?;
let json_str = ctx
.expr_to_json(&expr)
.map_err(|e| NickelEvalError::Export {
path: path_display.clone(),
message: format_nickel_error(&e),
})?;
serde_json::from_str(&json_str).map_err(|e| NickelEvalError::JsonReparse { source: e })
}
/// Formatea un error de Nickel como texto plano. Usa `ErrorFormat::Text`
/// (sin ANSI) para que sea legible en logs y mensajes de UI sin
/// escape sequences.
fn format_nickel_error(err: &nickel_lang::Error) -> String {
let mut buf: Vec<u8> = Vec::new();
if err
.format(&mut buf, nickel_lang::ErrorFormat::Text)
.is_err()
{
// Si la propia formateación falla, devolvemos el Debug —
// peor mensaje que el normal pero no perdemos info.
return format!("{err:?}");
}
String::from_utf8(buf).unwrap_or_else(|_| format!("{err:?}"))
}
+371
View File
@@ -0,0 +1,371 @@
//! Nickel reader + templates.
//!
//! V2 del brazo: la dispatcher acepta archivos `.ncl`. La evaluación
//! produce JSON intermedio que va a los readers estándar, así que un
//! `.ncl` puede generar cualquier `CardBody` siempre que su shape sea
//! reconocida.
//!
//! Templates: Nickel `import` + `&` merge nativos. El brazo no
//! inventa nada — sólo agrega el parent dir + el env
//! `BRAHMAN_CARDS_TEMPLATES_DIR` al import path.
use std::fs;
use std::path::PathBuf;
use brahman_cards::{
eval_nickel_file, load_card, CardBody, CardLoadError, NickelEvalError,
BRAHMAN_CARDS_TEMPLATES_ENV,
};
use serde_json::json;
// ===========================================================================
// Helpers
// ===========================================================================
fn unique_dir(name: &str) -> PathBuf {
let mut p = std::env::temp_dir();
p.push(format!(
"brahman-cards-nickel-{}-{}-{}",
std::process::id(),
nanos(),
name
));
fs::create_dir_all(&p).unwrap();
p
}
fn nanos() -> u128 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0)
}
fn write_file(dir: &std::path::Path, name: &str, content: &str) -> PathBuf {
let p = dir.join(name);
fs::write(&p, content).unwrap();
p
}
// ===========================================================================
// 1. Evaluación directa: Nickel → Value
// ===========================================================================
#[test]
fn eval_nickel_file_returns_value_for_valid_input() {
let dir = unique_dir("eval-basic");
let p = write_file(
&dir,
"card.ncl",
r#"
{
id = "demo",
label = "Demo",
entities = [],
menu = [],
views = {},
}
"#,
);
let v = eval_nickel_file(&p).expect("eval ok");
assert_eq!(v.get("id"), Some(&json!("demo")));
assert_eq!(v.get("label"), Some(&json!("Demo")));
assert!(v.get("entities").is_some());
fs::remove_dir_all(&dir).ok();
}
#[test]
fn eval_nickel_file_surfaces_evaluation_error() {
let dir = unique_dir("eval-err");
let p = write_file(
&dir,
"broken.ncl",
r#"
{
id = "x",
label = doesnotexist,
}
"#,
);
let err = eval_nickel_file(&p).unwrap_err();
match err {
NickelEvalError::Eval { path, message } => {
assert!(path.contains("broken.ncl"));
assert!(!message.is_empty(), "el msg debe traer info de Nickel");
}
other => panic!("expected Eval error, got {other:?}"),
}
fs::remove_dir_all(&dir).ok();
}
// ===========================================================================
// 2. load_card pipeline: .ncl → Card
// ===========================================================================
#[test]
fn load_card_dispatches_ncl_to_ui_module_variant() {
let dir = unique_dir("dispatch-ui");
let p = write_file(
&dir,
"module.ncl",
r#"
{
id = "demo",
label = "Demo",
entities = [],
menu = [{ label = "Stock", view = "stock_list" }],
views = {
stock_list = {
kind = "list",
title = "Stock",
entity = "Stock",
columns = [],
},
},
}
"#,
);
let card = load_card(&p).expect("load ok");
assert_eq!(card.body.kind_name(), "ui_module");
assert_eq!(card.id, "demo");
assert_eq!(card.label, "Demo");
fs::remove_dir_all(&dir).ok();
}
#[test]
fn load_card_dispatches_ncl_to_ente_variant() {
let dir = unique_dir("dispatch-ente");
let p = write_file(
&dir,
"ente.ncl",
r#"
{
schema_version = 1,
id = "01ARZ3NDEKTSV4RRFFQ69G5FAV",
label = "test-ente",
payload = "Virtual",
supervision = "OneShot",
}
"#,
);
let card = load_card(&p).expect("load ok");
assert_eq!(card.body.kind_name(), "ente");
assert_eq!(card.id, "01ARZ3NDEKTSV4RRFFQ69G5FAV");
fs::remove_dir_all(&dir).ok();
}
// ===========================================================================
// 3. Templates: import + merge native de Nickel
// ===========================================================================
/// El caso de uso que el usuario describió: "un Card simple usa un
/// Card ya hecho cambiando sólo nombre y id". Template define la
/// shape full; el archivo concreto importa + override.
#[test]
fn template_merge_overrides_id_and_label_only() {
let dir = unique_dir("template-merge");
// Template con la shape full de un UiModule. Los campos
// sobrescribibles se marcan `| default` — Nickel sólo permite
// override en merge cuando hay diferencia de prioridad. Sin
// `| default` los strings no-iguales fallan con "non mergeable".
write_file(
&dir,
"ui_module_basic.ncl",
r#"
{
id | String | default = "TEMPLATE_ID",
label | String | default = "TEMPLATE_LABEL",
description = "stock + form básico",
entities = [
{ name = "Item", label = "Item", fields = [] },
],
menu = [
{ label = "Items", view = "items_list" },
{ label = "+ Item", view = "items_form" },
],
views = {
items_list = {
kind = "list",
title = "Items",
entity = "Item",
columns = [],
},
items_form = {
kind = "form",
title = "Nuevo item",
entity = "Item",
fields = [],
on_submit = {
kind = "seed_entity",
entity = "Item",
next_view = "items_list",
},
},
},
}
"#,
);
// Card concreto: import + merge override.
let p = write_file(
&dir,
"my_module.ncl",
r#"
let base = import "ui_module_basic.ncl" in
base & {
id = "my_module",
label = "Mi Módulo",
}
"#,
);
let card = load_card(&p).expect("template merge ok");
assert_eq!(card.id, "my_module", "el override del id se aplicó");
assert_eq!(card.label, "Mi Módulo", "el override del label se aplicó");
assert_eq!(card.body.kind_name(), "ui_module");
match card.body {
CardBody::UiModule(m) => {
// El resto viene del template intacto.
assert_eq!(m.menu.len(), 2);
assert_eq!(m.entities.len(), 1);
assert_eq!(m.entities[0].name, "Item");
}
other => panic!("variant inesperado: {:?}", other.kind_name()),
}
fs::remove_dir_all(&dir).ok();
}
/// El env `BRAHMAN_CARDS_TEMPLATES_DIR` permite tener un registry
/// global: el usuario importa por nombre desnudo desde cualquier
/// ubicación.
///
/// Este test setea/unset el env de forma local (no thread-safe en
/// tests paralelos contra el mismo env, pero usamos una key dedicada
/// y borramos después). Si se vuelve flaky, agregar mutex.
#[test]
fn template_resolves_via_env_registry() {
let registry = unique_dir("template-registry");
let inputs = unique_dir("template-input");
write_file(
&registry,
"ui_module_minimal.ncl",
r#"
{
id | String | default = "X",
label | String | default = "X",
entities = [],
menu = [],
views = {},
}
"#,
);
let p = write_file(
&inputs,
"from_registry.ncl",
r#"
let base = import "ui_module_minimal.ncl" in
base & { id = "registry_user", label = "Usado del Registry" }
"#,
);
// Set env, evaluar, restaurar.
let prev = std::env::var(BRAHMAN_CARDS_TEMPLATES_ENV).ok();
// SAFETY: nickel-lang tests modifican un env ad-hoc que no es
// referenciado por nada externo y se restaura al salir. Ningún
// otro test del crate lee este env.
unsafe {
std::env::set_var(BRAHMAN_CARDS_TEMPLATES_ENV, &registry);
}
let result = load_card(&p);
unsafe {
if let Some(v) = prev {
std::env::set_var(BRAHMAN_CARDS_TEMPLATES_ENV, v);
} else {
std::env::remove_var(BRAHMAN_CARDS_TEMPLATES_ENV);
}
}
let card = result.expect("template via registry ok");
assert_eq!(card.id, "registry_user");
assert_eq!(card.body.kind_name(), "ui_module");
fs::remove_dir_all(&registry).ok();
fs::remove_dir_all(&inputs).ok();
}
// ===========================================================================
// 4. Errores propagan limpios al CardLoadError
// ===========================================================================
#[test]
fn load_card_wraps_nickel_error_in_card_load_error() {
let dir = unique_dir("wrap-err");
let p = write_file(&dir, "bad.ncl", "let x = unknown in x");
let err = load_card(&p).unwrap_err();
match err {
CardLoadError::Nickel(NickelEvalError::Eval { .. }) => {} // expected
other => panic!("expected Nickel(Eval), got {other:?}"),
}
fs::remove_dir_all(&dir).ok();
}
/// El value-add concreto de Nickel sobre JSON: un contract
/// violation se captura en evaluación, ANTES de que el reader
/// JSON tenga oportunidad de aceptar un shape mal-tipado. Acá un
/// `id | String` con un value que no es String falla en eval-time
/// con un mensaje legible. JSON puro lo aceptaría y rompería más
/// tarde aguas abajo.
#[test]
fn nickel_contract_violation_caught_at_eval_time() {
let dir = unique_dir("contract-violation");
let p = write_file(
&dir,
"bad_id.ncl",
r#"
{
id | String = 42,
label = "X",
entities = [],
menu = [],
views = {},
}
"#,
);
let err = load_card(&p).unwrap_err();
match err {
CardLoadError::Nickel(NickelEvalError::Eval { message, .. }) => {
// Mensaje de contract violation legible.
assert!(
message.contains("contract") || message.contains("String"),
"msg debe mencionar contract o String: {message}"
);
}
other => panic!("expected Nickel(Eval), got {other:?}"),
}
fs::remove_dir_all(&dir).ok();
}
/// Sanity: un Nickel que evalúa a un shape NO-reconocible (no
/// matchea ningún reader) cae en `NoMatchingReader` — la cadena
/// Nickel + dispatcher se mantiene coherente.
#[test]
fn ncl_evaluating_to_unknown_shape_returns_no_matching_reader() {
let dir = unique_dir("unknown-shape");
let p = write_file(
&dir,
"weird.ncl",
r#"{ random = "shape", without = "fingerprint" }"#,
);
let err = load_card(&p).unwrap_err();
assert!(
matches!(err, CardLoadError::NoMatchingReader),
"expected NoMatchingReader, got {err:?}"
);
fs::remove_dir_all(&dir).ok();
}