centralize/reuse coordinates structure

This commit is contained in:
Emil Lerch 2025-12-18 15:41:08 -08:00
parent 448c49ae79
commit 4195f43fa7
Signed by: lobo
GPG key ID: A7B62D657EF764F8
8 changed files with 69 additions and 92 deletions

View file

@ -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;

View file

@ -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);
}

View file

@ -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" {

View file

@ -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,

View file

@ -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,
};
}
}

View file

@ -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));

View file

@ -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);

View file

@ -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 {