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:
Sergio
2026-05-09 23:32:47 +00:00
parent 2a4443790c
commit f6361bbdca
6 changed files with 449 additions and 10 deletions
+58
View File
@@ -256,3 +256,61 @@ fn dispatch_to_reader(
}
Err(CardLoadError::NoMatchingReader)
}
/// Filenames convencionales que [`load_cards_from_dir`] busca dentro
/// de cada subdir, en orden de preferencia. Si `card.ncl` existe se
/// usa ese; sino `card.json`; sino los aliases legacy `module.*`. Los
/// últimos dos son por compat con el layout actual de
/// `examples/nakui-modules/<id>/module.json`.
pub const DEFAULT_CARD_FILENAMES: &[&str] =
&["card.ncl", "card.json", "module.ncl", "module.json"];
/// Carga todas las Cards encontradas como subdirs inmediatos de
/// `dir`. Por cada subdir, busca los filenames convencionales (ver
/// [`DEFAULT_CARD_FILENAMES`]) y carga el primero que existe. Subdirs
/// sin ningún filename matching se skipean silenciosamente — permite
/// que un dir contenga subdirs auxiliares (assets, fixtures, etc.).
///
/// Devuelve las Cards en orden lexicográfico por subdir name (estable
/// across runs). NO ordena por `Card.id` — el caller decide si quiere
/// re-ordenar y/o dedupar.
///
/// Errores: cualquier I/O al leer el `dir` mismo, o cualquier
/// `CardLoadError` de un archivo encontrado (NO continúa tras el
/// primer fallo — fallo loud > corrupción silenciosa).
pub fn load_cards_from_dir(dir: impl AsRef<Path>) -> Result<Vec<Card>, CardLoadError> {
load_cards_from_dir_with(dir, DEFAULT_CARD_FILENAMES, &default_readers())
}
/// Variante de [`load_cards_from_dir`] con filenames y readers
/// custom. Útil para apps que quieren restringir formatos o usar un
/// nombre canónico distinto.
pub fn load_cards_from_dir_with(
dir: impl AsRef<Path>,
filenames: &[&str],
readers: &[Box<dyn CardReader>],
) -> Result<Vec<Card>, CardLoadError> {
let dir = dir.as_ref();
let mut subdir_paths: Vec<std::path::PathBuf> = std::fs::read_dir(dir)?
.flatten()
.filter_map(|e| {
let p = e.path();
if p.is_dir() { Some(p) } else { None }
})
.collect();
// Orden estable por subdir name — el output del brazo no debería
// depender del orden de read_dir (que varía por filesystem).
subdir_paths.sort();
let mut out: Vec<Card> = Vec::new();
for sub in subdir_paths {
for fname in filenames {
let candidate = sub.join(fname);
if candidate.exists() {
out.push(load_card_with(&candidate, readers)?);
break;
}
}
}
Ok(out)
}