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, tiingo_key: ?[]const u8 = null, openfigi_key: ?[]const u8 = null, /// URL of a zfin-server instance for lazy cache sync (e.g. "https://zfin.lerch.org") server_url: ?[]const u8 = null, cache_dir: []const u8, cache_dir_owned: bool = false, // true when cache_dir was allocated via path.join 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, /// Strings allocated by resolve() from process environment variables. env_owned: std.ArrayList([]const u8) = .empty, 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.tiingo_key = self.resolve("TIINGO_API_KEY"); self.openfigi_key = self.resolve("OPENFIGI_API_KEY"); self.server_url = self.resolve("ZFIN_SERVER"); const env_cache = self.resolve("ZFIN_CACHE_DIR"); self.cache_dir = env_cache orelse blk: { self.cache_dir_owned = true; // XDG Base Directory: $XDG_CACHE_HOME/zfin, falling back to $HOME/.cache/zfin const xdg = self.resolve("XDG_CACHE_HOME"); const base = xdg orelse fallback: { const home = self.resolve("HOME") orelse "/tmp"; break :fallback std.fs.path.join(allocator, &.{ home, ".cache" }) catch @panic("OOM"); }; const base_allocated = xdg == 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| { if (self.env_map) |*m| { var map = m.*; map.deinit(); } if (self.env_buf) |buf| a.free(buf); for (self.env_owned.items) |s| a.free(s); self.env_owned.deinit(a); if (self.cache_dir_owned) { 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 or self.tiingo_key != null; } /// Look up a key: process environment first, then .env file fallback. fn resolve(self: *Config, key: []const u8) ?[]const u8 { if (self.allocator) |a| { if (std.process.getEnvVarOwned(a, key)) |v| { self.env_owned.append(a, v) catch {}; return v; } else |_| {} } 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; }