config.zig refactorings

This commit is contained in:
Emil Lerch 2026-03-03 13:14:07 -08:00
parent f936531721
commit 25a7d06d40
Signed by: lobo
GPG key ID: A7B62D657EF764F8

View file

@ -1,5 +1,7 @@
const std = @import("std"); const std = @import("std");
const EnvMap = std.StringHashMap([]const u8);
pub const Config = struct { pub const Config = struct {
twelvedata_key: ?[]const u8 = null, twelvedata_key: ?[]const u8 = null,
polygon_key: ?[]const u8 = null, polygon_key: ?[]const u8 = null,
@ -8,7 +10,10 @@ pub const Config = struct {
openfigi_key: ?[]const u8 = null, openfigi_key: ?[]const u8 = null,
cache_dir: []const u8, cache_dir: []const u8,
allocator: ?std.mem.Allocator = null, allocator: ?std.mem.Allocator = null,
/// Raw .env file contents (keys/values in env_map point into this).
env_buf: ?[]const u8 = null, env_buf: ?[]const u8 = null,
/// Parsed KEY=VALUE pairs from .env file.
env_map: ?EnvMap = null,
pub fn fromEnv(allocator: std.mem.Allocator) Config { pub fn fromEnv(allocator: std.mem.Allocator) Config {
var self = Config{ var self = Config{
@ -16,8 +21,11 @@ pub const Config = struct {
.allocator = allocator, .allocator = allocator,
}; };
// Try loading .env file from the project directory or home directory // Try loading .env file from the current working directory
self.env_buf = loadEnvFile(allocator); 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.twelvedata_key = self.resolve("TWELVEDATA_API_KEY");
self.polygon_key = self.resolve("POLYGON_API_KEY"); self.polygon_key = self.resolve("POLYGON_API_KEY");
@ -27,8 +35,14 @@ pub const Config = struct {
const env_cache = self.resolve("ZFIN_CACHE_DIR"); const env_cache = self.resolve("ZFIN_CACHE_DIR");
self.cache_dir = env_cache orelse blk: { self.cache_dir = env_cache orelse blk: {
const home = std.posix.getenv("HOME") orelse "/tmp"; // XDG Base Directory: $XDG_CACHE_HOME/zfin, falling back to $HOME/.cache/zfin
break :blk std.fs.path.join(allocator, &.{ home, ".cache", "zfin" }) catch @panic("OOM"); 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; return self;
@ -36,8 +50,13 @@ pub const Config = struct {
pub fn deinit(self: *Config) void { pub fn deinit(self: *Config) void {
if (self.allocator) |a| { if (self.allocator) |a| {
// Check if cache_dir was allocated (not from env/envfile) BEFORE freeing env_buf // 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; 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 (self.env_buf) |buf| a.free(buf);
if (!cache_dir_from_env) { if (!cache_dir_from_env) {
a.free(self.cache_dir); a.free(self.cache_dir);
@ -55,41 +74,24 @@ pub const Config = struct {
/// Look up a key: environment variable first, then .env file fallback. /// Look up a key: environment variable first, then .env file fallback.
fn resolve(self: Config, key: []const u8) ?[]const u8 { fn resolve(self: Config, key: []const u8) ?[]const u8 {
if (std.posix.getenv(key)) |v| return v; if (std.posix.getenv(key)) |v| return v;
return envFileGet(self.env_buf, key); if (self.env_map) |m| return m.get(key);
return null;
} }
}; };
/// Parse a KEY=VALUE line from .env content. Returns value for the given key. /// Parse all KEY=VALUE pairs from .env content into a HashMap.
fn envFileGet(buf: ?[]const u8, key: []const u8) ?[]const u8 { /// Values are slices into the original buffer (no extra allocations per entry).
const data = buf orelse return null; fn parseEnvFile(allocator: std.mem.Allocator, data: []const u8) ?EnvMap {
var map = EnvMap.init(allocator);
var iter = std.mem.splitScalar(u8, data, '\n'); var iter = std.mem.splitScalar(u8, data, '\n');
while (iter.next()) |line| { while (iter.next()) |line| {
const trimmed = std.mem.trim(u8, line, &std.ascii.whitespace); const trimmed = std.mem.trim(u8, line, &std.ascii.whitespace);
if (trimmed.len == 0 or trimmed[0] == '#') continue; if (trimmed.len == 0 or trimmed[0] == '#') continue;
if (std.mem.indexOfScalar(u8, trimmed, '=')) |eq| { if (std.mem.indexOfScalar(u8, trimmed, '=')) |eq| {
const k = std.mem.trim(u8, trimmed[0..eq], &std.ascii.whitespace); const k = std.mem.trim(u8, trimmed[0..eq], &std.ascii.whitespace);
if (std.mem.eql(u8, k, key)) { const v = std.mem.trim(u8, trimmed[eq + 1 ..], &std.ascii.whitespace);
return std.mem.trim(u8, trimmed[eq + 1 ..], &std.ascii.whitespace); map.put(k, v) catch return null;
}
} }
} }
return null; return map;
}
/// Try to load .env from the executable's directory, then cwd.
fn loadEnvFile(allocator: std.mem.Allocator) ?[]const u8 {
// Try relative to the executable
const exe_dir = std.fs.selfExeDirPathAlloc(allocator) catch null;
defer if (exe_dir) |d| allocator.free(d);
if (exe_dir) |dir| {
const path = std.fs.path.join(allocator, &.{ dir, "..", ".env" }) catch null;
defer if (path) |p| allocator.free(p);
if (path) |p| {
if (std.fs.cwd().readFileAlloc(allocator, p, 4096)) |data| return data else |_| {}
}
}
// Try cwd
return std.fs.cwd().readFileAlloc(allocator, ".env", 4096) catch null;
} }