diff --git a/src/cache/Cache.zig b/src/cache/Cache.zig index 2fa3f42..4269aae 100644 --- a/src/cache/Cache.zig +++ b/src/cache/Cache.zig @@ -7,17 +7,19 @@ const log = std.log.scoped(.cache); allocator: std.mem.Allocator, lru: Lru, -cache_dir: []const u8, +/// Cache directory for L2 persistent cache +cache_dir: ?[]const u8, pub const Config = struct { max_entries: usize = 1_000, - cache_dir: []const u8, + cache_dir: ?[]const u8, }; pub fn init(allocator: std.mem.Allocator, config: Config) !*Cache { - std.fs.makeDirAbsolute(config.cache_dir) catch |err| { - if (err != error.PathAlreadyExists) return err; - }; + if (config.cache_dir) |d| + std.fs.makeDirAbsolute(d) catch |err| { + if (err != error.PathAlreadyExists) return err; + }; const cache = try allocator.create(Cache); errdefer allocator.destroy(cache); @@ -25,15 +27,17 @@ pub fn init(allocator: std.mem.Allocator, config: Config) !*Cache { cache.* = Cache{ .allocator = allocator, .lru = try Lru.init(allocator, config.max_entries), - .cache_dir = try allocator.dupe(u8, config.cache_dir), + .cache_dir = if (config.cache_dir) |d| try allocator.dupe(u8, d) else null, }; cache.lru.setEvictionCallback(cache, evictCallback); // Clean up expired files and populate L1 cache from L2 - cache.loadFromDir() catch |err| { - std.log.warn("Failed to load cache from {s}: {}", .{ config.cache_dir, err }); - }; + if (config.cache_dir) |d| { + cache.loadFromDir() catch |err| { + std.log.warn("Failed to load cache from {s}: {}", .{ d, err }); + }; + } return cache; } @@ -61,8 +65,10 @@ pub fn put(self: *Cache, key: []const u8, value: []const u8, ttl_seconds: u64) ! const now = std.time.milliTimestamp(); const expires = now + @as(i64, @intCast(ttl_seconds * 1000)); - // Write to L2 (disk) first - try self.saveToFile(key, value, expires); + // Write to L2 (disk) first if cache_dir is set + if (self.cache_dir != null) { + try self.saveToFile(key, value, expires); + } // Add to L1 (memory) try self.lru.put(key, value, expires); @@ -70,7 +76,7 @@ pub fn put(self: *Cache, key: []const u8, value: []const u8, ttl_seconds: u64) ! pub fn deinit(self: *Cache) void { self.lru.deinit(); - self.allocator.free(self.cache_dir); + if (self.cache_dir) |d| self.allocator.free(d); self.allocator.destroy(self); } @@ -96,10 +102,12 @@ const CacheEntry = struct { /// if the file access fails OR if the data has expired. /// If the data has expired, the file will be deleted fn loadFromFile(self: *Cache, key: []const u8) !CacheEntry { + if (self.cache_dir == null) return error.NoCacheDir; + const filename = try self.getCacheFilename(key); defer self.allocator.free(filename); - const file_path = try std.fs.path.join(self.allocator, &.{ self.cache_dir, filename }); + const file_path = try std.fs.path.join(self.allocator, &.{ self.cache_dir.?, filename }); defer self.allocator.free(file_path); return self.loadFromFilePath(file_path); @@ -155,10 +163,12 @@ fn deserialize(allocator: std.mem.Allocator, reader: *std.Io.Reader) !CacheEntry } fn saveToFile(self: *Cache, key: []const u8, value: []const u8, expires: i64) !void { + if (self.cache_dir == null) return error.NoCacheDir; + const filename = try self.getCacheFilename(key); defer self.allocator.free(filename); - const file_path = try std.fs.path.join(self.allocator, &.{ self.cache_dir, filename }); + const file_path = try std.fs.path.join(self.allocator, &.{ self.cache_dir.?, filename }); defer self.allocator.free(file_path); const file = try std.fs.cwd().createFile(file_path, .{}); @@ -172,14 +182,16 @@ fn saveToFile(self: *Cache, key: []const u8, value: []const u8, expires: i64) !v } fn loadFromDir(self: *Cache) !void { - var dir = try std.fs.cwd().openDir(self.cache_dir, .{ .iterate = true }); + if (self.cache_dir == null) return error.NoCacheDir; + + var dir = try std.fs.cwd().openDir(self.cache_dir.?, .{ .iterate = true }); defer dir.close(); var it = dir.iterate(); while (try it.next()) |entry| { if (entry.kind != .file) continue; - const file_path = try std.fs.path.join(self.allocator, &.{ self.cache_dir, entry.name }); + const file_path = try std.fs.path.join(self.allocator, &.{ self.cache_dir.?, entry.name }); defer self.allocator.free(file_path); const cached = self.loadFromFilePath(file_path) catch continue; @@ -191,10 +203,12 @@ fn loadFromDir(self: *Cache) !void { } fn deleteFile(self: *Cache, key: []const u8) void { + if (self.cache_dir == null) return; + const filename = self.getCacheFilename(key) catch @panic("OOM"); defer self.allocator.free(filename); - const file_path = std.fs.path.join(self.allocator, &.{ self.cache_dir, filename }) catch @panic("OOM"); + const file_path = std.fs.path.join(self.allocator, &.{ self.cache_dir.?, filename }) catch @panic("OOM"); defer self.allocator.free(file_path); std.fs.cwd().deleteFile(file_path) catch |e| { diff --git a/src/weather/Provider.zig b/src/weather/Provider.zig index 490428e..9890b66 100644 --- a/src/weather/Provider.zig +++ b/src/weather/Provider.zig @@ -45,3 +45,33 @@ pub fn fetch(self: WeatherProvider, allocator: std.mem.Allocator, coords: Coordi pub fn deinit(self: WeatherProvider) void { self.vtable.deinit(self.ptr); } + +test "provider fetch" { + const Mock = @import("Mock.zig"); + const MetNo = @import("MetNo.zig"); + + const cache = try Cache.init(std.testing.allocator, .{ .max_entries = 10, .cache_dir = null }); + defer cache.deinit(); + + var mock = try Mock.init(std.testing.allocator); + defer mock.deinit(); + + mock.parse_fn = MetNo.parse; + + const test_data = @embedFile("../tests/metno_test_data.json"); + try mock.addResponse(.{ .latitude = 33.4484, .longitude = -112.0740 }, test_data); + + const provider = mock.provider(cache); + const coords = Coordinates{ .latitude = 33.4484, .longitude = -112.0740 }; + + // First fetch - should allocate and cache + const weather1 = try provider.fetch(std.testing.allocator, coords); + defer weather1.deinit(); + + // Second fetch - should use cache + const weather2 = try provider.fetch(std.testing.allocator, coords); + defer weather2.deinit(); + + try std.testing.expect(weather1.forecast.len > 0); + try std.testing.expect(weather2.forecast.len > 0); +}