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 <noreply@anthropic.com>
This commit is contained in:
reugenio 2025-12-17 11:37:28 +01:00
commit 15c7f7357e
5 changed files with 626 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
.zig-cache/
zig-out/

200
CLAUDE.md Normal file
View file

@ -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 |

29
build.zig Normal file
View file

@ -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;
}

14
build.zig.zon Normal file
View file

@ -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",
},
}

381
src/zcatconfig.zig Normal file
View file

@ -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);
}