merge: issue/0005-type-schema-fields — schema de fields en types.yaml
Parser+writer para fields (string/int/float/bool/date/url/enum), principal_field y round-trip estable. Semilla con 44 fields en 11 tipos. CLI --test-types-yaml para verificar round-trip. Cierra issue 0005.
This commit is contained in:
+74
-5
@@ -1,57 +1,126 @@
|
|||||||
# Ejemplo OSINT — tipos comunes en investigacion de entidades.
|
# Ejemplo OSINT — tipos comunes en investigacion de entidades.
|
||||||
# Color como "#RRGGBB" (con o sin alpha "#RRGGBBAA").
|
# Color como "#RRGGBB" (con o sin alpha "#RRGGBBAA").
|
||||||
# Shapes: el campo `shape` se ignora — todos los nodos son circulo, salvo
|
# Shapes: campo `shape` ignorado — todos los nodos son circulo, salvo
|
||||||
# el tipo "Table" que es cuadrado (regla de forma aplicada en
|
# el tipo "Table" que es cuadrado (regla en types_registry.cpp::apply_types_yaml).
|
||||||
# types_registry.cpp::apply_types_yaml).
|
# Iconos: nombres ti-* mapeados en tabler_codepoint_by_name.
|
||||||
# Iconos: nombres ti-* mapeados en types_registry.cpp::tabler_codepoint_by_name
|
# Fields: schema por tipo, formato inline-map.
|
||||||
# Estilos de relacion: solid | dashed | dotted
|
# 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:
|
entities:
|
||||||
- name: Person
|
- name: Person
|
||||||
color: "#5B8DEF"
|
color: "#5B8DEF"
|
||||||
icon: ti-user
|
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
|
- name: Email
|
||||||
color: "#58CA8C"
|
color: "#58CA8C"
|
||||||
icon: ti-mail
|
icon: ti-mail
|
||||||
|
principal_field: address
|
||||||
|
fields:
|
||||||
|
- { name: address, type: string, required: true }
|
||||||
|
- { name: provider, type: string }
|
||||||
|
- { name: verified, type: bool }
|
||||||
|
|
||||||
- name: Domain
|
- name: Domain
|
||||||
color: "#F4B860"
|
color: "#F4B860"
|
||||||
icon: ti-world
|
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
|
- name: Phone
|
||||||
color: "#E36AC0"
|
color: "#E36AC0"
|
||||||
icon: ti-phone
|
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
|
- name: Org
|
||||||
color: "#C780E8"
|
color: "#C780E8"
|
||||||
icon: ti-building
|
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
|
- name: IBAN
|
||||||
color: "#52CDF2"
|
color: "#52CDF2"
|
||||||
icon: ti-building-bank
|
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
|
- name: Account
|
||||||
color: "#7FD3A0"
|
color: "#7FD3A0"
|
||||||
icon: ti-id
|
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
|
- name: Document
|
||||||
color: "#C9C9C9"
|
color: "#C9C9C9"
|
||||||
icon: ti-file
|
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
|
- name: Address
|
||||||
color: "#FFB870"
|
color: "#FFB870"
|
||||||
icon: ti-map-pin
|
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
|
- name: Url
|
||||||
color: "#89E0FC"
|
color: "#89E0FC"
|
||||||
icon: ti-link
|
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
|
# Nodo tabla — cuadrado (regla de forma). Issue 0010: contenedor con
|
||||||
# filas que son nodos del grafo.
|
# filas que son nodos del grafo.
|
||||||
- name: Table
|
- name: Table
|
||||||
color: "#0EA5E9"
|
color: "#0EA5E9"
|
||||||
icon: ti-database
|
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:
|
relations:
|
||||||
- name: owns
|
- name: owns
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
---
|
---
|
||||||
id: 0005
|
id: 0005
|
||||||
title: Type schema en types.yaml — fields por tipo + principal_field
|
title: Type schema en types.yaml — fields por tipo + principal_field
|
||||||
status: pending
|
status: completed
|
||||||
priority: high
|
priority: high
|
||||||
created: 2026-04-30
|
created: 2026-04-30
|
||||||
|
completed: 2026-05-01
|
||||||
---
|
---
|
||||||
|
|
||||||
## Objetivo
|
## Objetivo
|
||||||
@@ -189,9 +189,17 @@ static bool load_input() {
|
|||||||
g_atlas_bound = false;
|
g_atlas_bound = false;
|
||||||
if (g_atlas) { graph_icons_destroy(g_atlas); g_atlas = nullptr; }
|
if (g_atlas) { graph_icons_destroy(g_atlas); g_atlas = nullptr; }
|
||||||
g_atlas = ge::build_icon_atlas(codepoints);
|
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,
|
std::fprintf(stdout,
|
||||||
"[graph_explorer] types.yaml: %zu entities, %zu relations, %zu icons\n",
|
"[graph_explorer] types.yaml: %zu entities (%d con schema, %d fields totales),"
|
||||||
pt.entities.size(), pt.relations.size(), codepoints.size());
|
" %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 <path>\n"
|
" graph_explorer --input operations <path>\n"
|
||||||
" graph_explorer --types <types.yaml>\n"
|
" graph_explorer --types <types.yaml>\n"
|
||||||
" graph_explorer --layout force|grid|circular|radial|hierarchical|fixed\n"
|
" graph_explorer --layout force|grid|circular|radial|hierarchical|fixed\n"
|
||||||
" graph_explorer --project <slug>\n");
|
" graph_explorer --project <slug>\n"
|
||||||
|
" graph_explorer --test-types-yaml <path> (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) {
|
int main(int argc, char** argv) {
|
||||||
@@ -790,6 +866,8 @@ int main(int argc, char** argv) {
|
|||||||
g_layout_initial = argv[++i];
|
g_layout_initial = argv[++i];
|
||||||
} else if (std::strcmp(a, "--project") == 0 && i + 1 < argc) {
|
} else if (std::strcmp(a, "--project") == 0 && i + 1 < argc) {
|
||||||
project_arg = argv[++i];
|
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) {
|
} else if (std::strcmp(a, "--help") == 0 || std::strcmp(a, "-h") == 0) {
|
||||||
usage();
|
usage();
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
+15
-8
@@ -123,29 +123,33 @@ CREATE TABLE IF NOT EXISTS logs (
|
|||||||
);
|
);
|
||||||
)SQL";
|
)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.
|
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.
|
# Todos los nodos son circulo, salvo "Table" que es cuadrado.
|
||||||
# Iconos: nombres ti-* mapeados en types_registry.cpp
|
# Tipos de campo: string | int | float | bool | date | url | enum.
|
||||||
# Estilos de relacion: solid | dashed | dotted
|
|
||||||
|
|
||||||
entities:
|
entities:
|
||||||
- name: Person
|
- name: Person
|
||||||
color: "#5B8DEF"
|
color: "#5B8DEF"
|
||||||
icon: ti-user
|
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
|
- name: Email
|
||||||
color: "#58CA8C"
|
color: "#58CA8C"
|
||||||
icon: ti-mail
|
icon: ti-mail
|
||||||
|
principal_field: address
|
||||||
|
fields:
|
||||||
|
- { name: address, type: string, required: true }
|
||||||
|
|
||||||
- name: Domain
|
- name: Domain
|
||||||
color: "#F4B860"
|
color: "#F4B860"
|
||||||
icon: ti-world
|
icon: ti-world
|
||||||
|
|
||||||
- name: Phone
|
|
||||||
color: "#E36AC0"
|
|
||||||
icon: ti-phone
|
|
||||||
|
|
||||||
- name: Org
|
- name: Org
|
||||||
color: "#C780E8"
|
color: "#C780E8"
|
||||||
icon: ti-building
|
icon: ti-building
|
||||||
@@ -154,10 +158,13 @@ entities:
|
|||||||
color: "#C9C9C9"
|
color: "#C9C9C9"
|
||||||
icon: ti-file
|
icon: ti-file
|
||||||
|
|
||||||
# Nodo tabla — cuadrado (issue 0010).
|
|
||||||
- name: Table
|
- name: Table
|
||||||
color: "#0EA5E9"
|
color: "#0EA5E9"
|
||||||
icon: ti-database
|
icon: ti-database
|
||||||
|
principal_field: name
|
||||||
|
fields:
|
||||||
|
- { name: name, type: string, required: true }
|
||||||
|
- { name: row_type, type: string }
|
||||||
|
|
||||||
relations:
|
relations:
|
||||||
- name: owns
|
- name: owns
|
||||||
|
|||||||
+271
-35
@@ -101,6 +101,74 @@ uint8_t parse_style(const std::string& v) {
|
|||||||
return EDGE_USE_TYPE;
|
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<std::string> split_top_level_commas(const std::string& s) {
|
||||||
|
std::vector<std::string> 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<std::string> parse_inline_array(const std::string& s) {
|
||||||
|
std::vector<std::string> 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
|
// 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.
|
// 4 espacios para que el archivo siga siendo legible si alguien usa tabs.
|
||||||
int leading_indent(const std::string& line) {
|
int leading_indent(const std::string& line) {
|
||||||
@@ -120,6 +188,36 @@ bool starts_with(const std::string& s, const char* p) {
|
|||||||
|
|
||||||
} // namespace
|
} // 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) {
|
bool types_load_yaml(const char* path, ParsedTypes* out, std::string* error_msg) {
|
||||||
if (!out) return false;
|
if (!out) return false;
|
||||||
std::ifstream f(path);
|
std::ifstream f(path);
|
||||||
@@ -133,7 +231,9 @@ bool types_load_yaml(const char* path, ParsedTypes* out, std::string* error_msg)
|
|||||||
|
|
||||||
EntitySpec cur_entity;
|
EntitySpec cur_entity;
|
||||||
RelationSpec cur_rel;
|
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 = [&]() {
|
auto flush = [&]() {
|
||||||
if (!have_item) return;
|
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_entity = EntitySpec{};
|
||||||
cur_rel = RelationSpec{};
|
cur_rel = RelationSpec{};
|
||||||
have_item = false;
|
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;
|
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;
|
if (section == SEC_NONE) continue;
|
||||||
|
|
||||||
// Inicio de item: "- name: Foo" o "-"
|
// Item nuevo a nivel del seccion (indent <= item_indent o sin item):
|
||||||
if (starts_with(trimmed, "- ")) {
|
// disambigua "- " entre apertura de item y entrada de fields.
|
||||||
flush();
|
bool is_dash = starts_with(trimmed, "- ");
|
||||||
have_item = true;
|
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));
|
std::string body = trim(trimmed.substr(2));
|
||||||
// Si la linea trae un par key:value tras el guion, parsearlo aqui
|
|
||||||
auto colon = body.find(':');
|
auto colon = body.find(':');
|
||||||
if (colon != std::string::npos) {
|
if (colon != std::string::npos) {
|
||||||
std::string k = trim(body.substr(0, colon));
|
std::string k = trim(body.substr(0, colon));
|
||||||
std::string v = trim(body.substr(colon + 1));
|
std::string v = trim(body.substr(colon + 1));
|
||||||
if (k == "name") {
|
apply_subkey(k, v);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -222,26 +367,117 @@ bool types_load_yaml(const char* path, ParsedTypes* out, std::string* error_msg)
|
|||||||
if (colon == std::string::npos) continue;
|
if (colon == std::string::npos) continue;
|
||||||
std::string k = trim(trimmed.substr(0, colon));
|
std::string k = trim(trimmed.substr(0, colon));
|
||||||
std::string v = trim(trimmed.substr(colon + 1));
|
std::string v = trim(trimmed.substr(colon + 1));
|
||||||
if (k == "name") {
|
// Sub-key del entity con indent menor o igual al item: significa
|
||||||
if (section == SEC_ENTITIES) cur_entity.name = strip_quotes(v);
|
// que salimos de in_fields (el indent de fields_items es mayor).
|
||||||
else cur_rel.name = strip_quotes(v);
|
if (in_fields && indent <= item_indent + 2) {
|
||||||
} else if (k == "color") {
|
in_fields = false;
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
flush();
|
flush();
|
||||||
return true;
|
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
|
// Mapeo nombre Tabler -> codepoint
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|||||||
+40
-2
@@ -9,13 +9,18 @@
|
|||||||
|
|
||||||
namespace ge {
|
namespace ge {
|
||||||
|
|
||||||
// Representacion en memoria de un `types.yaml` minimo:
|
// Representacion en memoria de un `types.yaml`:
|
||||||
//
|
//
|
||||||
// entities:
|
// entities:
|
||||||
// - name: Person
|
// - name: Person
|
||||||
// color: "#5B8DEF"
|
// color: "#5B8DEF"
|
||||||
// shape: circle
|
|
||||||
// icon: ti-user
|
// 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:
|
// relations:
|
||||||
// - name: owns
|
// - name: owns
|
||||||
// color: "#888888"
|
// color: "#888888"
|
||||||
@@ -24,11 +29,36 @@ namespace ge {
|
|||||||
// Campos no presentes quedan con sentinel (`0` para color, `SHAPE_USE_TYPE`
|
// Campos no presentes quedan con sentinel (`0` para color, `SHAPE_USE_TYPE`
|
||||||
// para shape, `EDGE_USE_TYPE` para style, codepoint = 0 para icon).
|
// 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<std::string> enum_values; // solo si kind == FK_ENUM
|
||||||
|
};
|
||||||
|
|
||||||
struct EntitySpec {
|
struct EntitySpec {
|
||||||
std::string name;
|
std::string name;
|
||||||
uint32_t color = 0; // ABGR; 0 = no override
|
uint32_t color = 0; // ABGR; 0 = no override
|
||||||
uint8_t shape = SHAPE_USE_TYPE; // 0 = no override
|
uint8_t shape = SHAPE_USE_TYPE; // 0 = no override
|
||||||
uint16_t icon_cp = 0; // codepoint Tabler; 0 = sin icono
|
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<FieldSpec> fields; // schema; vacio = sin schema
|
||||||
};
|
};
|
||||||
|
|
||||||
struct RelationSpec {
|
struct RelationSpec {
|
||||||
@@ -47,6 +77,14 @@ struct ParsedTypes {
|
|||||||
// no es reconocido se ignora silenciosamente.
|
// no es reconocido se ignora silenciosamente.
|
||||||
bool types_load_yaml(const char* path, ParsedTypes* out, std::string* error_msg);
|
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
|
// 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
|
// existe. La tabla de mapeo vive dentro de la implementacion y solo cubre
|
||||||
// los iconos comunes que el ejemplo OSINT usa; ampliable a demanda.
|
// los iconos comunes que el ejemplo OSINT usa; ampliable a demanda.
|
||||||
|
|||||||
Reference in New Issue
Block a user