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