feat: Add FileWatcher and loadFromString

- FileWatcher: Observa archivo para detectar cambios via mtime
  - Intervalo configurable entre verificaciones
  - reset() y updateMtime() para control manual
- loadFromString(): Carga config desde string (para defaults embebidos)
- Tests para ambas funcionalidades

🤖 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-18 12:56:51 +01:00
parent 4ec8667853
commit ef45ce6934

View file

@ -806,6 +806,126 @@ const loadFn = load;
/// Funcion save renombrada para evitar conflicto con metodo
const saveFn = save;
// =============================================================================
// FILE WATCHER
// =============================================================================
/// Observa un archivo para detectar cambios (via mtime)
///
/// Uso tipico:
/// ```zig
/// var watcher = FileWatcher.init("config.txt", 1000); // check cada 1s
/// // En main loop:
/// if (watcher.checkForChanges()) {
/// // Recargar configuracion
/// }
/// ```
pub const FileWatcher = struct {
path: []const u8,
last_mtime: i128 = 0,
check_interval_ms: i64 = 1000,
last_check: i64 = 0,
/// Inicializa un FileWatcher para el path dado
/// @param path: ruta del archivo a observar
/// @param check_interval_ms: intervalo minimo entre verificaciones (default 1000ms)
pub fn init(path: []const u8, check_interval_ms: i64) FileWatcher {
return .{
.path = path,
.check_interval_ms = check_interval_ms,
};
}
/// Verifica si el archivo ha cambiado desde la ultima verificacion
/// Respeta el intervalo minimo entre verificaciones
/// @return true si el archivo cambio, false si no o si hubo error
pub fn checkForChanges(self: *FileWatcher) bool {
const now = std.time.milliTimestamp();
// Respetar intervalo minimo
if (now - self.last_check < self.check_interval_ms) {
return false;
}
self.last_check = now;
// Obtener mtime del archivo
const stat = std.fs.cwd().statFile(self.path) catch {
return false;
};
const file_mtime = stat.mtime;
// Primera vez - guardar mtime inicial
if (self.last_mtime == 0) {
self.last_mtime = file_mtime;
return false;
}
// Verificar si cambio
if (file_mtime != self.last_mtime) {
self.last_mtime = file_mtime;
return true;
}
return false;
}
/// Reinicia el watcher (olvida el mtime anterior)
pub fn reset(self: *FileWatcher) void {
self.last_mtime = 0;
self.last_check = 0;
}
/// Fuerza actualizar el mtime sin detectar cambio
/// Util despues de guardar el archivo
pub fn updateMtime(self: *FileWatcher) void {
const stat = std.fs.cwd().statFile(self.path) catch return;
self.last_mtime = stat.mtime;
}
};
// =============================================================================
// LOAD FROM STRING
// =============================================================================
/// Carga configuracion desde un string (util para defaults embebidos)
/// Similar a load() pero sin leer archivo
pub fn loadFromString(
comptime variables: []const ConfigVariable,
comptime ConfigType: type,
config: *ConfigType,
content: []const u8,
) void {
const EngineType = Engine(variables, ConfigType);
// Parsear linea por linea (igual que load())
var lines = std.mem.splitScalar(u8, content, '\n');
while (lines.next()) |line| {
const trimmed = std.mem.trim(u8, line, " \t\r");
if (trimmed.len == 0) continue;
if (trimmed[0] == '#') continue;
if (trimmed[0] != '@') continue;
// Buscar separador ':'
const colon_pos = std.mem.indexOf(u8, trimmed, ":") orelse continue;
const key = std.mem.trim(u8, trimmed[0..colon_pos], " \t");
const after_colon = trimmed[colon_pos + 1 ..];
// Quitar comentario inline
const comment_pos = findCommentStart(after_colon);
const value_with_spaces = if (comment_pos) |pos|
after_colon[0..pos]
else
after_colon;
const value = std.mem.trim(u8, value_with_spaces, " \t");
// Aplicar el valor
EngineType.set(config, key, value) catch {
continue;
};
}
}
// =============================================================================
// TESTS
// =============================================================================
@ -965,3 +1085,47 @@ test "ConfigResult toString" {
const color_result = ConfigResult{ .color = Color.rgb(255, 0, 128) };
try std.testing.expectEqualStrings("RGB(255,0,128)", color_result.toString(&buf));
}
test "FileWatcher init" {
const watcher = FileWatcher.init("test.txt", 500);
try std.testing.expectEqualStrings("test.txt", watcher.path);
try std.testing.expectEqual(@as(i64, 500), watcher.check_interval_ms);
try std.testing.expectEqual(@as(i128, 0), watcher.last_mtime);
}
test "loadFromString basic" {
const test_vars = [_]ConfigVariable{
.{
.name = "enabled",
.config_key = "@enabled",
.var_type = .boolean,
.default = "Si",
.description = "Test",
},
.{
.name = "count",
.config_key = "@count",
.var_type = .integer,
.default = "10",
.description = "Test",
},
};
const TestConfig = struct {
enabled: bool = true,
count: i32 = 10,
};
var config = TestConfig{};
const content =
\\# Comentario
\\@enabled: No
\\@count: 42 # inline comment
;
loadFromString(&test_vars, TestConfig, &config, content);
try std.testing.expectEqual(false, config.enabled);
try std.testing.expectEqual(@as(i32, 42), config.count);
}