feat(nakui-ui): migrar consumer al brazo brahman_cards::load_cards_from_dir
Primera consumer migration del brazo. nakui-ui ya no llama a nakui_ui_schema::load_modules_from_dir directamente; pasa por brahman_cards::load_cards_from_dir y extrae UiModule del CardBody. Beneficios concretos: - Soporta .ncl además de .json (templates Nickel + merge funcionan en cualquier subdir de modules). - Cards de otros body kinds (Ente/Monad) se skipean limpio con toast informativo, no rompen la carga. Cambios en brahman-cards: - Nuevo load_cards_from_dir(dir) + variante con readers/filenames custom. DEFAULT_CARD_FILENAMES = [card.ncl, card.json, module.ncl, module.json] (orden de prioridad). - 4 tests nuevos del helper. Cambios en nakui-ui: - Nueva dep brahman-cards. - Helper load_ui_modules(dir) -> (Vec<Module>, Vec<String>) envuelve el brazo, filtra a UiModule, aplica Module::validate(), detecta duplicate ids. - MetaUi::new usa el helper, emite toast con cards skipped si las hay. - 3 tests e2e nuevos. 26/26 brahman-cards verdes (+4). 48/48 nakui-ui verdes (+3). Workspace build verde. nakui_ui_schema::load_modules_from_dir queda intacto (sus tests lo usan + otros consumers futuros pueden preferirlo). Migración opt-in. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -268,6 +268,142 @@ fn extensions_field_starts_empty_in_v1() {
|
||||
assert_eq!(card.extensions, BTreeMap::new());
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// load_cards_from_dir (subdir walking)
|
||||
// ===========================================================================
|
||||
|
||||
#[test]
|
||||
fn load_cards_from_dir_walks_subdirs_and_finds_module_json() {
|
||||
let root = unique_dir("dir-walk");
|
||||
// Subdir A: tiene module.json (UiModule).
|
||||
let a = root.join("alpha");
|
||||
std::fs::create_dir(&a).unwrap();
|
||||
std::fs::write(
|
||||
a.join("module.json"),
|
||||
serde_json::to_vec_pretty(&json!({
|
||||
"id": "alpha",
|
||||
"label": "Alpha",
|
||||
"entities": [],
|
||||
"menu": [],
|
||||
"views": {}
|
||||
}))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
// Subdir B: tiene module.json (UiModule).
|
||||
let b = root.join("bravo");
|
||||
std::fs::create_dir(&b).unwrap();
|
||||
std::fs::write(
|
||||
b.join("module.json"),
|
||||
serde_json::to_vec_pretty(&json!({
|
||||
"id": "bravo",
|
||||
"label": "Bravo",
|
||||
"entities": [],
|
||||
"menu": [],
|
||||
"views": {}
|
||||
}))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
// Subdir C: NO tiene ninguno de los filenames convencionales —
|
||||
// se debe skipear sin error.
|
||||
let c = root.join("charlie");
|
||||
std::fs::create_dir(&c).unwrap();
|
||||
std::fs::write(c.join("readme.txt"), b"sin card aca").unwrap();
|
||||
|
||||
let cards = brahman_cards::load_cards_from_dir(&root).expect("ok");
|
||||
let ids: Vec<&str> = cards.iter().map(|c| c.id.as_str()).collect();
|
||||
assert_eq!(
|
||||
ids,
|
||||
vec!["alpha", "bravo"],
|
||||
"orden lexicográfico por subdir name"
|
||||
);
|
||||
for c in &cards {
|
||||
assert_eq!(c.body.kind_name(), "ui_module");
|
||||
}
|
||||
|
||||
std::fs::remove_dir_all(&root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_cards_from_dir_prefers_ncl_over_json_when_both_present() {
|
||||
let root = unique_dir("dir-prefer");
|
||||
let sub = root.join("only");
|
||||
std::fs::create_dir(&sub).unwrap();
|
||||
// Ambos archivos existen; el .ncl debería ganar.
|
||||
std::fs::write(
|
||||
sub.join("card.ncl"),
|
||||
r#"{ id = "from_ncl", label = "Ncl", entities = [], menu = [], views = {} }"#,
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(
|
||||
sub.join("card.json"),
|
||||
serde_json::to_vec(&json!({
|
||||
"id": "from_json",
|
||||
"label": "Json",
|
||||
"entities": [], "menu": [], "views": {}
|
||||
}))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let cards = brahman_cards::load_cards_from_dir(&root).expect("ok");
|
||||
assert_eq!(cards.len(), 1);
|
||||
assert_eq!(cards[0].id, "from_ncl", "card.ncl tiene prioridad");
|
||||
|
||||
std::fs::remove_dir_all(&root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_cards_from_dir_propagates_per_file_errors_loud() {
|
||||
let root = unique_dir("dir-error-loud");
|
||||
let sub = root.join("broken");
|
||||
std::fs::create_dir(&sub).unwrap();
|
||||
std::fs::write(sub.join("card.json"), b"{ this is not valid json").unwrap();
|
||||
|
||||
let err = brahman_cards::load_cards_from_dir(&root).unwrap_err();
|
||||
assert!(
|
||||
matches!(err, CardLoadError::JsonParse(_)),
|
||||
"el error de un file roto debe propagar fail-loud, got {err:?}"
|
||||
);
|
||||
|
||||
std::fs::remove_dir_all(&root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_cards_from_dir_with_custom_filenames() {
|
||||
let root = unique_dir("dir-custom-fname");
|
||||
let sub = root.join("only");
|
||||
std::fs::create_dir(&sub).unwrap();
|
||||
// Filename custom que NO está en el default.
|
||||
std::fs::write(
|
||||
sub.join("manifest.json"),
|
||||
serde_json::to_vec(&json!({
|
||||
"id": "x",
|
||||
"label": "X",
|
||||
"entities": [], "menu": [], "views": {}
|
||||
}))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Default no encuentra nada (skipea):
|
||||
let with_default = brahman_cards::load_cards_from_dir(&root).unwrap();
|
||||
assert_eq!(with_default.len(), 0, "default filenames no incluye manifest.json");
|
||||
|
||||
// Custom filename encuentra:
|
||||
let with_custom = brahman_cards::load_cards_from_dir_with(
|
||||
&root,
|
||||
&["manifest.json"],
|
||||
&brahman_cards::default_readers(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(with_custom.len(), 1);
|
||||
assert_eq!(with_custom[0].id, "x");
|
||||
|
||||
std::fs::remove_dir_all(&root).ok();
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Helpers de tests
|
||||
// ===========================================================================
|
||||
@@ -281,3 +417,18 @@ fn tempfile_path(name: &str) -> std::path::PathBuf {
|
||||
));
|
||||
p
|
||||
}
|
||||
|
||||
fn unique_dir(name: &str) -> std::path::PathBuf {
|
||||
let mut p = std::env::temp_dir();
|
||||
p.push(format!(
|
||||
"brahman-cards-test-{}-{}-{}",
|
||||
std::process::id(),
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_nanos())
|
||||
.unwrap_or(0),
|
||||
name
|
||||
));
|
||||
std::fs::create_dir_all(&p).unwrap();
|
||||
p
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user