diff --git a/src/http/handler.zig b/src/http/handler.zig index 9567844..28cf208 100644 --- a/src/http/handler.zig +++ b/src/http/handler.zig @@ -95,7 +95,7 @@ fn handleWeatherInternal( // Resolve location const loc_str = location_query orelse ""; const location = if (loc_str.len == 0) - Location{ .name = "London", .latitude = 51.5074, .longitude = -0.1278 } + Location{ .name = "London", .coords = .{ .latitude = 51.5074, .longitude = -0.1278 } } else opts.resolver.resolve(loc_str) catch |err| { switch (err) { @@ -113,10 +113,7 @@ fn handleWeatherInternal( }; // Fetch weather using coordinates - const coord_str = try std.fmt.allocPrint(allocator, "{d:.4},{d:.4}", .{ location.latitude, location.longitude }); - defer allocator.free(coord_str); - - const weather = opts.provider.fetch(allocator, coord_str) catch |err| { + const weather = opts.provider.fetch(allocator, location.coords) catch |err| { switch (err) { error.LocationNotFound => { res.status = 404; diff --git a/src/location/Airports.zig b/src/location/Airports.zig index 7399c23..99e3092 100644 --- a/src/location/Airports.zig +++ b/src/location/Airports.zig @@ -1,10 +1,10 @@ const std = @import("std"); +const Coordinates = @import("../Coordinates.zig"); pub const Airport = struct { iata: []const u8, name: []const u8, - latitude: f64, - longitude: f64, + coords: Coordinates, }; const Airports = @This(); @@ -79,8 +79,10 @@ fn parseAirportLine(allocator: std.mem.Allocator, line: []const u8) !Airport { return Airport{ .iata = iata, .name = name, - .latitude = lat, - .longitude = lon, + .coords = .{ + .latitude = lat, + .longitude = lon, + }, }; } @@ -101,8 +103,8 @@ test "parseAirportLine valid" { try std.testing.expectEqualStrings("GKA", airport.iata); try std.testing.expectEqualStrings("Goroka Airport", airport.name); - try std.testing.expectApproxEqAbs(@as(f64, -6.081689834590001), airport.latitude, 0.0001); - try std.testing.expectApproxEqAbs(@as(f64, 145.391998291), airport.longitude, 0.0001); + try std.testing.expectApproxEqAbs(@as(f64, -6.081689834590001), airport.coords.latitude, 0.0001); + try std.testing.expectApproxEqAbs(@as(f64, 145.391998291), airport.coords.longitude, 0.0001); } test "parseAirportLine with null IATA" { @@ -122,5 +124,5 @@ test "AirportDB lookup" { const result = db.lookup("MUC"); try std.testing.expect(result != null); try std.testing.expectEqualStrings("Munich Airport", result.?.name); - try std.testing.expectApproxEqAbs(@as(f64, 48.353802), result.?.latitude, 0.0001); + try std.testing.expectApproxEqAbs(@as(f64, 48.353802), result.?.coords.latitude, 0.0001); } diff --git a/src/location/GeoCache.zig b/src/location/GeoCache.zig index 7a688fd..216aee1 100644 --- a/src/location/GeoCache.zig +++ b/src/location/GeoCache.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const Coordinates = @import("../Coordinates.zig"); const GeoCache = @This(); @@ -8,8 +9,7 @@ cache_file: ?[]const u8, pub const CachedLocation = struct { name: []const u8, - latitude: f64, - longitude: f64, + coords: Coordinates, }; pub fn init(allocator: std.mem.Allocator, cache_file: ?[]const u8) !GeoCache { @@ -54,8 +54,7 @@ pub fn put(self: *GeoCache, query: []const u8, location: CachedLocation) !void { const key = try self.allocator.dupe(u8, query); const value = CachedLocation{ .name = try self.allocator.dupe(u8, location.name), - .latitude = location.latitude, - .longitude = location.longitude, + .coords = location.coords, }; try self.cache.put(key, value); } @@ -81,8 +80,10 @@ fn loadFromFile(allocator: std.mem.Allocator, cache: *std.StringHashMap(CachedLo const key = try allocator.dupe(u8, entry.key_ptr.*); const value = CachedLocation{ .name = try allocator.dupe(u8, obj.get("name").?.string), - .latitude = obj.get("latitude").?.float, - .longitude = obj.get("longitude").?.float, + .coords = .{ + .latitude = obj.get("latitude").?.float, + .longitude = obj.get("longitude").?.float, + }, }; try cache.put(key, value); } @@ -108,8 +109,8 @@ fn saveToFile(self: *GeoCache, file_path: []const u8) !void { std.json.fmt(entry.key_ptr.*, .{}), std.json.fmt(.{ .name = entry.value_ptr.name, - .latitude = entry.value_ptr.latitude, - .longitude = entry.value_ptr.longitude, + .latitude = entry.value_ptr.coords.latitude, + .longitude = entry.value_ptr.coords.longitude, }, .{}), }); } @@ -126,14 +127,16 @@ test "GeoCache basic operations" { // Test put and get try cache.put("London", .{ .name = "London, UK", - .latitude = 51.5074, - .longitude = -0.1278, + .coords = .{ + .latitude = 51.5074, + .longitude = -0.1278, + }, }); const result = cache.get("London"); try std.testing.expect(result != null); - try std.testing.expectApproxEqAbs(@as(f64, 51.5074), result.?.latitude, 0.0001); - try std.testing.expectApproxEqAbs(@as(f64, -0.1278), result.?.longitude, 0.0001); + try std.testing.expectApproxEqAbs(@as(f64, 51.5074), result.?.coords.latitude, 0.0001); + try std.testing.expectApproxEqAbs(@as(f64, -0.1278), result.?.coords.longitude, 0.0001); } test "GeoCache miss returns null" { diff --git a/src/location/GeoIp.zig b/src/location/GeoIp.zig index 39e3218..5d3b86a 100644 --- a/src/location/GeoIp.zig +++ b/src/location/GeoIp.zig @@ -1,9 +1,5 @@ const std = @import("std"); - -pub const Coordinates = struct { - latitude: f64, - longitude: f64, -}; +const Coordinates = @import("../Coordinates.zig"); pub const MMDB = extern struct { filename: [*:0]const u8, diff --git a/src/location/resolver.zig b/src/location/resolver.zig index 889199b..97abf29 100644 --- a/src/location/resolver.zig +++ b/src/location/resolver.zig @@ -2,11 +2,11 @@ const std = @import("std"); const GeoIp = @import("GeoIp.zig"); const GeoCache = @import("GeoCache.zig"); const Airports = @import("Airports.zig"); +const Coordinates = @import("../Coordinates.zig"); pub const Location = struct { name: []const u8, - latitude: f64, - longitude: f64, + coords: Coordinates, }; pub const LocationType = enum { @@ -56,10 +56,9 @@ pub const Resolver = struct { fn resolveIP(self: *Resolver, ip: []const u8) !Location { if (self.geoip) |geoip| { if (try geoip.lookup(ip)) |coords| { - return Location{ + return .{ .name = try self.allocator.dupe(u8, ip), - .latitude = coords.latitude, - .longitude = coords.longitude, + .coords = coords, }; } } @@ -89,8 +88,7 @@ pub const Resolver = struct { if (self.geocache.get(name)) |cached| { return Location{ .name = try self.allocator.dupe(u8, cached.name), - .latitude = cached.latitude, - .longitude = cached.longitude, + .coords = cached.coords, }; } @@ -145,14 +143,18 @@ pub const Resolver = struct { // Cache the result try self.geocache.put(name, .{ .name = display_name, - .latitude = lat, - .longitude = lon, + .coords = .{ + .latitude = lat, + .longitude = lon, + }, }); return Location{ .name = try self.allocator.dupe(u8, display_name), - .latitude = lat, - .longitude = lon, + .coords = .{ + .latitude = lat, + .longitude = lon, + }, }; } @@ -168,8 +170,7 @@ pub const Resolver = struct { if (airports.lookup(&upper_code)) |airport| { return Location{ .name = try self.allocator.dupe(u8, airport.name), - .latitude = airport.latitude, - .longitude = airport.longitude, + .coords = airport.coords, }; } } diff --git a/src/weather/MetNo.zig b/src/weather/MetNo.zig index 7abee31..a22757f 100644 --- a/src/weather/MetNo.zig +++ b/src/weather/MetNo.zig @@ -1,5 +1,6 @@ const std = @import("std"); const WeatherProvider = @import("Provider.zig"); +const Coordinates = @import("../Coordinates.zig"); const types = @import("types.zig"); const MetNo = @This(); @@ -22,16 +23,13 @@ pub fn provider(self: *MetNo) WeatherProvider { }; } -fn fetch(ptr: *anyopaque, allocator: std.mem.Allocator, location: []const u8) !types.WeatherData { +fn fetch(ptr: *anyopaque, allocator: std.mem.Allocator, coords: Coordinates) !types.WeatherData { const self: *MetNo = @ptrCast(@alignCast(ptr)); - // Parse location as "lat,lon" or use default - const coords = parseLocation(location) catch Coords{ .lat = 51.5074, .lon = -0.1278 }; - const url = try std.fmt.allocPrint( self.allocator, "https://api.met.no/weatherapi/locationforecast/2.0/compact?lat={d:.4}&lon={d:.4}", - .{ coords.lat, coords.lon }, + .{ coords.latitude, coords.longitude }, ); defer self.allocator.free(url); @@ -67,7 +65,7 @@ fn fetch(ptr: *anyopaque, allocator: std.mem.Allocator, location: []const u8) !t ); defer parsed.deinit(); - return try parseMetNoResponse(allocator, location, parsed.value); + return try parseMetNoResponse(allocator, coords, parsed.value); } fn deinitProvider(ptr: *anyopaque) void { @@ -79,24 +77,7 @@ pub fn deinit(self: *MetNo) void { _ = self; } -const Coords = struct { - lat: f64, - lon: f64, -}; - -fn parseLocation(location: []const u8) !Coords { - if (std.mem.indexOf(u8, location, ",")) |comma_idx| { - const lat_str = std.mem.trim(u8, location[0..comma_idx], " "); - const lon_str = std.mem.trim(u8, location[comma_idx + 1 ..], " "); - return Coords{ - .lat = try std.fmt.parseFloat(f64, lat_str), - .lon = try std.fmt.parseFloat(f64, lon_str), - }; - } - return error.InvalidLocationFormat; -} - -fn parseMetNoResponse(allocator: std.mem.Allocator, location: []const u8, json: std.json.Value) !types.WeatherData { +fn parseMetNoResponse(allocator: std.mem.Allocator, coords: Coordinates, json: std.json.Value) !types.WeatherData { const properties = json.object.get("properties") orelse return error.InvalidResponse; const timeseries = properties.object.get("timeseries") orelse return error.InvalidResponse; @@ -128,7 +109,7 @@ fn parseMetNoResponse(allocator: std.mem.Allocator, location: []const u8, json: "N"; return types.WeatherData{ - .location = try allocator.dupe(u8, location), + .location = try std.fmt.allocPrint(allocator, "{d:.4},{d:.4}", .{ coords.latitude, coords.longitude }), .current = .{ .temp_c = temp_c, .temp_f = temp_c * 9.0 / 5.0 + 32.0, @@ -178,22 +159,6 @@ fn degreeToDirection(deg: f32) []const u8 { return directions[@min(idx, 7)]; } -test "parseLocation with valid coordinates" { - const coords = try parseLocation("51.5074,-0.1278"); - try std.testing.expectApproxEqAbs(@as(f64, 51.5074), coords.lat, 0.0001); - try std.testing.expectApproxEqAbs(@as(f64, -0.1278), coords.lon, 0.0001); -} - -test "parseLocation with whitespace" { - const coords = try parseLocation(" 40.7128 , -74.0060 "); - try std.testing.expectApproxEqAbs(@as(f64, 40.7128), coords.lat, 0.0001); - try std.testing.expectApproxEqAbs(@as(f64, -74.0060), coords.lon, 0.0001); -} - -test "parseLocation with invalid format" { - try std.testing.expectError(error.InvalidLocationFormat, parseLocation("London")); -} - test "degreeToDirection" { try std.testing.expectEqualStrings("N", degreeToDirection(0)); try std.testing.expectEqualStrings("NE", degreeToDirection(45)); diff --git a/src/weather/Mock.zig b/src/weather/Mock.zig index fcf48f4..15c7ec1 100644 --- a/src/weather/Mock.zig +++ b/src/weather/Mock.zig @@ -1,5 +1,6 @@ const std = @import("std"); const WeatherProvider = @import("Provider.zig"); +const Coordinates = @import("../Coordinates.zig"); const types = @import("types.zig"); const Mock = @This(); @@ -24,14 +25,17 @@ pub fn provider(self: *Mock) WeatherProvider { }; } -pub fn addResponse(self: *Mock, location: []const u8, data: types.WeatherData) !void { - const key = try self.allocator.dupe(u8, location); +pub fn addResponse(self: *Mock, coords: Coordinates, data: types.WeatherData) !void { + const key = try std.fmt.allocPrint(self.allocator, "{d:.4},{d:.4}", .{ coords.latitude, coords.longitude }); try self.responses.put(key, data); } -fn fetch(ptr: *anyopaque, allocator: std.mem.Allocator, location: []const u8) !types.WeatherData { +fn fetch(ptr: *anyopaque, allocator: std.mem.Allocator, coords: Coordinates) !types.WeatherData { const self: *Mock = @ptrCast(@alignCast(ptr)); - const data = self.responses.get(location) orelse return error.LocationNotFound; + const key = try std.fmt.allocPrint(allocator, "{d:.4},{d:.4}", .{ coords.latitude, coords.longitude }); + defer allocator.free(key); + + const data = self.responses.get(key) orelse return error.LocationNotFound; return types.WeatherData{ .location = try allocator.dupe(u8, data.location), @@ -58,6 +62,8 @@ test "mock weather provider" { var mock = try Mock.init(std.testing.allocator); defer mock.deinit(); + const coords = Coordinates{ .latitude = 51.5074, .longitude = -0.1278 }; + const data = types.WeatherData{ .location = "London", .current = .{ @@ -75,10 +81,10 @@ test "mock weather provider" { .allocator = std.testing.allocator, }; - try mock.addResponse("London", data); + try mock.addResponse(coords, data); const p = mock.provider(); - const result = try p.fetch(std.testing.allocator, "London"); + const result = try p.fetch(std.testing.allocator, coords); defer result.deinit(); try std.testing.expectEqual(@as(f32, 15.0), result.current.temp_c); diff --git a/src/weather/Provider.zig b/src/weather/Provider.zig index fcd700f..291569e 100644 --- a/src/weather/Provider.zig +++ b/src/weather/Provider.zig @@ -1,5 +1,12 @@ +//! Weather provider interface using vtable pattern. +//! +//! This provides a common interface for different weather data sources +//! (e.g., Met.no, mock providers) to implement. Providers must implement +//! the fetch and deinit functions in their vtable. + const std = @import("std"); const types = @import("types.zig"); +const Coordinates = @import("../Coordinates.zig"); const WeatherProvider = @This(); @@ -7,12 +14,12 @@ ptr: *anyopaque, vtable: *const VTable, pub const VTable = struct { - fetch: *const fn (ptr: *anyopaque, allocator: std.mem.Allocator, location: []const u8) anyerror!types.WeatherData, + fetch: *const fn (ptr: *anyopaque, allocator: std.mem.Allocator, coords: Coordinates) anyerror!types.WeatherData, deinit: *const fn (ptr: *anyopaque) void, }; -pub fn fetch(self: WeatherProvider, allocator: std.mem.Allocator, location: []const u8) !types.WeatherData { - return self.vtable.fetch(self.ptr, allocator, location); +pub fn fetch(self: WeatherProvider, allocator: std.mem.Allocator, coords: Coordinates) !types.WeatherData { + return self.vtable.fetch(self.ptr, allocator, coords); } pub fn deinit(self: WeatherProvider) void {