diff --git a/types_registry.cpp b/types_registry.cpp index 16ccff1..ac24545 100644 --- a/types_registry.cpp +++ b/types_registry.cpp @@ -101,6 +101,74 @@ uint8_t parse_style(const std::string& v) { return EDGE_USE_TYPE; } +// ---------------------------------------------------------------------------- +// Inline-map parser: { key: value, key: [a, b, c], key: value } +// ---------------------------------------------------------------------------- + +// Split a string by ',' respecting [...] brackets. Devuelve los segmentos +// trimeados. +std::vector split_top_level_commas(const std::string& s) { + std::vector out; + int depth = 0; + std::string cur; + for (char c : s) { + if (c == '[' || c == '{') ++depth; + else if (c == ']' || c == '}') --depth; + if (c == ',' && depth == 0) { + out.push_back(trim(cur)); + cur.clear(); + } else { + cur.push_back(c); + } + } + std::string t = trim(cur); + if (!t.empty()) out.push_back(t); + return out; +} + +// Parse "[a, b, c]" → {"a","b","c"}. Soporta strings sin/con comillas. +std::vector parse_inline_array(const std::string& s) { + std::vector out; + std::string body = trim(s); + if (body.size() < 2 || body.front() != '[' || body.back() != ']') return out; + body = body.substr(1, body.size() - 2); + for (auto& tok : split_top_level_commas(body)) { + std::string v = strip_quotes(trim(tok)); + if (!v.empty()) out.push_back(v); + } + return out; +} + +// Parse "{ name: x, type: y, required: true, values: [a,b,c] }" → FieldSpec. +// Devuelve true si parseo algo (al menos un name no vacio). +bool parse_field_inline_map(const std::string& raw, FieldSpec* out) { + if (!out) return false; + std::string body = trim(raw); + if (body.size() < 2 || body.front() != '{' || body.back() != '}') return false; + body = body.substr(1, body.size() - 2); + + *out = FieldSpec{}; + for (auto& pair : split_top_level_commas(body)) { + auto colon = pair.find(':'); + if (colon == std::string::npos) continue; + std::string k = trim(pair.substr(0, colon)); + std::string v = trim(pair.substr(colon + 1)); + if (k == "name") { + out->name = strip_quotes(v); + } else if (k == "type") { + out->kind = field_kind_from_name(strip_quotes(v).c_str()); + } else if (k == "required") { + std::string lv = strip_quotes(v); + std::transform(lv.begin(), lv.end(), lv.begin(), + [](unsigned char c) { return std::tolower(c); }); + out->required = (lv == "true" || lv == "1" || lv == "yes"); + } else if (k == "values") { + out->enum_values = parse_inline_array(v); + } + } + return !out->name.empty(); +} + // Cuenta el numero de espacios al principio de una linea. Tabs cuentan como // 4 espacios para que el archivo siga siendo legible si alguien usa tabs. int leading_indent(const std::string& line) { @@ -120,6 +188,36 @@ bool starts_with(const std::string& s, const char* p) { } // namespace +const char* field_kind_name(FieldKind k) { + switch (k) { + case FK_STRING: return "string"; + case FK_INT: return "int"; + case FK_FLOAT: return "float"; + case FK_BOOL: return "bool"; + case FK_DATE: return "date"; + case FK_URL: return "url"; + case FK_ENUM: return "enum"; + } + return "string"; +} + +FieldKind field_kind_from_name(const char* s) { + if (!s || !*s) return FK_STRING; + std::string v = s; + std::transform(v.begin(), v.end(), v.begin(), + [](unsigned char c) { return std::tolower(c); }); + if (v == "string" || v == "str" || v == "text") return FK_STRING; + if (v == "int" || v == "integer") return FK_INT; + if (v == "float" || v == "double") return FK_FLOAT; + if (v == "bool" || v == "boolean") return FK_BOOL; + if (v == "date") return FK_DATE; + if (v == "url" || v == "uri") return FK_URL; + if (v == "enum") return FK_ENUM; + std::fprintf(stderr, + "[types.yaml] tipo de campo desconocido '%s' → tratado como string\n", s); + return FK_STRING; +} + bool types_load_yaml(const char* path, ParsedTypes* out, std::string* error_msg) { if (!out) return false; std::ifstream f(path); @@ -133,7 +231,9 @@ bool types_load_yaml(const char* path, ParsedTypes* out, std::string* error_msg) EntitySpec cur_entity; RelationSpec cur_rel; - bool have_item = false; + bool have_item = false; + int item_indent = -1; // indent de "- " que abre el item + bool in_fields = false; // dentro de `fields:` del entity actual auto flush = [&]() { if (!have_item) return; @@ -145,6 +245,46 @@ bool types_load_yaml(const char* path, ParsedTypes* out, std::string* error_msg) cur_entity = EntitySpec{}; cur_rel = RelationSpec{}; have_item = false; + in_fields = false; + item_indent = -1; + }; + + // Aplica un sub-key "key: value" al item actual. Si es `fields` con + // valor vacio, activa modo fields. + auto apply_subkey = [&](const std::string& k, const std::string& v) { + if (k == "name") { + if (section == SEC_ENTITIES) cur_entity.name = strip_quotes(v); + else cur_rel.name = strip_quotes(v); + } else if (k == "color") { + uint32_t c = parse_color_hex(v); + if (section == SEC_ENTITIES) cur_entity.color = c; + else cur_rel.color = c; + } else if (k == "shape" && section == SEC_ENTITIES) { + cur_entity.shape = parse_shape(v); + } else if (k == "icon" && section == SEC_ENTITIES) { + std::string nm = strip_quotes(v); + cur_entity.icon_name = nm; + cur_entity.icon_cp = tabler_codepoint_by_name(nm.c_str()); + } else if (k == "principal_field" && section == SEC_ENTITIES) { + cur_entity.principal_field = strip_quotes(v); + } else if (k == "fields" && section == SEC_ENTITIES) { + // Sub-seccion: si el valor es vacio entramos en modo fields y + // las siguientes lineas "- { ... }" se parsean como FieldSpec. + // Si trae lista inline ("[...]") la parseamos aqui (raro pero + // soportado). + std::string vv = trim(v); + if (vv.empty()) { + in_fields = true; + } else if (!vv.empty() && vv.front() == '[') { + // No soportado en v1: lista inline de inline-maps en una + // misma linea. Se ignora con warning. + std::fprintf(stderr, + "[types.yaml] fields: en linea con [...] no soportado;" + " usar lista multilinea con `- { ... }`\n"); + } + } else if (k == "style" && section == SEC_RELATIONS) { + cur_rel.style = parse_style(v); + } }; std::string line; @@ -186,32 +326,37 @@ bool types_load_yaml(const char* path, ParsedTypes* out, std::string* error_msg) if (section == SEC_NONE) continue; - // Inicio de item: "- name: Foo" o "-" - if (starts_with(trimmed, "- ")) { - flush(); - have_item = true; + // Item nuevo a nivel del seccion (indent <= item_indent o sin item): + // disambigua "- " entre apertura de item y entrada de fields. + bool is_dash = starts_with(trimmed, "- "); + bool is_field_line = in_fields && is_dash && indent > item_indent; + bool is_new_item = is_dash && !is_field_line; + + if (is_field_line) { + // Parse "- { ... }" como FieldSpec + std::string body = trim(trimmed.substr(2)); + FieldSpec fs; + if (parse_field_inline_map(body, &fs)) { + cur_entity.fields.push_back(std::move(fs)); + } else { + std::fprintf(stderr, + "[types.yaml] field invalido (esperado inline map " + "{ name: ..., type: ... }): %s\n", body.c_str()); + } + continue; + } + + if (is_new_item) { + flush(); + have_item = true; + item_indent = indent; + in_fields = false; std::string body = trim(trimmed.substr(2)); - // Si la linea trae un par key:value tras el guion, parsearlo aqui auto colon = body.find(':'); if (colon != std::string::npos) { std::string k = trim(body.substr(0, colon)); std::string v = trim(body.substr(colon + 1)); - if (k == "name") { - if (section == SEC_ENTITIES) cur_entity.name = strip_quotes(v); - else cur_rel.name = strip_quotes(v); - } - // Cualquier otra key inline se reasigna mas abajo si hay sub-lineas - else if (k == "color") { - uint32_t c = parse_color_hex(v); - if (section == SEC_ENTITIES) cur_entity.color = c; - else cur_rel.color = c; - } else if (k == "shape" && section == SEC_ENTITIES) { - cur_entity.shape = parse_shape(v); - } else if (k == "icon" && section == SEC_ENTITIES) { - cur_entity.icon_cp = tabler_codepoint_by_name(strip_quotes(v).c_str()); - } else if (k == "style" && section == SEC_RELATIONS) { - cur_rel.style = parse_style(v); - } + apply_subkey(k, v); } continue; } @@ -222,26 +367,117 @@ bool types_load_yaml(const char* path, ParsedTypes* out, std::string* error_msg) if (colon == std::string::npos) continue; std::string k = trim(trimmed.substr(0, colon)); std::string v = trim(trimmed.substr(colon + 1)); - if (k == "name") { - if (section == SEC_ENTITIES) cur_entity.name = strip_quotes(v); - else cur_rel.name = strip_quotes(v); - } else if (k == "color") { - uint32_t c = parse_color_hex(v); - if (section == SEC_ENTITIES) cur_entity.color = c; - else cur_rel.color = c; - } else if (k == "shape" && section == SEC_ENTITIES) { - cur_entity.shape = parse_shape(v); - } else if (k == "icon" && section == SEC_ENTITIES) { - cur_entity.icon_cp = tabler_codepoint_by_name(strip_quotes(v).c_str()); - } else if (k == "style" && section == SEC_RELATIONS) { - cur_rel.style = parse_style(v); + // Sub-key del entity con indent menor o igual al item: significa + // que salimos de in_fields (el indent de fields_items es mayor). + if (in_fields && indent <= item_indent + 2) { + in_fields = false; } + apply_subkey(k, v); } } flush(); return true; } +// ---------------------------------------------------------------------------- +// Writer: ParsedTypes -> types.yaml +// ---------------------------------------------------------------------------- + +namespace { + +// ABGR -> "#RRGGBB" (o "#RRGGBBAA" si alpha != 0xFF). Devuelve "" si color es 0. +std::string color_to_hex(uint32_t c) { + if (c == 0) return ""; + uint8_t r = (uint8_t)( c & 0xFF); + uint8_t g = (uint8_t)((c >> 8) & 0xFF); + uint8_t b = (uint8_t)((c >> 16) & 0xFF); + uint8_t a = (uint8_t)((c >> 24) & 0xFF); + char buf[16]; + if (a == 0xFF) { + std::snprintf(buf, sizeof(buf), "#%02X%02X%02X", r, g, b); + } else { + std::snprintf(buf, sizeof(buf), "#%02X%02X%02X%02X", r, g, b, a); + } + return buf; +} + +const char* style_name(uint8_t s) { + switch (s) { + case EDGE_SOLID: return "solid"; + case EDGE_DASHED: return "dashed"; + case EDGE_DOTTED: return "dotted"; + default: return ""; + } +} + +// Serializa un FieldSpec como inline-map: "{ name: X, type: Y, required: true, values: [a,b,c] }". +std::string serialize_field(const FieldSpec& f) { + std::string out = "{ name: "; + out += f.name; + out += ", type: "; + out += field_kind_name(f.kind); + if (f.required) out += ", required: true"; + if (f.kind == FK_ENUM && !f.enum_values.empty()) { + out += ", values: ["; + for (size_t i = 0; i < f.enum_values.size(); ++i) { + if (i) out += ", "; + out += f.enum_values[i]; + } + out += "]"; + } + out += " }"; + return out; +} + +} // namespace + +bool types_save_yaml(const char* path, const ParsedTypes& types, + std::string* error_msg) { + if (!path || !*path) { + if (error_msg) *error_msg = "path vacio"; + return false; + } + std::ofstream f(path); + if (!f.good()) { + if (error_msg) *error_msg = std::string("cannot open ") + path; + return false; + } + + f << "# types.yaml — autogenerado por graph_explorer Type Editor.\n"; + f << "# Editable a mano. Round-trip con types_load_yaml/types_save_yaml.\n"; + f << "# Todos los nodos son circulo, salvo \"Table\" que es cuadrado.\n"; + f << "# Tipos de campo: string | int | float | bool | date | url | enum.\n"; + f << "\n"; + + f << "entities:\n"; + for (const auto& e : types.entities) { + f << " - name: " << e.name << "\n"; + std::string col = color_to_hex(e.color); + if (!col.empty()) f << " color: \"" << col << "\"\n"; + if (!e.icon_name.empty()) f << " icon: " << e.icon_name << "\n"; + if (!e.principal_field.empty()) + f << " principal_field: " << e.principal_field << "\n"; + if (!e.fields.empty()) { + f << " fields:\n"; + for (const auto& fs : e.fields) { + f << " - " << serialize_field(fs) << "\n"; + } + } + f << "\n"; + } + + f << "relations:\n"; + for (const auto& r : types.relations) { + f << " - name: " << r.name << "\n"; + std::string col = color_to_hex(r.color); + if (!col.empty()) f << " color: \"" << col << "\"\n"; + const char* st = style_name(r.style); + if (st && *st) f << " style: " << st << "\n"; + f << "\n"; + } + return true; +} + // ---------------------------------------------------------------------------- // Mapeo nombre Tabler -> codepoint // ---------------------------------------------------------------------------- diff --git a/types_registry.h b/types_registry.h index 4e63102..ca0c4e2 100644 --- a/types_registry.h +++ b/types_registry.h @@ -9,13 +9,18 @@ namespace ge { -// Representacion en memoria de un `types.yaml` minimo: +// Representacion en memoria de un `types.yaml`: // // entities: // - name: Person // color: "#5B8DEF" -// shape: circle // icon: ti-user +// principal_field: name # opcional, default = "name" +// fields: +// - { name: name, type: string, required: true } +// - { name: first_name, type: string } +// - { name: age, type: int } +// - { name: gender, type: enum, values: [male, female, other] } // relations: // - name: owns // color: "#888888" @@ -24,11 +29,36 @@ namespace ge { // Campos no presentes quedan con sentinel (`0` para color, `SHAPE_USE_TYPE` // para shape, `EDGE_USE_TYPE` para style, codepoint = 0 para icon). +// Tipos de campo soportados en el schema. v1 — ampliable. Tipos desconocidos +// se mapean a FK_STRING con warning del parser. +enum FieldKind : uint8_t { + FK_STRING = 0, + FK_INT, + FK_FLOAT, + FK_BOOL, + FK_DATE, // YYYY-MM-DD (validacion blanda en el inspector) + FK_URL, + FK_ENUM, // requiere `values: [...]` +}; + +const char* field_kind_name(FieldKind k); +FieldKind field_kind_from_name(const char* s); // case-insensitive; default FK_STRING + +struct FieldSpec { + std::string name; + FieldKind kind = FK_STRING; + bool required = false; + std::vector enum_values; // solo si kind == FK_ENUM +}; + struct EntitySpec { std::string name; uint32_t color = 0; // ABGR; 0 = no override uint8_t shape = SHAPE_USE_TYPE; // 0 = no override uint16_t icon_cp = 0; // codepoint Tabler; 0 = sin icono + std::string icon_name; // nombre original ti-* (para round-trip) + std::string principal_field; // vacio = usar "name" + std::vector fields; // schema; vacio = sin schema }; struct RelationSpec { @@ -47,6 +77,14 @@ struct ParsedTypes { // no es reconocido se ignora silenciosamente. bool types_load_yaml(const char* path, ParsedTypes* out, std::string* error_msg); +// Writer de `types.yaml`. Escribe el formato compacto de `examples/types.yaml` +// (un dict por entity/relation, sub-keys indentadas, fields como inline-maps). +// Round-trip estable: load → save → load produce el mismo ParsedTypes. +// +// Devuelve true en exito. En fallo, `error_msg` describe el motivo. +bool types_save_yaml(const char* path, const ParsedTypes& types, + std::string* error_msg); + // Resuelve un nombre tipo `ti-user` -> codepoint Tabler. Devuelve 0 si no // existe. La tabla de mapeo vive dentro de la implementacion y solo cubre // los iconos comunes que el ejemplo OSINT usa; ampliable a demanda.