226 lines
7.1 KiB
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);
|
|
}
|