//! DataProvider - Interface genérica para fuentes de datos //! //! Permite que VirtualAdvancedTable trabaje con cualquier fuente de datos //! (SQLite, arrays, APIs, etc.) mediante un vtable pattern. //! //! Ejemplo de implementación: //! ```zig //! const WhoDataProvider = struct { //! dm: *DataManager, //! //! pub fn toDataProvider(self: *WhoDataProvider) DataProvider { //! return DataProvider.init(self, &vtable); //! } //! //! const vtable = DataProvider.VTable{ //! .fetchWindow = fetchWindow, //! .getTotalCount = getTotalCount, //! // ... //! }; //! //! fn fetchWindow(ptr: *anyopaque, offset: usize, limit: usize) ![]const RowData { //! const self: *WhoDataProvider = @ptrCast(@alignCast(ptr)); //! // Query BD con LIMIT/OFFSET //! } //! }; //! ``` const std = @import("std"); const types = @import("types.zig"); const RowData = types.RowData; const CountInfo = types.CountInfo; const SortDirection = types.SortDirection; /// Interface genérica para proveedores de datos pub const DataProvider = struct { ptr: *anyopaque, vtable: *const VTable, pub const VTable = struct { /// Obtiene una ventana de datos /// Los datos retornados son válidos hasta el próximo fetchWindow /// @param offset: índice del primer registro /// @param limit: número máximo de registros a retornar /// @return slice de RowData (propiedad del provider, no liberar) fetchWindow: *const fn ( ptr: *anyopaque, offset: usize, limit: usize, ) error{OutOfMemory}![]const RowData, /// Obtiene información del conteo total /// Puede retornar estado parcial mientras calcula getTotalCount: *const fn (ptr: *anyopaque) CountInfo, /// Obtiene información del conteo filtrado /// Puede retornar estado parcial mientras calcula getFilteredCount: *const fn (ptr: *anyopaque) CountInfo, /// Aplica filtro de búsqueda /// @param filter: texto a buscar (vacío = sin filtro) setFilter: *const fn (ptr: *anyopaque, filter: []const u8) void, /// Aplica ordenación /// @param column: nombre de columna (de ColumnDef.name) /// @param direction: dirección de ordenación setSort: *const fn ( ptr: *anyopaque, column: []const u8, direction: SortDirection, ) void, /// Obtiene el ID del registro en un índice global /// Útil para mantener selección después de scroll /// @param index: índice global (no offset de ventana) /// @return ID del registro o null si fuera de rango getRowId: *const fn (ptr: *anyopaque, index: usize) ?i64, /// Procesa tareas en background (conteos diferidos, etc.) /// Llamar desde el main loop cuando hay tiempo idle /// @return true si hay más trabajo pendiente processBackgroundTasks: *const fn (ptr: *anyopaque) bool, /// Invalida caché y fuerza recarga invalidate: *const fn (ptr: *anyopaque) void, /// Libera recursos del provider deinit: *const fn (ptr: *anyopaque) void, }; /// Crea un DataProvider desde una implementación concreta pub fn init(ptr: anytype, vtable: *const VTable) DataProvider { const Ptr = @TypeOf(ptr); const ptr_info = @typeInfo(Ptr); if (ptr_info != .pointer) { @compileError("ptr must be a pointer"); } return .{ .ptr = @ptrCast(ptr), .vtable = vtable, }; } // ========================================================================= // Métodos wrapper para facilitar uso // ========================================================================= /// Obtiene una ventana de datos pub fn fetchWindow(self: DataProvider, offset: usize, limit: usize) ![]const RowData { return self.vtable.fetchWindow(self.ptr, offset, limit); } /// Obtiene información del conteo total pub fn getTotalCount(self: DataProvider) CountInfo { return self.vtable.getTotalCount(self.ptr); } /// Obtiene información del conteo filtrado pub fn getFilteredCount(self: DataProvider) CountInfo { return self.vtable.getFilteredCount(self.ptr); } /// Aplica filtro de búsqueda pub fn setFilter(self: DataProvider, filter: []const u8) void { self.vtable.setFilter(self.ptr, filter); } /// Aplica ordenación pub fn setSort(self: DataProvider, column: []const u8, direction: SortDirection) void { self.vtable.setSort(self.ptr, column, direction); } /// Obtiene el ID del registro en un índice pub fn getRowId(self: DataProvider, index: usize) ?i64 { return self.vtable.getRowId(self.ptr, index); } /// Procesa tareas en background pub fn processBackgroundTasks(self: DataProvider) bool { return self.vtable.processBackgroundTasks(self.ptr); } /// Invalida caché pub fn invalidate(self: DataProvider) void { self.vtable.invalidate(self.ptr); } /// Libera recursos pub fn deinit(self: DataProvider) void { self.vtable.deinit(self.ptr); } }; // ============================================================================= // Tests // ============================================================================= const testing = std.testing; /// Provider de prueba para tests const MockProvider = struct { data: []const RowData, total: usize, filter_applied: bool = false, sort_column: ?[]const u8 = null, invalidated: bool = false, pub fn toDataProvider(self: *MockProvider) DataProvider { return DataProvider.init(self, &vtable); } const vtable = DataProvider.VTable{ .fetchWindow = fetchWindow, .getTotalCount = getTotalCount, .getFilteredCount = getFilteredCount, .setFilter = setFilter, .setSort = setSort, .getRowId = getRowId, .processBackgroundTasks = processBackgroundTasks, .invalidate = invalidate, .deinit = deinitFn, }; fn fetchWindow(ptr: *anyopaque, offset: usize, limit: usize) ![]const RowData { const self: *MockProvider = @ptrCast(@alignCast(ptr)); const end = @min(offset + limit, self.data.len); if (offset >= self.data.len) return &.{}; return self.data[offset..end]; } fn getTotalCount(ptr: *anyopaque) CountInfo { const self: *MockProvider = @ptrCast(@alignCast(ptr)); return .{ .value = self.total, .state = .ready }; } fn getFilteredCount(ptr: *anyopaque) CountInfo { const self: *MockProvider = @ptrCast(@alignCast(ptr)); return .{ .value = self.data.len, .state = .ready }; } fn setFilter(ptr: *anyopaque, _: []const u8) void { const self: *MockProvider = @ptrCast(@alignCast(ptr)); self.filter_applied = true; } fn setSort(ptr: *anyopaque, column: []const u8, _: SortDirection) void { const self: *MockProvider = @ptrCast(@alignCast(ptr)); self.sort_column = column; } fn getRowId(ptr: *anyopaque, index: usize) ?i64 { const self: *MockProvider = @ptrCast(@alignCast(ptr)); if (index < self.data.len) return self.data[index].id; return null; } fn processBackgroundTasks(_: *anyopaque) bool { return false; } fn invalidate(ptr: *anyopaque) void { const self: *MockProvider = @ptrCast(@alignCast(ptr)); self.invalidated = true; } fn deinitFn(_: *anyopaque) void {} }; test "DataProvider basic operations" { const values1 = [_][]const u8{ "1", "Alice" }; const values2 = [_][]const u8{ "2", "Bob" }; const values3 = [_][]const u8{ "3", "Charlie" }; const rows = [_]RowData{ .{ .id = 1, .values = &values1 }, .{ .id = 2, .values = &values2 }, .{ .id = 3, .values = &values3 }, }; var mock = MockProvider{ .data = &rows, .total = 100, }; const provider = mock.toDataProvider(); // Test fetchWindow const window = try provider.fetchWindow(0, 2); try testing.expectEqual(@as(usize, 2), window.len); try testing.expectEqual(@as(i64, 1), window[0].id); try testing.expectEqual(@as(i64, 2), window[1].id); // Test counts const total = provider.getTotalCount(); try testing.expectEqual(@as(usize, 100), total.value); try testing.expectEqual(types.LoadState.ready, total.state); // Test getRowId try testing.expectEqual(@as(?i64, 2), provider.getRowId(1)); try testing.expectEqual(@as(?i64, null), provider.getRowId(100)); // Test setFilter provider.setFilter("test"); try testing.expect(mock.filter_applied); // Test setSort provider.setSort("name", .ascending); try testing.expectEqualStrings("name", mock.sort_column.?); // Test invalidate provider.invalidate(); try testing.expect(mock.invalidated); } test "DataProvider fetchWindow bounds" { const values1 = [_][]const u8{"1"}; const rows = [_]RowData{ .{ .id = 1, .values = &values1 }, }; var mock = MockProvider{ .data = &rows, .total = 1, }; const provider = mock.toDataProvider(); // Offset beyond data const empty = try provider.fetchWindow(100, 10); try testing.expectEqual(@as(usize, 0), empty.len); // Limit beyond data const partial = try provider.fetchWindow(0, 100); try testing.expectEqual(@as(usize, 1), partial.len); }