diff --git a/src/Config.zig b/src/Config.zig new file mode 100644 index 0000000..675b24d --- /dev/null +++ b/src/Config.zig @@ -0,0 +1,84 @@ +const std = @import("std"); + +const Config = @This(); + +listen_host: []const u8, +listen_port: u16, +cache_size: usize, +cache_dir: []const u8, + +/// GeoLite2 is used for GeoIP (IP -> geographic location) +/// IP2Location is a fallback if IP is not found in this db +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, + +/// 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, + +pub fn load(allocator: std.mem.Allocator) !Config { + var env = try std.process.getEnvMap(allocator); + defer env.deinit(); + + // 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" }), + .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}); + }, + }; +} + +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); +} + +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); +} diff --git a/src/config.zig b/src/config.zig deleted file mode 100644 index b9602e0..0000000 --- a/src/config.zig +++ /dev/null @@ -1,84 +0,0 @@ -const std = @import("std"); - -pub const Config = struct { - listen_host: []const u8, - listen_port: u16, - cache_size: usize, - cache_dir: []const u8, - - /// GeoLite2 is used for GeoIP (IP -> geographic location) - /// IP2Location is a fallback if IP is not found in this db - 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, - - /// 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, - - pub fn load(allocator: std.mem.Allocator) !Config { - var env = try std.process.getEnvMap(allocator); - defer env.deinit(); - - // 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 Config{ - .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" }), - .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}); - }, - }; - } - - 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); - } -}; - -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); -} diff --git a/src/location/GeoIp.zig b/src/location/GeoIp.zig index f037f9c..ba26c50 100644 --- a/src/location/GeoIp.zig +++ b/src/location/GeoIp.zig @@ -139,7 +139,7 @@ test "GeoIP init with invalid path fails" { test "isUSIP detects US IPs" { const allocator = std.testing.allocator; - const Config = @import("../config.zig").Config; + const Config = @import("../Config.zig"); const config = try Config.load(allocator); defer config.deinit(allocator); const build_options = @import("build_options"); diff --git a/src/main.zig b/src/main.zig index d9d088b..f001823 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,5 +1,5 @@ const std = @import("std"); -const config = @import("config.zig"); +const Config = @import("Config.zig"); const Cache = @import("cache/Cache.zig"); const MetNo = @import("weather/MetNo.zig"); const Server = @import("http/Server.zig"); @@ -15,7 +15,7 @@ pub fn main() !void { defer _ = gpa.deinit(); const allocator = gpa.allocator(); - const cfg = try config.Config.load(allocator); + const cfg = try Config.load(allocator); defer cfg.deinit(allocator); std.log.info("wttr starting on {s}:{d}", .{ cfg.listen_host, cfg.listen_port }); @@ -81,7 +81,7 @@ pub fn main() !void { test { std.testing.refAllDecls(@This()); - _ = @import("config.zig"); + _ = @import("Config.zig"); _ = @import("cache/Lru.zig"); _ = @import("weather/Mock.zig"); _ = @import("http/RateLimiter.zig");