centralize/reuse coordinates structure
This commit is contained in:
parent
448c49ae79
commit
4195f43fa7
8 changed files with 69 additions and 92 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue