wttr/src/location/resolver.zig

226 lines
7.1 KiB
Zig

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,
coords: Coordinates,
};
pub const LocationType = enum {
city_name,
airport_code,
special_location,
ip_address,
domain_name,
};
pub const Resolver = struct {
allocator: std.mem.Allocator,
geoip: ?*GeoIp,
geocache: *GeoCache,
airports: ?*Airports,
pub fn init(allocator: std.mem.Allocator, geoip: ?*GeoIp, geocache: *GeoCache, airports: ?*Airports) Resolver {
return .{
.allocator = allocator,
.geoip = geoip,
.geocache = geocache,
.airports = airports,
};
}
pub fn resolve(self: *Resolver, query: []const u8) !Location {
const location_type = detectType(query);
return switch (location_type) {
.ip_address => try self.resolveIP(query),
.domain_name => try self.resolveDomain(query[1..]), // Skip '@'
.special_location => try self.resolveGeocoded(query[1..]), // Skip '~'
.airport_code => try self.resolveAirport(query),
.city_name => try self.resolveGeocoded(query),
};
}
fn detectType(query: []const u8) LocationType {
if (query.len == 0) return .city_name;
if (query[0] == '@') return .domain_name;
if (query[0] == '~') return .special_location;
if (query.len == 3 and isAlpha(query)) return .airport_code;
if (isIPAddress(query)) return .ip_address;
return .city_name;
}
fn resolveIP(self: *Resolver, ip: []const u8) !Location {
if (self.geoip) |geoip| {
if (try geoip.lookup(ip)) |coords| {
return .{
.name = try self.allocator.dupe(u8, ip),
.coords = coords,
};
}
}
return error.LocationNotFound;
}
fn resolveDomain(self: *Resolver, domain: []const u8) !Location {
// Use std.net to resolve domain to IP
const addr_list = std.net.getAddressList(self.allocator, domain, 0) catch {
return error.LocationNotFound;
};
defer addr_list.deinit();
if (addr_list.addrs.len == 0) {
return error.LocationNotFound;
}
// Format IP address
const ip_str = try std.fmt.allocPrint(self.allocator, "{any}", .{addr_list.addrs[0].any});
defer self.allocator.free(ip_str);
return self.resolveIP(ip_str);
}
fn resolveGeocoded(self: *Resolver, name: []const u8) !Location {
// Check cache first
if (self.geocache.get(name)) |cached| {
return Location{
.name = try self.allocator.dupe(u8, cached.name),
.coords = cached.coords,
};
}
// Call Nominatim API
const url = try std.fmt.allocPrint(
self.allocator,
"https://nominatim.openstreetmap.org/search?q={s}&format=json&limit=1",
.{name},
);
defer self.allocator.free(url);
var client = std.http.Client{ .allocator = self.allocator };
defer client.deinit();
const uri = try std.Uri.parse(url);
var response_buf: [1024 * 1024]u8 = undefined;
var writer = std.Io.Writer.fixed(&response_buf);
const result = client.fetch(.{
.location = .{ .uri = uri },
.method = .GET,
.response_writer = &writer,
.extra_headers = &.{
.{ .name = "User-Agent", .value = "wttr.in-zig/1.0" },
},
}) catch {
std.log.warn("Nominatim API call failed for: {s}", .{name});
return error.GeocodingFailed;
};
if (result.status != .ok) {
return error.GeocodingFailed;
}
const response_body = response_buf[0..writer.end];
// Parse JSON response
const parsed = try std.json.parseFromSlice(
std.json.Value,
self.allocator,
response_body,
.{},
);
defer parsed.deinit();
if (parsed.value.array.items.len == 0) {
return error.LocationNotFound;
}
const first = parsed.value.array.items[0];
const display_name = first.object.get("display_name").?.string;
const lat = try std.fmt.parseFloat(f64, first.object.get("lat").?.string);
const lon = try std.fmt.parseFloat(f64, first.object.get("lon").?.string);
// Cache the result
try self.geocache.put(name, .{
.name = display_name,
.coords = .{
.latitude = lat,
.longitude = lon,
},
});
return Location{
.name = try self.allocator.dupe(u8, display_name),
.coords = .{
.latitude = lat,
.longitude = lon,
},
};
}
fn resolveAirport(self: *Resolver, code: []const u8) !Location {
// Try airport database first
if (self.airports) |airports| {
// Convert to uppercase for lookup
var upper_code: [3]u8 = undefined;
for (code, 0..) |c, i| {
upper_code[i] = std.ascii.toUpper(c);
}
if (airports.lookup(&upper_code)) |airport| {
return Location{
.name = try self.allocator.dupe(u8, airport.name),
.coords = airport.coords,
};
}
}
// Fall back to geocoding
return self.resolveGeocoded(code);
}
fn isAlpha(s: []const u8) bool {
for (s) |c| {
if (!std.ascii.isAlphabetic(c)) return false;
}
return true;
}
fn isIPAddress(s: []const u8) bool {
// Simple check for IPv4
var dots: u8 = 0;
for (s) |c| {
if (c == '.') {
dots += 1;
} else if (!std.ascii.isDigit(c)) {
return false;
}
}
return dots == 3;
}
};
test "detect IP address" {
try std.testing.expect(Resolver.isIPAddress("192.168.1.1"));
try std.testing.expect(!Resolver.isIPAddress("not.an.ip"));
}
test "detect location type" {
try std.testing.expectEqual(LocationType.ip_address, Resolver.detectType("8.8.8.8"));
try std.testing.expectEqual(LocationType.domain_name, Resolver.detectType("@github.com"));
try std.testing.expectEqual(LocationType.special_location, Resolver.detectType("~Eiffel+Tower"));
try std.testing.expectEqual(LocationType.airport_code, Resolver.detectType("muc"));
try std.testing.expectEqual(LocationType.city_name, Resolver.detectType("London"));
}
test "resolver init" {
const allocator = std.testing.allocator;
var geocache = try GeoCache.init(allocator, null);
defer geocache.deinit();
const resolver = Resolver.init(allocator, null, &geocache, null);
try std.testing.expect(resolver.geoip == null);
try std.testing.expect(resolver.airports == null);
}