diff --git a/examples/types.yaml b/examples/types.yaml index 9c1d793..d41fdef 100644 --- a/examples/types.yaml +++ b/examples/types.yaml @@ -1,57 +1,126 @@ # Ejemplo OSINT — tipos comunes en investigacion de entidades. # Color como "#RRGGBB" (con o sin alpha "#RRGGBBAA"). -# Shapes: el campo `shape` se ignora — todos los nodos son circulo, salvo -# el tipo "Table" que es cuadrado (regla de forma aplicada en -# types_registry.cpp::apply_types_yaml). -# Iconos: nombres ti-* mapeados en types_registry.cpp::tabler_codepoint_by_name -# Estilos de relacion: solid | dashed | dotted +# Shapes: campo `shape` ignorado — todos los nodos son circulo, salvo +# el tipo "Table" que es cuadrado (regla en types_registry.cpp::apply_types_yaml). +# Iconos: nombres ti-* mapeados en tabler_codepoint_by_name. +# Fields: schema por tipo, formato inline-map. +# tipos: string | int | float | bool | date | url | enum +# enum requiere `values: [...]` +# `principal_field` (opcional) es el nombre del field que sirve de label +# visible en el grafo; default = "name". entities: - name: Person color: "#5B8DEF" icon: ti-user + principal_field: name + fields: + - { name: name, type: string, required: true } + - { name: first_name, type: string } + - { name: last_name, type: string } + - { name: age, type: int } + - { name: nationality, type: string } + - { name: birth_date, type: date } + - { name: gender, type: enum, values: [male, female, other] } + - { name: occupation, type: string } - name: Email color: "#58CA8C" icon: ti-mail + principal_field: address + fields: + - { name: address, type: string, required: true } + - { name: provider, type: string } + - { name: verified, type: bool } - name: Domain color: "#F4B860" icon: ti-world + principal_field: name + fields: + - { name: name, type: string, required: true } + - { name: registrar, type: string } + - { name: created_at, type: date } + - { name: expires_at, type: date } - name: Phone color: "#E36AC0" icon: ti-phone + principal_field: number + fields: + - { name: number, type: string, required: true } + - { name: country_code, type: string } + - { name: kind, type: enum, values: [mobile, landline, voip] } - name: Org color: "#C780E8" icon: ti-building + principal_field: name + fields: + - { name: name, type: string, required: true } + - { name: country, type: string } + - { name: kind, type: enum, values: [company, ngo, gov, other] } + - { name: website, type: url } - name: IBAN color: "#52CDF2" icon: ti-building-bank + principal_field: iban + fields: + - { name: iban, type: string, required: true } + - { name: country, type: string } + - { name: bank_name, type: string } - name: Account color: "#7FD3A0" icon: ti-id + principal_field: handle + fields: + - { name: handle, type: string, required: true } + - { name: platform, type: string } + - { name: url, type: url } + - { name: verified, type: bool } - name: Document color: "#C9C9C9" icon: ti-file + principal_field: title + fields: + - { name: title, type: string, required: true } + - { name: url, type: url } + - { name: kind, type: string } + - { name: created, type: date } - name: Address color: "#FFB870" icon: ti-map-pin + principal_field: line1 + fields: + - { name: line1, type: string, required: true } + - { name: city, type: string } + - { name: country, type: string } + - { name: postcode, type: string } - name: Url color: "#89E0FC" icon: ti-link + principal_field: url + fields: + - { name: url, type: url, required: true } + - { name: title, type: string } + - { name: domain, type: string } # Nodo tabla — cuadrado (regla de forma). Issue 0010: contenedor con # filas que son nodos del grafo. - name: Table color: "#0EA5E9" icon: ti-database + principal_field: name + fields: + - { name: name, type: string, required: true } + - { name: row_type, type: string } + - { name: columns, type: string } + - { name: expanded, type: bool } relations: - name: owns diff --git a/main.cpp b/main.cpp index 442ad87..c4ea31f 100644 --- a/main.cpp +++ b/main.cpp @@ -189,9 +189,17 @@ static bool load_input() { g_atlas_bound = false; if (g_atlas) { graph_icons_destroy(g_atlas); g_atlas = nullptr; } g_atlas = ge::build_icon_atlas(codepoints); + int total_fields = 0; + int with_schema = 0; + for (const auto& e : pt.entities) { + total_fields += (int)e.fields.size(); + if (!e.fields.empty()) ++with_schema; + } std::fprintf(stdout, - "[graph_explorer] types.yaml: %zu entities, %zu relations, %zu icons\n", - pt.entities.size(), pt.relations.size(), codepoints.size()); + "[graph_explorer] types.yaml: %zu entities (%d con schema, %d fields totales)," + " %zu relations, %zu icons\n", + pt.entities.size(), with_schema, total_fields, + pt.relations.size(), codepoints.size()); } } @@ -765,7 +773,75 @@ static void usage() { " graph_explorer --input operations \n" " graph_explorer --types \n" " graph_explorer --layout force|grid|circular|radial|hierarchical|fixed\n" - " graph_explorer --project \n"); + " graph_explorer --project \n" + " graph_explorer --test-types-yaml (load+save+reload smoke test)\n"); +} + +// Smoke test del parser+writer (issue 0005 round-trip): carga `path`, +// serializa a un temporal y vuelve a cargar. Compara campos clave de +// ParsedTypes. Devuelve exit code 0 si OK, 1 si discrepancia, 2 si error. +static int test_types_yaml_roundtrip(const char* path) { + ge::ParsedTypes pt1; + std::string err; + if (!ge::types_load_yaml(path, &pt1, &err)) { + std::fprintf(stderr, "[test] load1 fail: %s\n", err.c_str()); + return 2; + } + std::string tmp = std::string(path) + ".roundtrip.yaml"; + if (!ge::types_save_yaml(tmp.c_str(), pt1, &err)) { + std::fprintf(stderr, "[test] save fail: %s\n", err.c_str()); + return 2; + } + ge::ParsedTypes pt2; + if (!ge::types_load_yaml(tmp.c_str(), &pt2, &err)) { + std::fprintf(stderr, "[test] load2 fail: %s\n", err.c_str()); + return 2; + } + + auto cmp = [&]() -> bool { + if (pt1.entities.size() != pt2.entities.size()) return false; + if (pt1.relations.size() != pt2.relations.size()) return false; + for (size_t i = 0; i < pt1.entities.size(); ++i) { + const auto& a = pt1.entities[i]; + const auto& b = pt2.entities[i]; + if (a.name != b.name) return false; + if (a.color != b.color) return false; + if (a.icon_name != b.icon_name) return false; + if (a.principal_field != b.principal_field) return false; + if (a.fields.size() != b.fields.size()) return false; + for (size_t j = 0; j < a.fields.size(); ++j) { + const auto& fa = a.fields[j]; + const auto& fb = b.fields[j]; + if (fa.name != fb.name) return false; + if (fa.kind != fb.kind) return false; + if (fa.required != fb.required) return false; + if (fa.enum_values != fb.enum_values) return false; + } + } + for (size_t i = 0; i < pt1.relations.size(); ++i) { + const auto& a = pt1.relations[i]; + const auto& b = pt2.relations[i]; + if (a.name != b.name) return false; + if (a.color != b.color) return false; + if (a.style != b.style) return false; + } + return true; + }; + + int total_fields = 0; + for (const auto& e : pt1.entities) total_fields += (int)e.fields.size(); + + if (cmp()) { + std::fprintf(stdout, + "[test] PASS — %zu entities, %d fields, %zu relations (round-trip estable)\n", + pt1.entities.size(), total_fields, pt1.relations.size()); + std::remove(tmp.c_str()); + return 0; + } + std::fprintf(stderr, + "[test] FAIL — discrepancia tras round-trip. dump preservado en %s\n", + tmp.c_str()); + return 1; } int main(int argc, char** argv) { @@ -790,6 +866,8 @@ int main(int argc, char** argv) { g_layout_initial = argv[++i]; } else if (std::strcmp(a, "--project") == 0 && i + 1 < argc) { project_arg = argv[++i]; + } else if (std::strcmp(a, "--test-types-yaml") == 0 && i + 1 < argc) { + return test_types_yaml_roundtrip(argv[++i]); } else if (std::strcmp(a, "--help") == 0 || std::strcmp(a, "-h") == 0) { usage(); return 0; diff --git a/project_manager.cpp b/project_manager.cpp index 18a7851..51d4024 100644 --- a/project_manager.cpp +++ b/project_manager.cpp @@ -123,29 +123,33 @@ CREATE TABLE IF NOT EXISTS logs ( ); )SQL"; -// Semilla types.yaml minima si examples/types.yaml no se encuentra. +// Semilla types.yaml minima si examples/types.yaml no se encuentra. Incluye +// `fields` (issue 0005) y el tipo Table como cuadrado (issue 0010 preview). static const char* k_seed_types_yaml = R"YAML(# types.yaml — generado al crear el proyecto. Editable desde el Type Editor. # Todos los nodos son circulo, salvo "Table" que es cuadrado. -# Iconos: nombres ti-* mapeados en types_registry.cpp -# Estilos de relacion: solid | dashed | dotted +# Tipos de campo: string | int | float | bool | date | url | enum. entities: - name: Person color: "#5B8DEF" icon: ti-user + principal_field: name + fields: + - { name: name, type: string, required: true } + - { name: first_name, type: string } + - { name: last_name, type: string } - name: Email color: "#58CA8C" icon: ti-mail + principal_field: address + fields: + - { name: address, type: string, required: true } - name: Domain color: "#F4B860" icon: ti-world - - name: Phone - color: "#E36AC0" - icon: ti-phone - - name: Org color: "#C780E8" icon: ti-building @@ -154,10 +158,13 @@ entities: color: "#C9C9C9" icon: ti-file - # Nodo tabla — cuadrado (issue 0010). - name: Table color: "#0EA5E9" icon: ti-database + principal_field: name + fields: + - { name: name, type: string, required: true } + - { name: row_type, type: string } relations: - name: owns