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,
|
||||
});
|
||||
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
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 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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
const std = @import("std");
|
||||
|
||||
pub const WeatherError = error{
|
||||
LocationNotFound,
|
||||
ApiError,
|
||||
NetworkError,
|
||||
};
|
||||
|
||||
pub const WeatherData = struct {
|
||||
location: []const u8,
|
||||
current: CurrentCondition,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue