wttr/src/Config.zig

130 lines
5.4 KiB
Zig

const std = @import("std");
const Config = @This();
pub const GeoIpFallback = enum {
ipwhois,
ip2location,
};
listen_host: []const u8,
listen_port: u16,
cache_size: usize,
cache_dir: []const u8,
/// GeoLite2 is used for GeoIP (IP -> geographic location)
/// When GeoLite2 data is missing or low-confidence, the configured
/// fallback provider is used (ipwho.is by default, or IP2Location)
geolite_path: []const u8,
/// Geocache file stores location lookups
/// (e.g. "Portland -> 45.52345°N, -122.67621° W). When not found in cache,
/// a web service from Nominatum (https://nominatim.org/) is used
geocache_file: ?[]const u8,
/// Which online service to use as a fallback when GeoLite2 has no data.
/// Default: ipwhois (ipwho.is). Alternative: ip2location (ip2location.io)
geoip_fallback: GeoIpFallback,
/// If provided, when GeoLite2 is missing data, https://www.ip2location.com/
/// can be used. This will also be cached in the cached file
ip2location_api_key: ?[]const u8,
ip2location_cache_file: []const u8,
/// Cache file for ipwho.is lookups
ipwhois_cache_file: []const u8,
/// Loads configuration from the process environment.
///
/// Zig 0.16 removed `std.process.getEnvMap`; the environment map is now
/// supplied by the runtime to `main` and threaded in by the caller.
pub fn load(allocator: std.mem.Allocator, env: *const std.process.Environ.Map) !Config {
// Get XDG_CACHE_HOME or default to ~/.cache
const home = env.get("HOME") orelse "/tmp";
const xdg_cache = env.get("XDG_CACHE_HOME") orelse
try std.fs.path.join(allocator, &[_][]const u8{ home, ".cache" });
defer if (env.get("XDG_CACHE_HOME") == null) allocator.free(xdg_cache);
const default_cache_dir = try std.fs.path.join(allocator, &[_][]const u8{ xdg_cache, "wttr" });
defer allocator.free(default_cache_dir);
return .{
.listen_host = env.get("WTTR_LISTEN_HOST") orelse try allocator.dupe(u8, "0.0.0.0"),
.listen_port = if (env.get("WTTR_LISTEN_PORT")) |p|
try std.fmt.parseInt(u16, p, 10)
else
8002,
.cache_size = if (env.get("WTTR_CACHE_SIZE")) |s|
try std.fmt.parseInt(usize, s, 10)
else
10_000,
.cache_dir = try allocator.dupe(u8, env.get("WTTR_CACHE_DIR") orelse default_cache_dir),
.geolite_path = blk: {
if (env.get("WTTR_GEOLITE_PATH")) |v| {
break :blk try allocator.dupe(u8, v);
}
break :blk try std.fmt.allocPrint(allocator, "{s}/GeoLite2-City.mmdb", .{
env.get("WTTR_CACHE_DIR") orelse default_cache_dir,
});
},
.geocache_file = if (env.get("WTTR_GEOCACHE_FILE")) |v| try allocator.dupe(u8, v) else try std.fs.path.join(allocator, &[_][]const u8{ default_cache_dir, "geocache.json" }),
.geoip_fallback = blk: {
if (env.get("WTTR_GEOIP_FALLBACK")) |v| {
if (std.mem.eql(u8, v, "ip2location")) break :blk .ip2location;
}
break :blk .ipwhois;
},
.ip2location_api_key = if (env.get("IP2LOCATION_API_KEY")) |v| try allocator.dupe(u8, v) else null,
.ip2location_cache_file = blk: {
if (env.get("IP2LOCATION_CACHE_FILE")) |v| {
break :blk try allocator.dupe(u8, v);
}
break :blk try std.fmt.allocPrint(allocator, "{s}/ip2location.cache", .{env.get("WTTR_CACHE_DIR") orelse default_cache_dir});
},
.ipwhois_cache_file = blk: {
if (env.get("IPWHOIS_CACHE_FILE")) |v| {
break :blk try allocator.dupe(u8, v);
}
break :blk try std.fmt.allocPrint(allocator, "{s}/ipwhois.cache", .{env.get("WTTR_CACHE_DIR") orelse default_cache_dir});
},
};
}
/// Loads configuration for tests.
///
/// Zig 0.16 removed any way to reach the process environment outside of `main`,
/// so tests cannot resolve `HOME`/`XDG_CACHE_HOME` themselves. Left to an empty
/// environment, `load` would fall back to `HOME = "/tmp"` and scatter test
/// artifacts (including the ~63 MB GeoLite2 database that `-Ddownload-geoip`
/// fetches) into `/tmp`.
///
/// Instead `build.zig` resolves the cache directory from its own environment --
/// build scripts still have access -- and passes it through `build_options`, so
/// tests read and write the same location the server uses. Everything else is
/// left unset so tests do not depend on the developer's shell.
pub fn loadForTest(allocator: std.mem.Allocator) !Config {
var env: std.process.Environ.Map = .init(allocator);
defer env.deinit();
try env.put("WTTR_CACHE_DIR", @import("build_options").cache_dir);
return load(allocator, &env);
}
pub fn deinit(self: Config, allocator: std.mem.Allocator) void {
allocator.free(self.listen_host);
allocator.free(self.cache_dir);
allocator.free(self.geolite_path);
if (self.geocache_file) |f| allocator.free(f);
if (self.ip2location_api_key) |k| allocator.free(k);
allocator.free(self.ip2location_cache_file);
allocator.free(self.ipwhois_cache_file);
}
test "config loads defaults" {
const allocator = std.testing.allocator;
const cfg = try Config.loadForTest(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);
}