more AI implementation
This commit is contained in:
parent
1b10a916e0
commit
bb25def875
10 changed files with 546 additions and 6 deletions
|
|
@ -9,6 +9,43 @@ pub fn build(b: *std.Build) void {
|
||||||
.optimize = optimize,
|
.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(.{
|
const exe = b.addExecutable(.{
|
||||||
.name = "wttr",
|
.name = "wttr",
|
||||||
.root_module = b.createModule(.{
|
.root_module = b.createModule(.{
|
||||||
|
|
@ -19,6 +56,7 @@ pub fn build(b: *std.Build) void {
|
||||||
});
|
});
|
||||||
|
|
||||||
exe.root_module.addImport("httpz", httpz.module("httpz"));
|
exe.root_module.addImport("httpz", httpz.module("httpz"));
|
||||||
|
exe.linkLibrary(maxminddb);
|
||||||
exe.linkLibC();
|
exe.linkLibC();
|
||||||
|
|
||||||
b.installArtifact(exe);
|
b.installArtifact(exe);
|
||||||
|
|
@ -40,6 +78,7 @@ pub fn build(b: *std.Build) void {
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
tests.root_module.addImport("httpz", httpz.module("httpz"));
|
tests.root_module.addImport("httpz", httpz.module("httpz"));
|
||||||
|
tests.linkLibrary(maxminddb);
|
||||||
tests.linkLibC();
|
tests.linkLibC();
|
||||||
|
|
||||||
const run_tests = b.addRunArtifact(tests);
|
const run_tests = b.addRunArtifact(tests);
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,10 @@
|
||||||
.url = "https://github.com/karlseguin/http.zig/archive/refs/heads/master.tar.gz",
|
.url = "https://github.com/karlseguin/http.zig/archive/refs/heads/master.tar.gz",
|
||||||
.hash = "httpz-0.0.0-PNVzrEktBwCzPoiua-S8LAYo2tILqczm3tSpneEzLQ9L",
|
.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,
|
.fingerprint = 0x710c2b57e81aa678,
|
||||||
.minimum_zig_version = "0.15.0",
|
.minimum_zig_version = "0.15.0",
|
||||||
|
|
|
||||||
|
|
@ -34,3 +34,15 @@ pub const Config = struct {
|
||||||
allocator.free(self.geolocator_url);
|
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);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ const Cache = @import("../cache/cache.zig").Cache;
|
||||||
const WeatherProvider = @import("../weather/provider.zig").WeatherProvider;
|
const WeatherProvider = @import("../weather/provider.zig").WeatherProvider;
|
||||||
const ansi = @import("../render/ansi.zig");
|
const ansi = @import("../render/ansi.zig");
|
||||||
const line = @import("../render/line.zig");
|
const line = @import("../render/line.zig");
|
||||||
|
const help = @import("help.zig");
|
||||||
|
|
||||||
pub const HandleWeatherOptions = struct {
|
pub const HandleWeatherOptions = struct {
|
||||||
cache: *Cache,
|
cache: *Cache,
|
||||||
|
|
@ -24,6 +25,20 @@ pub fn handleWeatherLocation(
|
||||||
res: *httpz.Response,
|
res: *httpz.Response,
|
||||||
) !void {
|
) !void {
|
||||||
const location = req.param("location") orelse "London";
|
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);
|
try handleWeatherInternal(opts, req, res, location);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -44,7 +59,20 @@ fn handleWeatherInternal(
|
||||||
}
|
}
|
||||||
|
|
||||||
const loc = location orelse "London";
|
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();
|
defer weather.deinit();
|
||||||
|
|
||||||
const query = try req.query();
|
const query = try req.query();
|
||||||
|
|
|
||||||
55
zig/src/http/help.zig
Normal file
55
zig/src/http/help.zig
Normal 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
148
zig/src/location/geoip.zig
Normal 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);
|
||||||
|
}
|
||||||
171
zig/src/location/resolver.zig
Normal file
171
zig/src/location/resolver.zig
Normal 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);
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ const MetNo = @import("weather/metno.zig").MetNo;
|
||||||
const types = @import("weather/types.zig");
|
const types = @import("weather/types.zig");
|
||||||
const Server = @import("http/server.zig").Server;
|
const Server = @import("http/server.zig").Server;
|
||||||
const RateLimiter = @import("http/rate_limiter.zig").RateLimiter;
|
const RateLimiter = @import("http/rate_limiter.zig").RateLimiter;
|
||||||
|
const GeoIP = @import("location/geoip.zig").GeoIP;
|
||||||
|
|
||||||
pub const std_options: std.Options = .{
|
pub const std_options: std.Options = .{
|
||||||
.log_level = .info,
|
.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("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 size: {d}\n", .{cfg.cache_size});
|
||||||
try stdout.print("Cache dir: {s}\n", .{cfg.cache_dir});
|
try stdout.print("Cache dir: {s}\n", .{cfg.cache_dir});
|
||||||
|
try stdout.print("GeoLite2 path: {s}\n", .{cfg.geolite_path});
|
||||||
try stdout.flush();
|
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, .{
|
var cache = try Cache.init(allocator, .{
|
||||||
.max_entries = cfg.cache_size,
|
.max_entries = cfg.cache_size,
|
||||||
.cache_dir = cfg.cache_dir,
|
.cache_dir = cfg.cache_dir,
|
||||||
|
|
@ -53,8 +63,13 @@ pub fn main() !void {
|
||||||
|
|
||||||
test {
|
test {
|
||||||
std.testing.refAllDecls(@This());
|
std.testing.refAllDecls(@This());
|
||||||
|
_ = @import("config.zig");
|
||||||
_ = @import("cache/lru.zig");
|
_ = @import("cache/lru.zig");
|
||||||
_ = @import("weather/mock.zig");
|
_ = @import("weather/mock.zig");
|
||||||
_ = @import("http/rate_limiter.zig");
|
_ = @import("http/rate_limiter.zig");
|
||||||
|
_ = @import("http/query.zig");
|
||||||
|
_ = @import("http/help.zig");
|
||||||
_ = @import("render/line.zig");
|
_ = @import("render/line.zig");
|
||||||
|
_ = @import("location/geoip.zig");
|
||||||
|
_ = @import("location/resolver.zig");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,73 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const types = @import("../weather/types.zig");
|
const types = @import("../weather/types.zig");
|
||||||
|
|
||||||
pub fn render(allocator: std.mem.Allocator, data: types.WeatherData) ![]const u8 {
|
pub fn render(allocator: std.mem.Allocator, weather: types.WeatherData) ![]const u8 {
|
||||||
var output: std.ArrayList(u8) = .empty;
|
var output = std.ArrayList(u8).init(allocator);
|
||||||
errdefer output.deinit(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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
|
pub const WeatherError = error{
|
||||||
|
LocationNotFound,
|
||||||
|
ApiError,
|
||||||
|
NetworkError,
|
||||||
|
};
|
||||||
|
|
||||||
pub const WeatherData = struct {
|
pub const WeatherData = struct {
|
||||||
location: []const u8,
|
location: []const u8,
|
||||||
current: CurrentCondition,
|
current: CurrentCondition,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue