refactor config/add default files
This commit is contained in:
parent
a7abc5f5d7
commit
c938da2e3c
6 changed files with 336 additions and 160 deletions
|
|
@ -29,7 +29,7 @@ repos:
|
|||
- id: test
|
||||
name: Run zig build test
|
||||
entry: zig
|
||||
args: ["build", "coverage", "-Dcoverage-threshold=40"]
|
||||
args: ["build", "coverage", "-Dcoverage-threshold=49"]
|
||||
language: system
|
||||
types: [file]
|
||||
pass_filenames: false
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ zig build # build the zfin binary (output: zig-out/bin/zfin)
|
|||
zig build test # run all tests (single binary, discovers all tests via refAllDeclsRecursive)
|
||||
zig build run -- <args> # build and run CLI
|
||||
zig build docs # generate library documentation
|
||||
zig build coverage -Dcoverage-threshold=40 # run tests with kcov coverage (Linux only)
|
||||
zig build coverage -Dcoverage-threshold=49 # run tests with kcov coverage (Linux only)
|
||||
```
|
||||
|
||||
**Tooling** (managed via `.mise.toml`):
|
||||
|
|
|
|||
332
src/Config.zig
Normal file
332
src/Config.zig
Normal file
|
|
@ -0,0 +1,332 @@
|
|||
//! Runtime configuration: API keys, cache directory, resolved data paths.
|
||||
//!
|
||||
//! File-as-struct: the file *is* the `Config` type. Callers reach it via
|
||||
//! `const Config = @import("Config.zig");` and use it anywhere a struct
|
||||
//! type is expected (`Config.fromEnv(alloc)`, `var c: Config = ...`,
|
||||
//! `Config.ResolvedPath`, `Config.default_portfolio_filename`, etc.).
|
||||
//!
|
||||
//! Configuration sources, in priority order:
|
||||
//! 1. Process environment variables
|
||||
//! 2. `.env` file in the current working directory
|
||||
//! 3. `.env` file in `$ZFIN_HOME`
|
||||
//!
|
||||
//! Cache directory defaults follow XDG: `$ZFIN_CACHE_DIR` > `$XDG_CACHE_HOME/zfin`
|
||||
//! > `$HOME/.cache/zfin`.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const EnvMap = std.StringHashMap([]const u8);
|
||||
|
||||
/// Default filename for the portfolio file when no explicit -p/--portfolio
|
||||
/// is provided. Looked up via `resolveUserFile` (cwd → ZFIN_HOME). Every
|
||||
/// command that loads a portfolio should fall back to this so behavior
|
||||
/// stays consistent.
|
||||
pub const default_portfolio_filename = "portfolio.srf";
|
||||
|
||||
/// Default filename for the watchlist file. Same resolution rules as
|
||||
/// `default_portfolio_filename`.
|
||||
pub const default_watchlist_filename = "watchlist.srf";
|
||||
|
||||
// ── Fields ───────────────────────────────────────────────────
|
||||
|
||||
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
|
||||
zfin_home: ?[]const u8 = null,
|
||||
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,
|
||||
|
||||
// ── Construction / teardown ──────────────────────────────────
|
||||
|
||||
pub fn fromEnv(allocator: std.mem.Allocator) @This() {
|
||||
var self = @This(){
|
||||
.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.zfin_home = self.resolve("ZFIN_HOME");
|
||||
|
||||
// Try loading .env file from ZFIN_HOME as well (cwd .env takes priority)
|
||||
if (self.env_buf == null) {
|
||||
if (self.zfin_home) |home| {
|
||||
const env_path = std.fs.path.join(allocator, &.{ home, ".env" }) catch null;
|
||||
if (env_path) |p| {
|
||||
defer allocator.free(p);
|
||||
self.env_buf = std.fs.cwd().readFileAlloc(allocator, p, 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: *@This()) 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── File resolution ──────────────────────────────────────────
|
||||
|
||||
pub const ResolvedPath = struct {
|
||||
path: []const u8,
|
||||
owned: bool,
|
||||
|
||||
pub fn deinit(self: ResolvedPath, allocator: std.mem.Allocator) void {
|
||||
if (self.owned) allocator.free(self.path);
|
||||
}
|
||||
};
|
||||
|
||||
/// Resolve a user file, trying cwd first then ZFIN_HOME.
|
||||
/// Returns the path to use; caller must call `deinit()` on the result.
|
||||
pub fn resolveUserFile(self: @This(), allocator: std.mem.Allocator, rel_path: []const u8) ?ResolvedPath {
|
||||
if (std.fs.cwd().access(rel_path, .{})) |_| {
|
||||
return .{ .path = rel_path, .owned = false };
|
||||
} else |_| {}
|
||||
|
||||
if (self.zfin_home) |home| {
|
||||
const full = std.fs.path.join(allocator, &.{ home, rel_path }) catch return null;
|
||||
if (std.fs.cwd().access(full, .{})) |_| {
|
||||
return .{ .path = full, .owned = true };
|
||||
} else |_| {
|
||||
allocator.free(full);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── Queries ──────────────────────────────────────────────────
|
||||
|
||||
pub fn hasAnyKey(self: @This()) 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;
|
||||
}
|
||||
|
||||
// ── Internals ────────────────────────────────────────────────
|
||||
|
||||
/// Look up a key: process environment first, then .env file fallback.
|
||||
fn resolve(self: *@This(), 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;
|
||||
}
|
||||
|
||||
// ── Tests ────────────────────────────────────────────────────
|
||||
|
||||
const testing = std.testing;
|
||||
|
||||
test "default_portfolio_filename / default_watchlist_filename are the expected literals" {
|
||||
// Guards against accidental rename; these constants are referenced by
|
||||
// every command that loads a portfolio or watchlist, and changing them
|
||||
// silently would break user setups.
|
||||
try testing.expectEqualStrings("portfolio.srf", default_portfolio_filename);
|
||||
try testing.expectEqualStrings("watchlist.srf", default_watchlist_filename);
|
||||
}
|
||||
|
||||
test "parseEnvFile: basic KEY=VALUE pairs" {
|
||||
const data = "FOO=bar\nBAZ=qux\n";
|
||||
var map = parseEnvFile(testing.allocator, data).?;
|
||||
defer map.deinit();
|
||||
try testing.expectEqualStrings("bar", map.get("FOO").?);
|
||||
try testing.expectEqualStrings("qux", map.get("BAZ").?);
|
||||
try testing.expect(map.get("MISSING") == null);
|
||||
}
|
||||
|
||||
test "parseEnvFile: trims whitespace around key and value" {
|
||||
const data = " SPACED_KEY = spaced value \n";
|
||||
var map = parseEnvFile(testing.allocator, data).?;
|
||||
defer map.deinit();
|
||||
try testing.expectEqualStrings("spaced value", map.get("SPACED_KEY").?);
|
||||
}
|
||||
|
||||
test "parseEnvFile: ignores comments and blank lines" {
|
||||
const data =
|
||||
\\# this is a comment
|
||||
\\
|
||||
\\FOO=bar
|
||||
\\# another comment
|
||||
\\
|
||||
\\BAZ=qux
|
||||
\\
|
||||
;
|
||||
var map = parseEnvFile(testing.allocator, data).?;
|
||||
defer map.deinit();
|
||||
try testing.expectEqual(@as(usize, 2), map.count());
|
||||
try testing.expectEqualStrings("bar", map.get("FOO").?);
|
||||
try testing.expectEqualStrings("qux", map.get("BAZ").?);
|
||||
}
|
||||
|
||||
test "parseEnvFile: lines without '=' are skipped" {
|
||||
const data =
|
||||
\\NOT_A_PAIR
|
||||
\\FOO=bar
|
||||
\\ALSO NOT A PAIR
|
||||
\\BAZ=qux
|
||||
\\
|
||||
;
|
||||
var map = parseEnvFile(testing.allocator, data).?;
|
||||
defer map.deinit();
|
||||
try testing.expectEqual(@as(usize, 2), map.count());
|
||||
try testing.expectEqualStrings("bar", map.get("FOO").?);
|
||||
try testing.expectEqualStrings("qux", map.get("BAZ").?);
|
||||
}
|
||||
|
||||
test "parseEnvFile: value containing '=' keeps everything after the first '='" {
|
||||
const data = "URL=https://example.com/path?q=1&r=2\n";
|
||||
var map = parseEnvFile(testing.allocator, data).?;
|
||||
defer map.deinit();
|
||||
try testing.expectEqualStrings("https://example.com/path?q=1&r=2", map.get("URL").?);
|
||||
}
|
||||
|
||||
test "parseEnvFile: empty input yields empty map" {
|
||||
var map = parseEnvFile(testing.allocator, "").?;
|
||||
defer map.deinit();
|
||||
try testing.expectEqual(@as(usize, 0), map.count());
|
||||
}
|
||||
|
||||
test "hasAnyKey: false when all keys are null" {
|
||||
const c: @This() = .{ .cache_dir = "/tmp" };
|
||||
try testing.expect(!c.hasAnyKey());
|
||||
}
|
||||
|
||||
test "hasAnyKey: true when any single provider key is set" {
|
||||
// Each key should independently flip the result to true. Iterating
|
||||
// through each variant catches a future field addition that forgets
|
||||
// to update hasAnyKey().
|
||||
const KeyField = enum { tiingo, twelvedata, polygon, finnhub, alphavantage };
|
||||
for ([_]KeyField{ .tiingo, .twelvedata, .polygon, .finnhub, .alphavantage }) |which| {
|
||||
var c: @This() = .{ .cache_dir = "/tmp" };
|
||||
switch (which) {
|
||||
.tiingo => c.tiingo_key = "abc",
|
||||
.twelvedata => c.twelvedata_key = "abc",
|
||||
.polygon => c.polygon_key = "abc",
|
||||
.finnhub => c.finnhub_key = "abc",
|
||||
.alphavantage => c.alphavantage_key = "abc",
|
||||
}
|
||||
try testing.expect(c.hasAnyKey());
|
||||
}
|
||||
}
|
||||
|
||||
test "hasAnyKey: openfigi / server_url don't count as provider keys" {
|
||||
// hasAnyKey is about candle/quote providers; OpenFIGI is CUSIP lookup
|
||||
// and server_url is the sync server. Neither should flip hasAnyKey.
|
||||
var c: @This() = .{ .cache_dir = "/tmp" };
|
||||
c.openfigi_key = "fg";
|
||||
c.server_url = "https://example.com";
|
||||
try testing.expect(!c.hasAnyKey());
|
||||
}
|
||||
|
||||
test "ResolvedPath.deinit: frees when owned, no-op when not owned" {
|
||||
const allocator = testing.allocator;
|
||||
|
||||
// Not-owned: a static literal must NOT be freed. The testing allocator
|
||||
// would panic if we tried to free a non-allocation — success here is
|
||||
// the test returning normally.
|
||||
const rp_static: ResolvedPath = .{ .path = "portfolio.srf", .owned = false };
|
||||
rp_static.deinit(allocator);
|
||||
|
||||
// Owned: allocator-allocated path is freed by deinit.
|
||||
const owned = try allocator.dupe(u8, "/tmp/portfolio.srf");
|
||||
const rp_owned: ResolvedPath = .{ .path = owned, .owned = true };
|
||||
rp_owned.deinit(allocator);
|
||||
// (If this leaked, the test allocator would fail the test.)
|
||||
}
|
||||
|
||||
test "resolve: env_map fallback when allocator is null (skips process env)" {
|
||||
// Setting allocator=null disables the getEnvVarOwned branch, so the
|
||||
// lookup must come from env_map alone. This exercises the .env-only
|
||||
// code path without depending on host environment variables.
|
||||
var map = EnvMap.init(testing.allocator);
|
||||
defer map.deinit();
|
||||
try map.put("FOO", "from-env-file");
|
||||
|
||||
var c: @This() = .{
|
||||
.cache_dir = "/tmp",
|
||||
.env_map = map,
|
||||
.allocator = null,
|
||||
};
|
||||
try testing.expectEqualStrings("from-env-file", c.resolve("FOO").?);
|
||||
try testing.expect(c.resolve("MISSING") == null);
|
||||
}
|
||||
|
||||
test "resolve: allocator=null and env_map=null returns null" {
|
||||
var c: @This() = .{ .cache_dir = "/tmp" };
|
||||
try testing.expect(c.resolve("ANYTHING") == null);
|
||||
}
|
||||
156
src/config.zig
156
src/config.zig
|
|
@ -1,156 +0,0 @@
|
|||
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
|
||||
zfin_home: ?[]const u8 = null,
|
||||
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.zfin_home = self.resolve("ZFIN_HOME");
|
||||
|
||||
// Try loading .env file from ZFIN_HOME as well (cwd .env takes priority)
|
||||
if (self.env_buf == null) {
|
||||
if (self.zfin_home) |home| {
|
||||
const env_path = std.fs.path.join(allocator, &.{ home, ".env" }) catch null;
|
||||
if (env_path) |p| {
|
||||
defer allocator.free(p);
|
||||
self.env_buf = std.fs.cwd().readFileAlloc(allocator, p, 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 const ResolvedPath = struct {
|
||||
path: []const u8,
|
||||
owned: bool,
|
||||
|
||||
pub fn deinit(self: ResolvedPath, allocator: std.mem.Allocator) void {
|
||||
if (self.owned) allocator.free(self.path);
|
||||
}
|
||||
};
|
||||
|
||||
/// Resolve a user file, trying cwd first then ZFIN_HOME.
|
||||
/// Returns the path to use; caller must call `deinit()` on the result.
|
||||
pub fn resolveUserFile(self: Config, allocator: std.mem.Allocator, rel_path: []const u8) ?ResolvedPath {
|
||||
if (std.fs.cwd().access(rel_path, .{})) |_| {
|
||||
return .{ .path = rel_path, .owned = false };
|
||||
} else |_| {}
|
||||
|
||||
if (self.zfin_home) |home| {
|
||||
const full = std.fs.path.join(allocator, &.{ home, rel_path }) catch return null;
|
||||
if (std.fs.cwd().access(full, .{})) |_| {
|
||||
return .{ .path = full, .owned = true };
|
||||
} else |_| {
|
||||
allocator.free(full);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
@ -60,7 +60,7 @@ pub const Quote = @import("models/quote.zig").Quote;
|
|||
// ── Infrastructure ───────────────────────────────────────────
|
||||
|
||||
/// Runtime configuration loaded from environment / .env file (API keys, paths).
|
||||
pub const Config = @import("config.zig").Config;
|
||||
pub const Config = @import("Config.zig");
|
||||
|
||||
// ── Cache ────────────────────────────────────────────────────
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ const OptionsChain = @import("models/option.zig").OptionsChain;
|
|||
const EarningsEvent = @import("models/earnings.zig").EarningsEvent;
|
||||
const Quote = @import("models/quote.zig").Quote;
|
||||
const EtfProfile = @import("models/etf_profile.zig").EtfProfile;
|
||||
const Config = @import("config.zig").Config;
|
||||
const Config = @import("Config.zig");
|
||||
const cache = @import("cache/store.zig");
|
||||
const srf = @import("srf");
|
||||
const analysis = @import("analytics/analysis.zig");
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue