more AI implementation

This commit is contained in:
Emil Lerch 2025-12-18 08:46:28 -08:00
parent 1b10a916e0
commit bb25def875
Signed by: lobo
GPG key ID: A7B62D657EF764F8
10 changed files with 546 additions and 6 deletions

View file

@ -9,6 +9,43 @@ pub fn build(b: *std.Build) void {
.optimize = optimize,
});
const maxminddb_upstream = b.dependency("maxminddb", .{});
// Build libmaxminddb as a static library
const maxminddb = b.addLibrary(.{
.name = "maxminddb",
.linkage = .static,
.root_module = b.createModule(.{
.target = target,
.optimize = optimize,
.link_libc = true,
}),
});
// Generate maxminddb_config.h
const maxminddb_config = b.addConfigHeader(.{
.style = .blank,
.include_path = "maxminddb_config.h",
}, .{
.PACKAGE_VERSION = "1.11.0",
.MMDB_UINT128_USING_MODE = 1,
});
maxminddb.addConfigHeader(maxminddb_config);
maxminddb.addIncludePath(maxminddb_upstream.path("include"));
maxminddb.addIncludePath(maxminddb_upstream.path("src"));
maxminddb.addCSourceFiles(.{
.root = maxminddb_upstream.path(""),
.files = &.{
"src/data-pool.c",
"src/maxminddb.c",
},
.flags = &.{},
});
maxminddb.installHeadersDirectory(maxminddb_upstream.path("include"), "", .{});
const exe = b.addExecutable(.{
.name = "wttr",
.root_module = b.createModule(.{
@ -19,6 +56,7 @@ pub fn build(b: *std.Build) void {
});
exe.root_module.addImport("httpz", httpz.module("httpz"));
exe.linkLibrary(maxminddb);
exe.linkLibC();
b.installArtifact(exe);
@ -40,6 +78,7 @@ pub fn build(b: *std.Build) void {
}),
});
tests.root_module.addImport("httpz", httpz.module("httpz"));
tests.linkLibrary(maxminddb);
tests.linkLibC();
const run_tests = b.addRunArtifact(tests);

View file

@ -6,6 +6,10 @@
.url = "https://github.com/karlseguin/http.zig/archive/refs/heads/master.tar.gz",
.hash = "httpz-0.0.0-PNVzrEktBwCzPoiua-S8LAYo2tILqczm3tSpneEzLQ9L",
},
.maxminddb = .{
.url = "https://github.com/maxmind/libmaxminddb/archive/refs/tags/1.11.0.tar.gz",
.hash = "N-V-__8AAAYyBQCd9x7qVVFKQIxi01UZ1K8ZFZFfTzj99CvX",
},
},
.fingerprint = 0x710c2b57e81aa678,
.minimum_zig_version = "0.15.0",

View file

@ -34,3 +34,15 @@ pub const Config = struct {
allocator.free(self.geolocator_url);
}
};
test "config loads defaults" {
const allocator = std.testing.allocator;
const cfg = try Config.load(allocator);
defer cfg.deinit(allocator);
try std.testing.expectEqualStrings("0.0.0.0", cfg.listen_host);
try std.testing.expectEqual(@as(u16, 8002), cfg.listen_port);
try std.testing.expectEqual(@as(usize, 10_000), cfg.cache_size);
try std.testing.expectEqualStrings("./GeoLite2-City.mmdb", cfg.geolite_path);
try std.testing.expectEqualStrings("http://localhost:8004", cfg.geolocator_url);
}

View file

@ -4,6 +4,7 @@ const Cache = @import("../cache/cache.zig").Cache;
const WeatherProvider = @import("../weather/provider.zig").WeatherProvider;
const ansi = @import("../render/ansi.zig");
const line = @import("../render/line.zig");
const help = @import("help.zig");
pub const HandleWeatherOptions = struct {
cache: *Cache,
@ -24,6 +25,20 @@ pub fn handleWeatherLocation(
res: *httpz.Response,
) !void {
const location = req.param("location") orelse "London";
// Handle special endpoints
if (std.mem.startsWith(u8, location, ":")) {
if (std.mem.eql(u8, location, ":help")) {
res.content_type = .TEXT;
res.body = help.help_page;
return;
} else if (std.mem.eql(u8, location, ":translation")) {
res.content_type = .TEXT;
res.body = help.translation_page;
return;
}
}
try handleWeatherInternal(opts, req, res, location);
}
@ -44,7 +59,20 @@ fn handleWeatherInternal(
}
const loc = location orelse "London";
const weather = try opts.provider.fetch(allocator, loc);
const weather = opts.provider.fetch(allocator, loc) catch |err| {
switch (err) {
error.LocationNotFound => {
res.status = 404;
res.body = "Location not found\n";
return;
},
else => {
res.status = 500;
res.body = "Internal server error\n";
return;
},
}
};
defer weather.deinit();
const query = try req.query();

55
zig/src/http/help.zig Normal file
View file

@ -0,0 +1,55 @@
const std = @import("std");
pub const help_page =
\\wttr.in - Weather Forecast Service
\\
\\Usage:
\\ curl wttr.in # Weather for your location
\\ curl wttr.in/London # Weather for London
\\ curl wttr.in/~Eiffel+Tower # Weather for special location
\\ curl wttr.in/@github.com # Weather for domain location
\\ curl wttr.in/muc # Weather for airport (IATA code)
\\
\\Query Parameters:
\\ ?format=FORMAT Output format (1,2,3,4,j1,p1,v2)
\\ ?lang=LANG Language code (en,de,fr,etc)
\\ ?u Use USCS units
\\ ?m Use metric units
\\ ?t Transparency for PNG
\\
\\Special Endpoints:
\\ /:help This help page
\\ /:translation Translation information
\\
\\Examples:
\\ curl wttr.in/Paris?format=3
\\ curl wttr.in/Berlin?lang=de
\\ curl wttr.in/Tokyo?m
\\
\\For more information visit: https://github.com/chubin/wttr.in
\\
;
pub const translation_page =
\\wttr.in Translation
\\
\\wttr.in is currently translated into 54 languages.
\\
\\Language Support:
\\ - Automatic detection via Accept-Language header
\\ - Manual selection via ?lang=CODE parameter
\\ - Subdomain selection (e.g., de.wttr.in)
\\
\\Contributing:
\\ To help translate wttr.in, visit:
\\ https://github.com/chubin/wttr.in
\\
;
test "help page exists" {
try std.testing.expect(help_page.len > 0);
}
test "translation page exists" {
try std.testing.expect(translation_page.len > 0);
}

148
zig/src/location/geoip.zig Normal file
View file

@ -0,0 +1,148 @@
const std = @import("std");
pub const Coordinates = struct {
latitude: f64,
longitude: f64,
};
pub const MMDB = extern struct {
filename: [*:0]const u8,
flags: u32,
file_content: ?*anyopaque,
file_size: usize,
data_section: ?*anyopaque,
data_section_size: u32,
metadata_section: ?*anyopaque,
metadata_section_size: u32,
full_record_byte_size: u16,
depth: u16,
ipv4_start_node: extern struct {
node_value: u32,
netmask: u16,
},
metadata: extern struct {
node_count: u32,
record_size: u16,
ip_version: u16,
database_type: [*:0]const u8,
languages: extern struct {
count: usize,
names: [*][*:0]const u8,
},
binary_format_major_version: u16,
binary_format_minor_version: u16,
build_epoch: u64,
description: extern struct {
count: usize,
descriptions: [*]?*anyopaque,
},
},
};
pub const MMDBLookupResult = extern struct {
found_entry: bool,
entry: MMDBEntry,
netmask: u16,
};
pub const MMDBEntry = extern struct {
mmdb: *MMDB,
offset: u32,
};
pub const MMDBEntryData = extern struct {
has_data: bool,
data_type: u32,
offset: u32,
offset_to_next: u32,
data_size: u32,
utf8_string: [*:0]const u8,
double_value: f64,
bytes: [*]const u8,
uint16: u16,
uint32: u32,
int32: i32,
uint64: u64,
uint128: u128,
boolean: bool,
float_value: f32,
};
extern fn MMDB_open(filename: [*:0]const u8, flags: u32, mmdb: *MMDB) c_int;
extern fn MMDB_close(mmdb: *MMDB) void;
extern fn MMDB_lookup_string(mmdb: *MMDB, ipstr: [*:0]const u8, gai_error: *c_int, mmdb_error: *c_int) MMDBLookupResult;
extern fn MMDB_get_value(entry: *MMDBEntry, entry_data: *MMDBEntryData, ...) c_int;
extern fn MMDB_strerror(error_code: c_int) [*:0]const u8;
pub const GeoIP = struct {
mmdb: MMDB,
pub fn init(db_path: []const u8) !GeoIP {
var mmdb: MMDB = undefined;
const path_z = try std.heap.c_allocator.dupeZ(u8, db_path);
defer std.heap.c_allocator.free(path_z);
const status = MMDB_open(path_z.ptr, 0, &mmdb);
if (status != 0) {
return error.CannotOpenDatabase;
}
return GeoIP{ .mmdb = mmdb };
}
pub fn deinit(self: *GeoIP) void {
MMDB_close(&self.mmdb);
}
pub fn lookup(self: *GeoIP, ip: []const u8) !?Coordinates {
const ip_z = try std.heap.c_allocator.dupeZ(u8, ip);
defer std.heap.c_allocator.free(ip_z);
var gai_error: c_int = 0;
var mmdb_error: c_int = 0;
const result = MMDB_lookup_string(&self.mmdb, ip_z.ptr, &gai_error, &mmdb_error);
if (gai_error != 0 or mmdb_error != 0) {
return null;
}
if (!result.found_entry) {
return null;
}
return try self.extractCoordinates(result.entry);
}
fn extractCoordinates(self: *GeoIP, entry: MMDBEntry) !Coordinates {
_ = self;
var entry_mut = entry;
var latitude_data: MMDBEntryData = undefined;
var longitude_data: MMDBEntryData = undefined;
const lat_path = [_][*:0]const u8{ "location", "latitude", null };
const lon_path = [_][*:0]const u8{ "location", "longitude", null };
const lat_status = MMDB_get_value(&entry_mut, &latitude_data, lat_path[0], lat_path[1], lat_path[2]);
const lon_status = MMDB_get_value(&entry_mut, &longitude_data, lon_path[0], lon_path[1], lon_path[2]);
if (lat_status != 0 or lon_status != 0 or !latitude_data.has_data or !longitude_data.has_data) {
return error.CoordinatesNotFound;
}
return Coordinates{
.latitude = latitude_data.double_value,
.longitude = longitude_data.double_value,
};
}
};
test "MMDB functions are callable" {
const mmdb_error = MMDB_strerror(0);
try std.testing.expect(mmdb_error[0] != 0);
}
test "GeoIP init with invalid path fails" {
const result = GeoIP.init("/nonexistent/path.mmdb");
try std.testing.expectError(error.CannotOpenDatabase, result);
}

View file

@ -0,0 +1,171 @@
const std = @import("std");
const GeoIP = @import("geoip.zig").GeoIP;
pub const Location = struct {
name: []const u8,
latitude: f64,
longitude: f64,
};
pub const LocationType = enum {
city_name,
airport_code,
special_location,
ip_address,
domain_name,
};
pub const Resolver = struct {
allocator: std.mem.Allocator,
geoip: ?*GeoIP,
geolocator_url: []const u8,
pub fn init(allocator: std.mem.Allocator, geoip: ?*GeoIP, geolocator_url: []const u8) Resolver {
return .{
.allocator = allocator,
.geoip = geoip,
.geolocator_url = geolocator_url,
};
}
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 Location{
.name = try self.allocator.dupe(u8, ip),
.latitude = coords.latitude,
.longitude = coords.longitude,
};
}
}
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, "{}", .{addr_list.addrs[0].any});
defer self.allocator.free(ip_str);
return self.resolveIP(ip_str);
}
fn resolveGeocoded(self: *Resolver, name: []const u8) !Location {
// Call external geocoding service
const client = std.http.Client{ .allocator = self.allocator };
defer client.deinit();
const url = try std.fmt.allocPrint(
self.allocator,
"{s}/geocode?q={s}",
.{ self.geolocator_url, name },
);
defer self.allocator.free(url);
var response = std.ArrayList(u8).init(self.allocator);
defer response.deinit();
const result = client.fetch(.{
.location = .{ .url = url },
.response_storage = .{ .dynamic = &response },
}) catch {
return error.GeocodingFailed;
};
if (result.status != .ok) {
return error.LocationNotFound;
}
// Parse JSON response: {"name": "...", "lat": ..., "lon": ...}
const parsed = std.json.parseFromSlice(
struct { name: []const u8, lat: f64, lon: f64 },
self.allocator,
response.items,
.{},
) catch {
return error.InvalidGeocodingResponse;
};
defer parsed.deinit();
return Location{
.name = try self.allocator.dupe(u8, parsed.value.name),
.latitude = parsed.value.lat,
.longitude = parsed.value.lon,
};
}
fn resolveAirport(self: *Resolver, code: []const u8) !Location {
// For now, treat as geocoded location
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;
const resolver = Resolver.init(allocator, null, "http://localhost:8004");
try std.testing.expect(resolver.geoip == null);
try std.testing.expectEqualStrings("http://localhost:8004", resolver.geolocator_url);
}

View file

@ -5,6 +5,7 @@ const MetNo = @import("weather/metno.zig").MetNo;
const types = @import("weather/types.zig");
const Server = @import("http/server.zig").Server;
const RateLimiter = @import("http/rate_limiter.zig").RateLimiter;
const GeoIP = @import("location/geoip.zig").GeoIP;
pub const std_options: std.Options = .{
.log_level = .info,
@ -25,8 +26,17 @@ pub fn main() !void {
try stdout.print("wttr starting on {s}:{d}\n", .{ cfg.listen_host, cfg.listen_port });
try stdout.print("Cache size: {d}\n", .{cfg.cache_size});
try stdout.print("Cache dir: {s}\n", .{cfg.cache_dir});
try stdout.print("GeoLite2 path: {s}\n", .{cfg.geolite_path});
try stdout.flush();
// Initialize GeoIP database
var geoip = GeoIP.init(cfg.geolite_path) catch |err| {
std.log.warn("Failed to load GeoIP database: {}", .{err});
std.log.warn("IP-based location resolution will be unavailable", .{});
return err;
};
defer geoip.deinit();
var cache = try Cache.init(allocator, .{
.max_entries = cfg.cache_size,
.cache_dir = cfg.cache_dir,
@ -53,8 +63,13 @@ pub fn main() !void {
test {
std.testing.refAllDecls(@This());
_ = @import("config.zig");
_ = @import("cache/lru.zig");
_ = @import("weather/mock.zig");
_ = @import("http/rate_limiter.zig");
_ = @import("http/query.zig");
_ = @import("http/help.zig");
_ = @import("render/line.zig");
_ = @import("location/geoip.zig");
_ = @import("location/resolver.zig");
}

View file

@ -1,11 +1,73 @@
const std = @import("std");
const types = @import("../weather/types.zig");
pub fn render(allocator: std.mem.Allocator, data: types.WeatherData) ![]const u8 {
var output: std.ArrayList(u8) = .empty;
errdefer output.deinit(allocator);
pub fn render(allocator: std.mem.Allocator, weather: types.WeatherData) ![]const u8 {
var output = std.ArrayList(u8).init(allocator);
errdefer output.deinit();
try std.json.stringify(data, .{}, output.writer(allocator));
try std.json.stringify(.{
.current_condition = .{
.temp_C = weather.current.temp_c,
.temp_F = weather.current.temp_f,
.weatherCode = weather.current.weather_code,
.weatherDesc = .{.{ .value = weather.current.condition }},
.humidity = weather.current.humidity,
.windspeedKmph = weather.current.wind_kph,
.winddirDegree = weather.current.wind_dir,
.pressure = weather.current.pressure_mb,
.precipMM = weather.current.precip_mm,
},
.weather = blk: {
var forecast_array = std.ArrayList(struct {
date: []const u8,
maxtempC: f32,
mintempC: f32,
weatherCode: u16,
weatherDesc: []const u8,
}).init(allocator);
defer forecast_array.deinit();
return output.toOwnedSlice(allocator);
for (weather.forecast) |day| {
try forecast_array.append(.{
.date = day.date,
.maxtempC = day.max_temp_c,
.mintempC = day.min_temp_c,
.weatherCode = day.weather_code,
.weatherDesc = day.condition,
});
}
break :blk try forecast_array.toOwnedSlice();
},
}, .{}, output.writer());
return output.toOwnedSlice();
}
test "render json format" {
const allocator = std.testing.allocator;
const weather = types.WeatherData{
.location = "London",
.current = .{
.temp_c = 15.0,
.temp_f = 59.0,
.condition = "Partly cloudy",
.weather_code = 116,
.humidity = 72,
.wind_kph = 13.0,
.wind_dir = "SW",
.pressure_mb = 1013.0,
.precip_mm = 0.0,
},
.forecast = &[_]types.ForecastDay{},
.allocator = allocator,
};
const output = try render(allocator, weather);
defer allocator.free(output);
try std.testing.expect(output.len > 0);
try std.testing.expect(std.mem.indexOf(u8, output, "temp_C") != null);
try std.testing.expect(std.mem.indexOf(u8, output, "15") != null);
}

View file

@ -1,5 +1,11 @@
const std = @import("std");
pub const WeatherError = error{
LocationNotFound,
ApiError,
NetworkError,
};
pub const WeatherData = struct {
location: []const u8,
current: CurrentCondition,