6104484498
Sigue de la Fase 1 (lift del schema). Ahora los helpers puros que cualquier widget renderer o backend ejecutor consume sobre el schema viven en yahweh-meta-runtime. Sin GPUI, sin nakui — usa cierres en lugar de traits para decoupling máximo. Crate nuevo crates/modules/ui_engine/libs/meta-runtime: - parse.rs: parse_field_value, infer_param_value, resolve_param_value. - delta.rs: compute_field_delta, compute_clear_fields. - refs.rs: validate_entity_refs(load: F, refs) con cierre Fn(&str, Uuid) -> Option<Value> en vez de trait Store. - format.rs: human_label_for_record, render_value, value_to_input_text, short_uuid. - 33 tests propios. nakui-ui: - Nueva dep yahweh-meta-runtime. - Borrado código local equivalente (~200 líneas) + 34 tests duplicados. - validate_entity_refs callsite usa cierre: validate_entity_refs(|e, id| store.load(e, id), &refs). - 14 tests runtime-específicos quedan (compact/snapshot/event-log/ morphism pipeline/load_ui_modules). Distribución tests: 48 → 14 nakui-ui; +33 yahweh-meta-runtime. Cada crate afectado builds + tests limpio individualmente. Workspace build full no completó esta corrida por OOM al compilar surrealdb-core (ambiental, no relacionado). Fase 2b pendiente: extraer render widgets (form/list/modal/ EntityRef selector) a yahweh — requiere diseñar MetaBackend trait. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
117 lines
4.2 KiB
Rust
117 lines
4.2 KiB
Rust
//! Helpers de presentación humana para records y values.
|
|
//!
|
|
//! Sin GPUI: devuelven `String`s. El widget renderer los wrap-ea
|
|
//! en `div().child(...)` o equivalente.
|
|
|
|
use serde_json::Value;
|
|
use uuid::Uuid;
|
|
|
|
/// Etiqueta humana para representar un record en el selector de
|
|
/// EntityRef. Heurística: prefiere campos comunes en este orden:
|
|
/// `name`, `label`, `title`, `sku`, `sku_id`. Fallback al UUID corto.
|
|
pub fn human_label_for_record(value: &Value, id: &Uuid) -> String {
|
|
for key in ["name", "label", "title", "sku", "sku_id"] {
|
|
if let Some(v) = value.get(key).and_then(Value::as_str) {
|
|
if !v.is_empty() {
|
|
return format!("{} ({})", v, short_uuid(id));
|
|
}
|
|
}
|
|
}
|
|
short_uuid(id)
|
|
}
|
|
|
|
/// Render legible de un `Value` arbitrario para mostrar en una celda
|
|
/// de lista. Strings van pelados; bools como ✓/✗; el resto via
|
|
/// `Display`.
|
|
pub fn render_value(v: Option<&Value>) -> String {
|
|
match v {
|
|
None | Some(Value::Null) => String::new(),
|
|
Some(Value::String(s)) => s.clone(),
|
|
Some(Value::Bool(b)) => if *b { "✓" } else { "✗" }.to_string(),
|
|
Some(Value::Number(n)) => n.to_string(),
|
|
Some(other) => other.to_string(),
|
|
}
|
|
}
|
|
|
|
/// Conversión inversa a `parse_field_value`: del JSON al texto raw
|
|
/// que un input puede tomar y volver a parsearse igual al submit.
|
|
/// Usado para pre-llenar inputs en modo edit.
|
|
pub fn value_to_input_text(v: &Value) -> String {
|
|
match v {
|
|
Value::Null => String::new(),
|
|
Value::String(s) => s.clone(),
|
|
Value::Bool(b) => if *b { "true" } else { "false" }.to_string(),
|
|
Value::Number(n) => n.to_string(),
|
|
other => other.to_string(),
|
|
}
|
|
}
|
|
|
|
/// Primeros 8 chars del UUID en forma canónica. Útil para logs y UI
|
|
/// donde el UUID full es ruido visual.
|
|
pub fn short_uuid(id: &Uuid) -> String {
|
|
id.to_string().chars().take(8).collect()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use serde_json::json;
|
|
|
|
#[test]
|
|
fn human_label_prefers_name_over_id() {
|
|
let id = Uuid::new_v4();
|
|
let v = json!({"name": "Acme S.A.", "email": "x@y.z"});
|
|
let label = human_label_for_record(&v, &id);
|
|
assert!(label.starts_with("Acme S.A."));
|
|
assert!(label.contains(&short_uuid(&id)));
|
|
}
|
|
|
|
#[test]
|
|
fn human_label_falls_back_through_label_title_sku() {
|
|
let id = Uuid::new_v4();
|
|
let only_label = json!({"label": "X"});
|
|
assert!(human_label_for_record(&only_label, &id).starts_with("X "));
|
|
let only_title = json!({"title": "Y"});
|
|
assert!(human_label_for_record(&only_title, &id).starts_with("Y "));
|
|
let only_sku = json!({"sku": "Z"});
|
|
assert!(human_label_for_record(&only_sku, &id).starts_with("Z "));
|
|
let only_sku_id = json!({"sku_id": "W"});
|
|
assert!(human_label_for_record(&only_sku_id, &id).starts_with("W "));
|
|
}
|
|
|
|
#[test]
|
|
fn human_label_falls_back_to_short_uuid_when_no_keys_match() {
|
|
let id = Uuid::new_v4();
|
|
let v = json!({"random": "field"});
|
|
assert_eq!(human_label_for_record(&v, &id), short_uuid(&id));
|
|
}
|
|
|
|
#[test]
|
|
fn render_value_handles_basic_kinds() {
|
|
assert_eq!(render_value(None), "");
|
|
assert_eq!(render_value(Some(&Value::Null)), "");
|
|
assert_eq!(render_value(Some(&json!("hola"))), "hola");
|
|
assert_eq!(render_value(Some(&json!(true))), "✓");
|
|
assert_eq!(render_value(Some(&json!(false))), "✗");
|
|
assert_eq!(render_value(Some(&json!(42))), "42");
|
|
}
|
|
|
|
#[test]
|
|
fn value_to_input_text_round_trip_with_strings_and_numbers() {
|
|
assert_eq!(value_to_input_text(&Value::Null), "");
|
|
assert_eq!(value_to_input_text(&json!("x")), "x");
|
|
assert_eq!(value_to_input_text(&json!(true)), "true");
|
|
assert_eq!(value_to_input_text(&json!(false)), "false");
|
|
assert_eq!(value_to_input_text(&json!(42)), "42");
|
|
}
|
|
|
|
#[test]
|
|
fn short_uuid_returns_first_8_chars() {
|
|
let id = Uuid::parse_str("01ARZ3ND-EKTS-V4RR-FFQ6-9G5FAV000000").ok();
|
|
// Si el parse falla, usamos uno fresco — el invariant es la
|
|
// longitud, no el contenido.
|
|
let id = id.unwrap_or_else(Uuid::new_v4);
|
|
assert_eq!(short_uuid(&id).len(), 8);
|
|
}
|
|
}
|