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:
parent
4ec8667853
commit
ef45ce6934
1 changed files with 164 additions and 0 deletions
|
|
@ -806,6 +806,126 @@ const loadFn = load;
|
||||||
/// Funcion save renombrada para evitar conflicto con metodo
|
/// Funcion save renombrada para evitar conflicto con metodo
|
||||||
const saveFn = save;
|
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
|
// TESTS
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
@ -965,3 +1085,47 @@ test "ConfigResult toString" {
|
||||||
const color_result = ConfigResult{ .color = Color.rgb(255, 0, 128) };
|
const color_result = ConfigResult{ .color = Color.rgb(255, 0, 128) };
|
||||||
try std.testing.expectEqualStrings("RGB(255,0,128)", color_result.toString(&buf));
|
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);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue