const std = @import("std"); const EnvMap = std.StringHashMap([]const u8); pub const Config = struct { twelvedata_key: ?[]const u8 = null, polygon_key: ?[]const u8 = null, finnhub_key: ?[]const u8 = null, alphavantage_key: ?[]const u8 = null, openfigi_key: ?[]const u8 = null, cache_dir: []const u8, allocator: ?std.mem.Allocator = null, /// Raw .env file contents (keys/values in env_map point into this). env_buf: ?[]const u8 = null, /// Parsed KEY=VALUE pairs from .env file. env_map: ?EnvMap = null, pub fn fromEnv(allocator: std.mem.Allocator) Config { var self = Config{ .cache_dir = undefined, .allocator = allocator, }; // Try loading .env file from the current working directory self.env_buf = std.fs.cwd().readFileAlloc(allocator, ".env", 4096) catch null; if (self.env_buf) |buf| { self.env_map = parseEnvFile(allocator, buf); } self.twelvedata_key = self.resolve("TWELVEDATA_API_KEY"); self.polygon_key = self.resolve("POLYGON_API_KEY"); self.finnhub_key = self.resolve("FINNHUB_API_KEY"); self.alphavantage_key = self.resolve("ALPHAVANTAGE_API_KEY"); self.openfigi_key = self.resolve("OPENFIGI_API_KEY"); const env_cache = self.resolve("ZFIN_CACHE_DIR"); self.cache_dir = env_cache orelse blk: { // XDG Base Directory: $XDG_CACHE_HOME/zfin, falling back to $HOME/.cache/zfin const base = std.posix.getenv("XDG_CACHE_HOME") orelse fallback: { const home = std.posix.getenv("HOME") orelse "/tmp"; break :fallback std.fs.path.join(allocator, &.{ home, ".cache" }) catch @panic("OOM"); }; const base_allocated = std.posix.getenv("XDG_CACHE_HOME") == null; defer if (base_allocated) allocator.free(base); break :blk std.fs.path.join(allocator, &.{ base, "zfin" }) catch @panic("OOM"); }; return self; } pub fn deinit(self: *Config) void { if (self.allocator) |a| { // cache_dir is allocated (via path.join) unless ZFIN_CACHE_DIR was set directly. // Check BEFORE freeing env_map/env_buf, since resolve() reads from them. const cache_dir_from_env = self.resolve("ZFIN_CACHE_DIR") != null; if (self.env_map) |*m| { var map = m.*; map.deinit(); } if (self.env_buf) |buf| a.free(buf); if (!cache_dir_from_env) { a.free(self.cache_dir); } } } pub fn hasAnyKey(self: Config) bool { return self.twelvedata_key != null or self.polygon_key != null or self.finnhub_key != null or self.alphavantage_key != null; } /// Look up a key: environment variable first, then .env file fallback. fn resolve(self: Config, key: []const u8) ?[]const u8 { if (std.posix.getenv(key)) |v| return v; if (self.env_map) |m| return m.get(key); return null; } }; /// Parse all KEY=VALUE pairs from .env content into a HashMap. /// Values are slices into the original buffer (no extra allocations per entry). fn parseEnvFile(allocator: std.mem.Allocator, data: []const u8) ?EnvMap { var map = EnvMap.init(allocator); var iter = std.mem.splitScalar(u8, data, '\n'); while (iter.next()) |line| { const trimmed = std.mem.trim(u8, line, &std.ascii.whitespace); if (trimmed.len == 0 or trimmed[0] == '#') continue; if (std.mem.indexOfScalar(u8, trimmed, '=')) |eq| { const k = std.mem.trim(u8, trimmed[0..eq], &std.ascii.whitespace); const v = std.mem.trim(u8, trimmed[eq + 1 ..], &std.ascii.whitespace); map.put(k, v) catch return null; } } return map; }