feat(brahman-cards): templates Nickel canónicos para cada body kind
Materializa el patrón "import + override" del brazo. Hasta ahora BRAHMAN_CARDS_TEMPLATES_DIR existía como mecanismo pero el repo no shippeaba ningún template. 3 templates basic bajo crates/core/brahman-cards/templates/: - ente_basic.ncl: Card runtime mínima (Virtual + OneShot). - monad_basic.ncl: Mónada con metadata vacía. - ui_module_basic.ncl: descriptor UI con entities/menu/views vacíos. Cada field override-able marcada `| default` (sin eso Nickel rebota merge de strings/numbers no-iguales). Nuevo `pub fn canonical_templates_dir() -> PathBuf` resuelve el dir via CARGO_MANIFEST_DIR. Para distribución del binary standalone queda como pending (include_dir! o convención de install path). 5 tests E2E que cubren los 3 templates con import+override, sanity del default sin override, y existencia física del dir. Tests brahman-cards: 26 → 31 (+5). Workspace intacto. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,79 @@ ratio/diff ver `git show <sha>`.
|
|||||||
|
|
||||||
## 2026-05-10
|
## 2026-05-10
|
||||||
|
|
||||||
|
### feat(brahman-cards): templates Nickel canónicos para cada body kind
|
||||||
|
Materializa el patrón "import + override" del brazo: hasta ahora
|
||||||
|
`BRAHMAN_CARDS_TEMPLATES_DIR` existía como mecanismo pero el repo
|
||||||
|
no shippeaba ningún template. Ahora hay 3 templates basic (uno
|
||||||
|
por body kind del CardBody) bajo
|
||||||
|
`crates/core/brahman-cards/templates/`:
|
||||||
|
|
||||||
|
- **`ente_basic.ncl`** — Card runtime mínima: `payload="Virtual"`,
|
||||||
|
`supervision="OneShot"`, `schema_version=1`. Override típico:
|
||||||
|
`id` + `label`.
|
||||||
|
- **`monad_basic.ncl`** — agrupación semántica de archivos
|
||||||
|
(Mónada Nouser): metadata vacía, `dominant_lens="grid"` (lowercase
|
||||||
|
por convención serde rename_all). Override típico: `id`, `label`,
|
||||||
|
`members`, `cardinality`.
|
||||||
|
- **`ui_module_basic.ncl`** — descriptor UI con `entities=[]`,
|
||||||
|
`menu=[]`, `views={}`. Override típico: `id`, `label` y los
|
||||||
|
3 payloads.
|
||||||
|
|
||||||
|
Cada field override-able marcada `| default` (sin eso Nickel
|
||||||
|
rebota merge de strings/numbers no-iguales).
|
||||||
|
|
||||||
|
API nueva en `lib.rs`:
|
||||||
|
- **`pub fn canonical_templates_dir() -> PathBuf`**: devuelve el
|
||||||
|
directorio de templates del crate (resuelto via
|
||||||
|
`CARGO_MANIFEST_DIR`). Útil para apuntar el env
|
||||||
|
`BRAHMAN_CARDS_TEMPLATES_DIR` en runtime/tests sin hardcoding
|
||||||
|
del path.
|
||||||
|
- Doc explica que para distribución del binary standalone (cuando
|
||||||
|
emerja), incluir templates como recursos via `include_dir!` o
|
||||||
|
instalar el directorio junto al ejecutable.
|
||||||
|
|
||||||
|
5 tests E2E (`tests/templates.rs`) que cubren:
|
||||||
|
- `ente_basic` import + override `id`+`label` → Card body Ente
|
||||||
|
con `payload=Virtual` (default preserved).
|
||||||
|
- `monad_basic` import + override `id`+`label`+`cardinality` →
|
||||||
|
Card body Monad con members=[] y summary="" (defaults).
|
||||||
|
- `ui_module_basic` import + override de `id`+`label`+menu+views
|
||||||
|
→ Card body UiModule con entities=[] (default).
|
||||||
|
- Sanity: import sin override → defaults `"TEMPLATE_ID"` /
|
||||||
|
`"TEMPLATE_LABEL"` pasan al wrapper sin error.
|
||||||
|
- Sanity: el path de `canonical_templates_dir()` apunta a un
|
||||||
|
directorio existente con los 3 archivos esperados.
|
||||||
|
|
||||||
|
Helper de test `with_canonical_templates(F)` setea/restaura el
|
||||||
|
env localmente; cada test single-thread-safe.
|
||||||
|
|
||||||
|
Tests suite brahman-cards: 26 → **31** verdes (+5). El resto del
|
||||||
|
workspace intacto.
|
||||||
|
|
||||||
|
Beneficio operativo:
|
||||||
|
- Un usuario que quiera declarar un Card nuevo puede empezar con
|
||||||
|
un override de 2 líneas (`id` + `label`) en lugar de copiar el
|
||||||
|
shape full desde cero.
|
||||||
|
- Templates auto-documentan la convención `| default` para que
|
||||||
|
copiar uno y agregar fields propios "just works" en merge.
|
||||||
|
- El brazo sigue siendo agnostic — los templates son sólo
|
||||||
|
archivos `.ncl` resueltos via el import resolver Nickel; nada
|
||||||
|
hardcoded en código Rust.
|
||||||
|
|
||||||
|
Limitaciones:
|
||||||
|
- No hay templates "ricos" tipo `crud_basic.ncl` que parametricen
|
||||||
|
por entity name. Nickel no expone funciones-templates de la
|
||||||
|
forma típica de templating engines; lo más cercano sería un
|
||||||
|
template con un field `entity_name | String` y references
|
||||||
|
internas via `me.entity_name`. Cuando aparezca el caso de uso
|
||||||
|
real (e.g., un módulo donde el patrón list+form es repetitivo),
|
||||||
|
se diseña el template paramétrico.
|
||||||
|
- `canonical_templates_dir()` resuelve via `CARGO_MANIFEST_DIR` —
|
||||||
|
funciona en `cargo` (test/run/build) pero no para un binary
|
||||||
|
instalado fuera del workspace. Para release distribution la API
|
||||||
|
necesitará un fallback (resources embedded o convención de
|
||||||
|
install path).
|
||||||
|
|
||||||
### refactor(nakui-core): KCL → Nickel — `kcl_wrapper` reemplazado por evaluación in-process
|
### refactor(nakui-core): KCL → Nickel — `kcl_wrapper` reemplazado por evaluación in-process
|
||||||
Cierra el ciclo: el motor de validación de entities deja de
|
Cierra el ciclo: el motor de validación de entities deja de
|
||||||
shellear el binario externo `kcl` y pasa a evaluar **Nickel
|
shellear el binario externo `kcl` y pasa a evaluar **Nickel
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::path::Path;
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
@@ -188,6 +188,24 @@ mod readers;
|
|||||||
pub use nickel_eval::{eval_nickel_file, NickelEvalError, BRAHMAN_CARDS_TEMPLATES_ENV};
|
pub use nickel_eval::{eval_nickel_file, NickelEvalError, BRAHMAN_CARDS_TEMPLATES_ENV};
|
||||||
pub use readers::{EnteJsonReader, MonadJsonReader, UiModuleJsonReader};
|
pub use readers::{EnteJsonReader, MonadJsonReader, UiModuleJsonReader};
|
||||||
|
|
||||||
|
/// Path al directorio de templates Nickel canónicos shipped con el
|
||||||
|
/// crate (`crates/core/brahman-cards/templates/` en el repo).
|
||||||
|
///
|
||||||
|
/// Este directorio contiene los `*_basic.ncl` para cada body kind:
|
||||||
|
/// - `ente_basic.ncl`
|
||||||
|
/// - `monad_basic.ncl`
|
||||||
|
/// - `ui_module_basic.ncl`
|
||||||
|
///
|
||||||
|
/// Usar como path para [`BRAHMAN_CARDS_TEMPLATES_ENV`] o pasarlo
|
||||||
|
/// directo a Nickel via env. Resuelto via `CARGO_MANIFEST_DIR` —
|
||||||
|
/// funciona en `cargo test`/`cargo run` desde el workspace. Para
|
||||||
|
/// distribución del binary standalone (cuando emerja el caso de
|
||||||
|
/// uso), incluir los templates como recursos via `include_dir!` o
|
||||||
|
/// instalar el directorio junto al ejecutable.
|
||||||
|
pub fn canonical_templates_dir() -> PathBuf {
|
||||||
|
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("templates")
|
||||||
|
}
|
||||||
|
|
||||||
/// Construye el set default de readers para inputs JSON. El orden
|
/// Construye el set default de readers para inputs JSON. El orden
|
||||||
/// es deliberado: el más específico (UiModule, que tiene `entities`
|
/// es deliberado: el más específico (UiModule, que tiene `entities`
|
||||||
/// y `views` simultáneamente) antes que el más laxo. Si dos readers
|
/// y `views` simultáneamente) antes que el más laxo. Si dos readers
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# `ente_basic.ncl` — template canónico para Cards de tipo Ente.
|
||||||
|
#
|
||||||
|
# Use case típico: declarar una entity runtime mínima (Virtual
|
||||||
|
# payload, OneShot supervision) sobrescribiendo sólo `id` y `label`:
|
||||||
|
#
|
||||||
|
# let base = import "ente_basic.ncl" in
|
||||||
|
# base & {
|
||||||
|
# id = "01ARZ3NDEKTSV4RRFFQ69G5FAV",
|
||||||
|
# label = "mi-ente",
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# El brazo `brahman-cards::load_card` lo dispatcha al
|
||||||
|
# `EnteJsonReader` porque el shape resultante tiene `payload` Y
|
||||||
|
# `supervision` (los campos detect-key del reader Ente).
|
||||||
|
#
|
||||||
|
# **Convención obligatoria**: cada field que el usuario va a
|
||||||
|
# sobrescribir está marcada `| default`. Sin eso Nickel rebota el
|
||||||
|
# merge de strings/numbers no-iguales con misma prioridad.
|
||||||
|
|
||||||
|
{
|
||||||
|
schema_version | Number | default = 1,
|
||||||
|
|
||||||
|
# Identidad: el usuario casi siempre las sobrescribe.
|
||||||
|
id | String | default = "TEMPLATE_ID",
|
||||||
|
label | String | default = "TEMPLATE_LABEL",
|
||||||
|
|
||||||
|
# Runtime defaults razonables: nodo lógico sin proceso, sin
|
||||||
|
# restart. Override si querés un ente con payload Wasm/Native.
|
||||||
|
payload | default = "Virtual",
|
||||||
|
supervision | default = "OneShot",
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# `monad_basic.ncl` — template canónico para Cards de tipo Monad.
|
||||||
|
#
|
||||||
|
# Use case típico: declarar una agrupación semántica de archivos
|
||||||
|
# (Mónada de Nouser) con metadata mínima:
|
||||||
|
#
|
||||||
|
# let base = import "monad_basic.ncl" in
|
||||||
|
# base & {
|
||||||
|
# id = "01ARZ3NDEKTSV4RRFFQ69G5FAW",
|
||||||
|
# label = "fotos-2026",
|
||||||
|
# members = ["01ARZ3FILE1", "01ARZ3FILE2"],
|
||||||
|
# cardinality = 2,
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# El brazo lo dispatcha al `MonadJsonReader` por la presencia
|
||||||
|
# simultánea de `members` Y `cardinality`.
|
||||||
|
|
||||||
|
{
|
||||||
|
schema_version | Number | default = 1,
|
||||||
|
|
||||||
|
# Identidad: override siempre.
|
||||||
|
id | String | default = "TEMPLATE_ID",
|
||||||
|
label | String | default = "TEMPLATE_LABEL",
|
||||||
|
|
||||||
|
# Metadata semántica: defaults vacíos. El usuario typically
|
||||||
|
# override `members` + `cardinality`, opcionalmente `summary`
|
||||||
|
# / `keywords` / `dominant_lens`.
|
||||||
|
summary | String | default = "",
|
||||||
|
keywords | default = [],
|
||||||
|
centroid | default = [],
|
||||||
|
cardinality | Number | default = 0,
|
||||||
|
entropy | Number | default = 0.0,
|
||||||
|
# Lens variants serialize lowercase (serde rename_all): grid /
|
||||||
|
# code / gallery / database / markdown / tree.
|
||||||
|
dominant_lens | default = "grid",
|
||||||
|
|
||||||
|
# Membership: vacío por default. El usuario llena con los IDs
|
||||||
|
# de archivo cuando los conoce.
|
||||||
|
members | default = [],
|
||||||
|
pins | default = [],
|
||||||
|
|
||||||
|
# Timestamps Unix ms — default 0 = "no timestamp registrado".
|
||||||
|
# Override con el momento real cuando importa.
|
||||||
|
created_at_ms | Number | default = 0,
|
||||||
|
updated_at_ms | Number | default = 0,
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# `ui_module_basic.ncl` — template canónico para Cards de tipo
|
||||||
|
# UiModule (descriptores de módulos para metainterfaz yahweh).
|
||||||
|
#
|
||||||
|
# Use case típico: declarar un módulo nuevo sobrescribiendo `id`,
|
||||||
|
# `label`, y aportando los `entities`/`menu`/`views` propios:
|
||||||
|
#
|
||||||
|
# let base = import "ui_module_basic.ncl" in
|
||||||
|
# base & {
|
||||||
|
# id = "customers",
|
||||||
|
# label = "Clientes",
|
||||||
|
# entities = [
|
||||||
|
# { name = "Customer", label = "Cliente", fields = [...] },
|
||||||
|
# ],
|
||||||
|
# menu = [{ label = "Listar", view = "list" }],
|
||||||
|
# views = { list = { kind = "list", ... } },
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# El brazo lo dispatcha al `UiModuleJsonReader` por la presencia
|
||||||
|
# simultánea de `entities` Y `views` Y `menu`.
|
||||||
|
|
||||||
|
{
|
||||||
|
# Identidad: override siempre.
|
||||||
|
id | String | default = "TEMPLATE_ID",
|
||||||
|
label | String | default = "TEMPLATE_LABEL",
|
||||||
|
|
||||||
|
# Subtítulo opcional (tooltip en el sidebar). null por default.
|
||||||
|
description | default = null,
|
||||||
|
|
||||||
|
# Las 3 listas/maps son el **payload** real del módulo. El
|
||||||
|
# template las deja vacías para que el usuario las defina sin
|
||||||
|
# heredar nada útil-pero-equivocado de un default.
|
||||||
|
entities | default = [],
|
||||||
|
menu | default = [],
|
||||||
|
views | default = {},
|
||||||
|
}
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
//! Tests E2E de los templates canónicos shipped con el crate.
|
||||||
|
//!
|
||||||
|
//! Cada test escribe un Card user-side en un tempdir, importa el
|
||||||
|
//! template canónico, override id/label/etc., y verifica que el
|
||||||
|
//! brazo lo dispatcha al variant correcto del CardBody con los
|
||||||
|
//! valores merged.
|
||||||
|
//!
|
||||||
|
//! `BRAHMAN_CARDS_TEMPLATES_DIR` se setea localmente en cada test.
|
||||||
|
//! Como Nickel también busca relativo al input file, usamos el env
|
||||||
|
//! para que `import "ente_basic.ncl"` (sin path) resuelva desde
|
||||||
|
//! cualquier ubicación del input.
|
||||||
|
|
||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use brahman_cards::{
|
||||||
|
canonical_templates_dir, load_card, CardBody, BRAHMAN_CARDS_TEMPLATES_ENV,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Helper: corre `f()` con `BRAHMAN_CARDS_TEMPLATES_ENV` set al
|
||||||
|
/// directorio de templates canónicos, restaurando el env al salir.
|
||||||
|
///
|
||||||
|
/// Tests no son thread-safe entre sí cuando comparten env. Por eso
|
||||||
|
/// quedan en serial via `nextest --test-threads=1` o `cargo test`
|
||||||
|
/// que paralelizara sólo entre `tests/*.rs` distintos. Como este
|
||||||
|
/// archivo encapsula todo el setup de env, aún en paralelo entre
|
||||||
|
/// archivos de tests no chocan (cada thread setea/restaura).
|
||||||
|
fn with_canonical_templates<F: FnOnce()>(f: F) {
|
||||||
|
let prev = std::env::var(BRAHMAN_CARDS_TEMPLATES_ENV).ok();
|
||||||
|
let dir = canonical_templates_dir();
|
||||||
|
// SAFETY: env mutation single-threaded en este test.
|
||||||
|
unsafe {
|
||||||
|
std::env::set_var(BRAHMAN_CARDS_TEMPLATES_ENV, &dir);
|
||||||
|
}
|
||||||
|
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
|
||||||
|
unsafe {
|
||||||
|
match prev {
|
||||||
|
Some(v) => std::env::set_var(BRAHMAN_CARDS_TEMPLATES_ENV, v),
|
||||||
|
None => std::env::remove_var(BRAHMAN_CARDS_TEMPLATES_ENV),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Err(panic) = result {
|
||||||
|
std::panic::resume_unwind(panic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unique_dir(name: &str) -> PathBuf {
|
||||||
|
let mut p = std::env::temp_dir();
|
||||||
|
p.push(format!(
|
||||||
|
"brahman-cards-templates-{}-{}-{}",
|
||||||
|
std::process::id(),
|
||||||
|
std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.map(|d| d.as_nanos())
|
||||||
|
.unwrap_or(0),
|
||||||
|
name
|
||||||
|
));
|
||||||
|
fs::create_dir_all(&p).unwrap();
|
||||||
|
p
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ente_basic_template_overridden_loads_as_ente_card() {
|
||||||
|
with_canonical_templates(|| {
|
||||||
|
let dir = unique_dir("ente");
|
||||||
|
let card_path = dir.join("my_ente.ncl");
|
||||||
|
fs::write(
|
||||||
|
&card_path,
|
||||||
|
r#"
|
||||||
|
let base = import "ente_basic.ncl" in
|
||||||
|
base & {
|
||||||
|
id = "01ARZ3NDEKTSV4RRFFQ69G5FAV",
|
||||||
|
label = "mi-ente",
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let card = load_card(&card_path).expect("load ente");
|
||||||
|
assert_eq!(card.id, "01ARZ3NDEKTSV4RRFFQ69G5FAV");
|
||||||
|
assert_eq!(card.label, "mi-ente");
|
||||||
|
assert_eq!(card.body.kind_name(), "ente");
|
||||||
|
match card.body {
|
||||||
|
CardBody::Ente(e) => {
|
||||||
|
assert_eq!(e.label, "mi-ente");
|
||||||
|
// Defaults del template intactos.
|
||||||
|
assert_eq!(e.schema_version, 1);
|
||||||
|
// Payload es el "Virtual" del template default.
|
||||||
|
assert!(
|
||||||
|
matches!(e.payload, brahman_card::Payload::Virtual),
|
||||||
|
"payload debería ser Virtual, got {:?}",
|
||||||
|
e.payload
|
||||||
|
);
|
||||||
|
}
|
||||||
|
other => panic!("variant inesperado: {:?}", other.kind_name()),
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::remove_dir_all(&dir).ok();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn monad_basic_template_overridden_loads_as_monad_card() {
|
||||||
|
with_canonical_templates(|| {
|
||||||
|
let dir = unique_dir("monad");
|
||||||
|
let card_path = dir.join("my_monad.ncl");
|
||||||
|
fs::write(
|
||||||
|
&card_path,
|
||||||
|
r#"
|
||||||
|
let base = import "monad_basic.ncl" in
|
||||||
|
base & {
|
||||||
|
id = "01ARZ3NDEKTSV4RRFFQ69G5FAW",
|
||||||
|
label = "fotos-2026",
|
||||||
|
cardinality = 5,
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let card = load_card(&card_path).expect("load monad");
|
||||||
|
assert_eq!(card.id, "01ARZ3NDEKTSV4RRFFQ69G5FAW");
|
||||||
|
assert_eq!(card.label, "fotos-2026");
|
||||||
|
assert_eq!(card.body.kind_name(), "monad");
|
||||||
|
match card.body {
|
||||||
|
CardBody::Monad(m) => {
|
||||||
|
assert_eq!(m.label, "fotos-2026");
|
||||||
|
assert_eq!(m.cardinality, 5);
|
||||||
|
// Defaults del template intactos.
|
||||||
|
assert_eq!(m.schema_version, 1);
|
||||||
|
assert!(m.members.is_empty());
|
||||||
|
assert!(m.summary.is_empty());
|
||||||
|
}
|
||||||
|
other => panic!("variant inesperado: {:?}", other.kind_name()),
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::remove_dir_all(&dir).ok();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ui_module_basic_template_overridden_loads_as_ui_module_card() {
|
||||||
|
with_canonical_templates(|| {
|
||||||
|
let dir = unique_dir("ui");
|
||||||
|
let card_path = dir.join("my_module.ncl");
|
||||||
|
fs::write(
|
||||||
|
&card_path,
|
||||||
|
r#"
|
||||||
|
let base = import "ui_module_basic.ncl" in
|
||||||
|
base & {
|
||||||
|
id = "customers",
|
||||||
|
label = "Clientes",
|
||||||
|
menu = [{ label = "Lista", view = "list" }],
|
||||||
|
views = {
|
||||||
|
list = {
|
||||||
|
kind = "list",
|
||||||
|
title = "Customers",
|
||||||
|
entity = "Customer",
|
||||||
|
columns = [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let card = load_card(&card_path).expect("load ui_module");
|
||||||
|
assert_eq!(card.id, "customers");
|
||||||
|
assert_eq!(card.label, "Clientes");
|
||||||
|
assert_eq!(card.body.kind_name(), "ui_module");
|
||||||
|
match card.body {
|
||||||
|
CardBody::UiModule(m) => {
|
||||||
|
assert_eq!(m.id, "customers");
|
||||||
|
assert_eq!(m.menu.len(), 1);
|
||||||
|
assert!(m.views.contains_key("list"));
|
||||||
|
// Defaults del template: entities vacío.
|
||||||
|
assert!(m.entities.is_empty());
|
||||||
|
}
|
||||||
|
other => panic!("variant inesperado: {:?}", other.kind_name()),
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::remove_dir_all(&dir).ok();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn template_default_id_and_label_pass_through_when_not_overridden() {
|
||||||
|
// Sanity: si el usuario importa el template SIN override de
|
||||||
|
// id/label, los defaults `"TEMPLATE_ID"` y `"TEMPLATE_LABEL"`
|
||||||
|
// pasan al wrapper Card.id/label. El brazo no falla — sólo
|
||||||
|
// los muestra como están. Validar este flow garantiza que un
|
||||||
|
// user "vacío" (importa y no override) carga sin error.
|
||||||
|
with_canonical_templates(|| {
|
||||||
|
let dir = unique_dir("defaults");
|
||||||
|
let card_path = dir.join("noop.ncl");
|
||||||
|
fs::write(&card_path, r#"import "ui_module_basic.ncl""#).unwrap();
|
||||||
|
|
||||||
|
let card = load_card(&card_path).expect("load defaults");
|
||||||
|
assert_eq!(card.id, "TEMPLATE_ID");
|
||||||
|
assert_eq!(card.label, "TEMPLATE_LABEL");
|
||||||
|
|
||||||
|
fs::remove_dir_all(&dir).ok();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn canonical_templates_dir_actually_exists() {
|
||||||
|
// Sanity: el path expuesto por canonical_templates_dir tiene
|
||||||
|
// que apuntar a un directorio que existe físicamente, sino los
|
||||||
|
// tests anteriores fallarían silenciosamente (Nickel reporta
|
||||||
|
// import-not-found pero el test ya estaría roto).
|
||||||
|
let d = canonical_templates_dir();
|
||||||
|
assert!(d.is_dir(), "templates dir no existe: {}", d.display());
|
||||||
|
for fname in ["ente_basic.ncl", "monad_basic.ncl", "ui_module_basic.ncl"] {
|
||||||
|
let p = d.join(fname);
|
||||||
|
assert!(p.is_file(), "template missing: {}", p.display());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user