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