commit 15c7f7357e10454440593a6d07c95a973084be2a Author: reugenio Date: Wed Dec 17 11:37:28 2025 +0100 feat: zcatconfig v0.1.0 - Sistema de configuracion declarativo Libreria para gestion de configuracion con: - Definicion declarativa de variables (ConfigVariable) - Engine generico con comptime (inline for + @field) - Persistencia a archivo texto legible - Validacion de valores (rangos, tipos) - Soporte: boolean, integer, float, string, color Extraido y generalizado de zsimifactu/src/config/ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3389c86 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.zig-cache/ +zig-out/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..80c6f36 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,200 @@ +# ZCATCONFIG - Sistema de Configuracion Declarativo + +> **IMPORTANTE PARA CLAUDE**: Lee la seccion "PROTOCOLO DE INICIO" antes de hacer cualquier cosa. + +--- + +## PROTOCOLO DE INICIO (LEER PRIMERO) + +### Paso 1: Leer normas del equipo +``` +/mnt/cello2/arno/re/recode/teamdocs/LAST_UPDATE.md +``` + +### Paso 2: Verificar estado del proyecto +```bash +cd /mnt/cello2/arno/re/recode/zig/zcatconfig +git status +git log --oneline -5 +PATH=/mnt/cello2/arno/re/recode/zig/zig-0.15.2/zig-x86_64-linux-0.15.2:$PATH zig build test +``` + +--- + +## INFORMACION DEL PROYECTO + +| Campo | Valor | +|-------|-------| +| **Nombre** | zcatconfig | +| **Version** | v0.1.0 | +| **Fecha inicio** | 2025-12-17 | +| **Estado** | EN DESARROLLO - Estructura inicial | +| **Lenguaje** | Zig 0.15.2 | +| **Dependencias** | Ninguna (Zig puro) | + +### Descripcion + +**zcatconfig** es una libreria para gestion de configuracion declarativa: +- Definicion de variables con metadatos (tipo, default, descripcion, categoria) +- Generacion automatica de struct Config via comptime +- Persistencia a archivo de texto legible +- Validacion de valores +- Sistema Get/Set generico con inline for + @field + +--- + +## ORIGEN: zsimifactu config/ + +Esta libreria extrae y generaliza el sistema de configuracion implementado en zsimifactu. + +### Archivos originales en zsimifactu: +``` +src/config/ +├── config.zig # Re-exports publicos +├── types.zig # ConfigVariable, ConfigVarType, Color +├── variables.zig # Definiciones declarativas (proyecto-especifico) +├── structures.zig # Config struct (proyecto-especifico) +├── engine.zig # Meta-engine Get/Set con validacion +└── persistence.zig # Load/Save archivo texto +``` + +### Que se extrae a zcatconfig: +- types.zig (completo) +- engine.zig (generalizado) +- persistence.zig (generalizado) + +### Que queda en el proyecto consumidor: +- variables.zig (definiciones especificas del proyecto) +- structures.zig (struct Config especifico) + +--- + +## ARQUITECTURA + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ PROYECTO CONSUMIDOR │ +│ │ +│ variables.zig: │ +│ pub const config_variables = [_]ConfigVariable{ │ +│ .{ .name = "auto_save", .var_type = .boolean, ... }, │ +│ .{ .name = "font_size", .var_type = .integer, ... }, │ +│ }; │ +│ │ +│ structures.zig: │ +│ pub const Config = struct { │ +│ auto_save: bool = true, │ +│ font_size: i32 = 14, │ +│ }; │ +├─────────────────────────────────────────────────────────────────┤ +│ ZCATCONFIG │ +│ │ +│ types.zig: │ +│ ConfigVariable, ConfigVarType, ConfigResult, Color │ +│ │ +│ engine.zig: │ +│ Engine(comptime variables, comptime ConfigStruct) │ +│ - get(), set(), getByName(), setByName() │ +│ - Validacion automatica │ +│ │ +│ persistence.zig: │ +│ - load(config, path, variables) │ +│ - save(config, path, variables) │ +│ - Formato: @variable_name: valor # comentario │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## API PROPUESTA + +### Uso basico + +```zig +const zcatconfig = @import("zcatconfig"); + +// En el proyecto consumidor: +const variables = @import("config/variables.zig"); +const Config = @import("config/structures.zig").Config; + +// Crear engine tipado +const Engine = zcatconfig.Engine(variables.config_variables, Config); + +// Uso +var config = Config{}; +Engine.load(&config, "app_config.txt") catch {}; +config.font_size = 16; +try Engine.save(&config, "app_config.txt"); +``` + +### Tipos de variables soportados + +| Tipo | Zig Type | Formato archivo | +|------|----------|-----------------| +| boolean | bool | Si/No | +| integer | i32 | 123 | +| float | f32 | 1.5 | +| string | []const u8 | texto libre | +| color | Color | RGB(r,g,b) | +| enum_tipo | enum | NombreVariante | + +--- + +## COMANDOS + +```bash +# Compilar +PATH=/mnt/cello2/arno/re/recode/zig/zig-0.15.2/zig-x86_64-linux-0.15.2:$PATH zig build + +# Tests +PATH=/mnt/cello2/arno/re/recode/zig/zig-0.15.2/zig-x86_64-linux-0.15.2:$PATH zig build test +``` + +--- + +## RUTAS + +```bash +# Este proyecto +/mnt/cello2/arno/re/recode/zig/zcatconfig/ + +# Proyecto origen (referencia) +/mnt/cello2/arno/re/recode/zig/zsimifactu/src/config/ + +# Documentacion equipo +/mnt/cello2/arno/re/recode/teamdocs/ +``` + +--- + +## PLAN DE TRABAJO + +### Fase 1: Estructura base +- [x] Crear proyecto (build.zig, CLAUDE.md) +- [ ] Extraer types.zig de zsimifactu +- [ ] Adaptar engine.zig (parametrizar variables y Config) +- [ ] Adaptar persistence.zig + +### Fase 2: Generalizacion +- [ ] Engine generico con comptime +- [ ] Tests unitarios +- [ ] Documentacion API + +### Fase 3: Integracion +- [ ] Integrar en zsimifactu como dependencia +- [ ] Verificar que zsimifactu funciona igual + +--- + +## EQUIPO + +- **Usuario (R.Eugenio)**: Desarrollador principal +- **Claude**: Asistente de programacion (Claude Code / Opus 4.5) + +--- + +## HISTORIAL + +| Fecha | Version | Cambios | +|-------|---------|---------| +| 2025-12-17 | v0.1.0 | Proyecto creado, estructura inicial | diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..450ad0c --- /dev/null +++ b/build.zig @@ -0,0 +1,29 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + // Library module + const lib_mod = b.addModule("zcatconfig", .{ + .root_source_file = b.path("src/zcatconfig.zig"), + .target = target, + .optimize = optimize, + }); + + // Tests + const lib_unit_tests = b.addTest(.{ + .root_module = b.createModule(.{ + .root_source_file = b.path("src/zcatconfig.zig"), + .target = target, + .optimize = optimize, + }), + }); + + const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests); + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_lib_unit_tests.step); + + // Prevent unused variable warning + _ = lib_mod; +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..2ad442d --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,14 @@ +.{ + .name = .zcatconfig, + .version = "0.1.0", + .minimum_zig_version = "0.15.0", + .fingerprint = 0x6211a84c80e77eb, + + .dependencies = .{}, + + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/src/zcatconfig.zig b/src/zcatconfig.zig new file mode 100644 index 0000000..da5e90f --- /dev/null +++ b/src/zcatconfig.zig @@ -0,0 +1,381 @@ +//! zcatconfig - Sistema de Configuracion Declarativo +//! +//! Libreria para gestion de configuracion con: +//! - Definicion declarativa de variables +//! - Generacion automatica de struct via comptime +//! - Persistencia a archivo de texto legible +//! - Validacion de valores +//! +//! ## Uso basico +//! +//! ```zig +//! const zcatconfig = @import("zcatconfig"); +//! const variables = @import("config/variables.zig"); +//! const Config = @import("config/structures.zig").Config; +//! +//! const Engine = zcatconfig.Engine(variables.config_variables, Config); +//! +//! var config = Config{}; +//! Engine.load(&config, "config.txt") catch {}; +//! config.font_size = 16; +//! try Engine.save(&config, "config.txt"); +//! ``` + +const std = @import("std"); + +// ============================================================================= +// TIPOS +// ============================================================================= + +/// Tipo de variable de configuracion +pub const ConfigVarType = enum { + boolean, + integer, + float, + string, + color, + // Extensible para enums custom +}; + +/// Categoria de variable (para UI de configuracion) +pub const ConfigCategory = enum { + general, + apariencia, + comportamiento, + avanzado, +}; + +/// Color RGB +pub const Color = struct { + r: u8 = 0, + g: u8 = 0, + b: u8 = 0, + + pub fn rgb(r: u8, g: u8, b: u8) Color { + return .{ .r = r, .g = g, .b = b }; + } + + pub fn format(self: Color, allocator: std.mem.Allocator) ![]u8 { + return std.fmt.allocPrint(allocator, "RGB({},{},{})", .{ self.r, self.g, self.b }); + } + + pub fn parse(str: []const u8) ?Color { + // Formato: RGB(r,g,b) + if (!std.mem.startsWith(u8, str, "RGB(")) return null; + if (!std.mem.endsWith(u8, str, ")")) return null; + + const inner = str[4 .. str.len - 1]; + var it = std.mem.splitScalar(u8, inner, ','); + + const r_str = it.next() orelse return null; + const g_str = it.next() orelse return null; + const b_str = it.next() orelse return null; + + const r = std.fmt.parseInt(u8, std.mem.trim(u8, r_str, " "), 10) catch return null; + const g = std.fmt.parseInt(u8, std.mem.trim(u8, g_str, " "), 10) catch return null; + const b = std.fmt.parseInt(u8, std.mem.trim(u8, b_str, " "), 10) catch return null; + + return Color.rgb(r, g, b); + } +}; + +/// Definicion de una variable de configuracion +pub const ConfigVariable = struct { + /// Nombre del campo en el struct Config + name: []const u8, + /// Clave en el archivo de configuracion (ej: "@auto_save") + config_key: []const u8, + /// Tipo de la variable + var_type: ConfigVarType, + /// Valor por defecto como string + default: []const u8, + /// Descripcion para documentacion/UI + description: []const u8, + /// Categoria para agrupar en UI + category: ConfigCategory = .general, + /// Valor minimo (para integer/float) + min: ?i32 = null, + /// Valor maximo (para integer/float) + max: ?i32 = null, +}; + +/// Resultado de una operacion get +pub const ConfigResult = union(enum) { + boolean: bool, + integer: i32, + float: f32, + string: []const u8, + color: Color, + not_found, + type_mismatch, +}; + +// ============================================================================= +// ENGINE +// ============================================================================= + +/// Crea un Engine de configuracion para un conjunto de variables y struct Config +pub fn Engine(comptime variables: []const ConfigVariable, comptime ConfigType: type) type { + return struct { + const Self = @This(); + + /// Obtiene el valor de una variable por nombre + pub fn get(config: *const ConfigType, name: []const u8) ConfigResult { + inline for (variables) |v| { + if (std.mem.eql(u8, v.name, name)) { + return getField(config, v); + } + } + return .not_found; + } + + /// Obtiene el valor de un campo segun su definicion + fn getField(config: *const ConfigType, comptime v: ConfigVariable) ConfigResult { + const value = @field(config.*, v.name); + return switch (v.var_type) { + .boolean => .{ .boolean = value }, + .integer => .{ .integer = value }, + .float => .{ .float = value }, + .string => .{ .string = value }, + .color => .{ .color = value }, + }; + } + + /// Establece el valor de una variable por nombre (desde string) + pub fn setFromString(config: *ConfigType, name: []const u8, str_value: []const u8) bool { + inline for (variables) |v| { + if (std.mem.eql(u8, v.name, name)) { + return setFieldFromString(config, v, str_value); + } + } + return false; + } + + /// Establece un campo desde string + fn setFieldFromString(config: *ConfigType, comptime v: ConfigVariable, str_value: []const u8) bool { + switch (v.var_type) { + .boolean => { + if (std.mem.eql(u8, str_value, "Si") or + std.mem.eql(u8, str_value, "si") or + std.mem.eql(u8, str_value, "true") or + std.mem.eql(u8, str_value, "1")) + { + @field(config.*, v.name) = true; + return true; + } else if (std.mem.eql(u8, str_value, "No") or + std.mem.eql(u8, str_value, "no") or + std.mem.eql(u8, str_value, "false") or + std.mem.eql(u8, str_value, "0")) + { + @field(config.*, v.name) = false; + return true; + } + return false; + }, + .integer => { + const val = std.fmt.parseInt(i32, str_value, 10) catch return false; + // Validar rango si existe + if (v.min) |min| { + if (val < min) return false; + } + if (v.max) |max| { + if (val > max) return false; + } + @field(config.*, v.name) = val; + return true; + }, + .float => { + const val = std.fmt.parseFloat(f32, str_value) catch return false; + @field(config.*, v.name) = val; + return true; + }, + .string => { + @field(config.*, v.name) = str_value; + return true; + }, + .color => { + if (Color.parse(str_value)) |c| { + @field(config.*, v.name) = c; + return true; + } + return false; + }, + } + } + + /// Lista de variables disponibles + pub fn getVariables() []const ConfigVariable { + return variables; + } + }; +} + +// ============================================================================= +// PERSISTENCE +// ============================================================================= + +/// Carga configuracion desde archivo +pub fn load( + comptime variables: []const ConfigVariable, + comptime ConfigType: type, + config: *ConfigType, + path: []const u8, +) !void { + const EngineType = Engine(variables, ConfigType); + + const file = try std.fs.cwd().openFile(path, .{}); + defer file.close(); + + var buf_reader = std.io.bufferedReader(file.reader()); + var reader = buf_reader.reader(); + + var line_buf: [1024]u8 = undefined; + while (reader.readUntilDelimiterOrEof(&line_buf, '\n')) |line_opt| { + const line = line_opt orelse break; + + // Ignorar comentarios y lineas vacias + const trimmed = std.mem.trim(u8, line, " \t\r"); + if (trimmed.len == 0) continue; + if (trimmed[0] == '#') continue; + + // Buscar separador ':' + if (std.mem.indexOf(u8, trimmed, ":")) |sep_idx| { + const key = std.mem.trim(u8, trimmed[0..sep_idx], " \t"); + var value = std.mem.trim(u8, trimmed[sep_idx + 1 ..], " \t"); + + // Quitar comentario inline + if (std.mem.indexOf(u8, value, "#")) |hash_idx| { + value = std.mem.trim(u8, value[0..hash_idx], " \t"); + } + + // Buscar variable por config_key + inline for (variables) |v| { + if (std.mem.eql(u8, key, v.config_key)) { + _ = EngineType.setFromString(config, v.name, value); + break; + } + } + } + } else |err| { + return err; + } +} + +/// Guarda configuracion a archivo +pub fn save( + comptime variables: []const ConfigVariable, + comptime ConfigType: type, + config: *const ConfigType, + allocator: std.mem.Allocator, + path: []const u8, +) !void { + const EngineType = Engine(variables, ConfigType); + + const file = try std.fs.cwd().createFile(path, .{}); + defer file.close(); + + var writer = file.writer(); + + // Header + try writer.writeAll("# Archivo de configuracion (autogenerado)\n"); + try writer.writeAll("# Formato: @variable: valor # descripcion\n\n"); + + var current_category: ?ConfigCategory = null; + + inline for (variables) |v| { + // Separador de categoria + if (current_category == null or current_category.? != v.category) { + current_category = v.category; + try writer.print("\n# === {} ===\n", .{@tagName(v.category)}); + } + + // Obtener valor actual + const result = EngineType.get(config, v.name); + const value_str = switch (result) { + .boolean => |b| if (b) "Si" else "No", + .integer => |i| blk: { + const buf = try allocator.alloc(u8, 16); + const len = std.fmt.formatInt(i, 10, .lower, .{}, buf) catch 0; + break :blk buf[0..len]; + }, + .float => |f| blk: { + const buf = try allocator.alloc(u8, 32); + const slice = std.fmt.bufPrint(buf, "{d:.2}", .{f}) catch ""; + break :blk slice; + }, + .string => |s| s, + .color => |c| blk: { + break :blk try c.format(allocator); + }, + else => "???", + }; + + try writer.print("{s}: {s} # {s}\n", .{ v.config_key, value_str, v.description }); + } +} + +// ============================================================================= +// TESTS +// ============================================================================= + +test "Color parse and format" { + const c = Color.parse("RGB(255,128,64)").?; + try std.testing.expectEqual(@as(u8, 255), c.r); + try std.testing.expectEqual(@as(u8, 128), c.g); + try std.testing.expectEqual(@as(u8, 64), c.b); +} + +test "Color parse invalid" { + try std.testing.expect(Color.parse("invalid") == null); + try std.testing.expect(Color.parse("RGB(256,0,0)") == null); // 256 > u8 +} + +test "Engine basic" { + const test_vars = [_]ConfigVariable{ + .{ + .name = "enabled", + .config_key = "@enabled", + .var_type = .boolean, + .default = "Si", + .description = "Activar funcionalidad", + }, + .{ + .name = "count", + .config_key = "@count", + .var_type = .integer, + .default = "10", + .description = "Contador", + .min = 0, + .max = 100, + }, + }; + + const TestConfig = struct { + enabled: bool = true, + count: i32 = 10, + }; + + const TestEngine = Engine(&test_vars, TestConfig); + + var config = TestConfig{}; + + // Test get + const enabled_result = TestEngine.get(&config, "enabled"); + try std.testing.expectEqual(true, enabled_result.boolean); + + // Test setFromString + try std.testing.expect(TestEngine.setFromString(&config, "enabled", "No")); + try std.testing.expectEqual(false, config.enabled); + + try std.testing.expect(TestEngine.setFromString(&config, "count", "42")); + try std.testing.expectEqual(@as(i32, 42), config.count); + + // Test range validation + try std.testing.expect(!TestEngine.setFromString(&config, "count", "200")); // > max + try std.testing.expectEqual(@as(i32, 42), config.count); // unchanged +} + +test "version" { + // Placeholder test + try std.testing.expect(true); +}