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
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue