migrate cache to new cli framework
This commit is contained in:
parent
9813d951d9
commit
722ddc0e98
2 changed files with 239 additions and 141 deletions
|
|
@ -1,11 +1,43 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const zfin = @import("../root.zig");
|
const zfin = @import("../root.zig");
|
||||||
const cli = @import("common.zig");
|
const cli = @import("common.zig");
|
||||||
|
const framework = @import("framework.zig");
|
||||||
const srf = @import("srf");
|
const srf = @import("srf");
|
||||||
|
|
||||||
const Store = zfin.cache.Store;
|
const Store = zfin.cache.Store;
|
||||||
const DataType = zfin.cache.DataType;
|
const DataType = zfin.cache.DataType;
|
||||||
|
|
||||||
|
pub const Subcommand = enum { stats, clear };
|
||||||
|
|
||||||
|
pub const ParsedArgs = struct {
|
||||||
|
sub: Subcommand,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const meta = struct {
|
||||||
|
pub const name: []const u8 = "cache";
|
||||||
|
pub const group: framework.Group = .infra;
|
||||||
|
pub const synopsis: []const u8 = "Inspect or clear the local provider-data cache";
|
||||||
|
pub const help: []const u8 =
|
||||||
|
\\Usage: zfin cache <stats|clear>
|
||||||
|
\\
|
||||||
|
\\Subcommands:
|
||||||
|
\\ stats List every cached symbol with per-data-type size,
|
||||||
|
\\ age, and freshness state. Stale entries (past TTL)
|
||||||
|
\\ are flagged. Includes the cusip_tickers.srf file
|
||||||
|
\\ if present.
|
||||||
|
\\ clear Delete every file under the cache directory.
|
||||||
|
\\ No confirmation; the next provider call will
|
||||||
|
\\ re-fetch everything.
|
||||||
|
\\
|
||||||
|
\\Cache directory is `$ZFIN_CACHE_DIR` if set, else `~/.cache/zfin`.
|
||||||
|
\\
|
||||||
|
;
|
||||||
|
};
|
||||||
|
|
||||||
|
comptime {
|
||||||
|
framework.validateCommandModule(@This());
|
||||||
|
}
|
||||||
|
|
||||||
/// Data types to show in the stats table (skip candles_meta and meta — internal bookkeeping).
|
/// Data types to show in the stats table (skip candles_meta and meta — internal bookkeeping).
|
||||||
const display_types = [_]DataType{
|
const display_types = [_]DataType{
|
||||||
.candles_daily,
|
.candles_daily,
|
||||||
|
|
@ -25,10 +57,45 @@ const display_labels = [_][]const u8{
|
||||||
"etf_profile",
|
"etf_profile",
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn run(io: std.Io, allocator: std.mem.Allocator, config: zfin.Config, subcommand: []const u8, out: *std.Io.Writer) !void {
|
pub fn parseArgs(ctx: *framework.RunCtx, cmd_args: []const []const u8) !ParsedArgs {
|
||||||
if (std.mem.eql(u8, subcommand, "stats")) {
|
if (cmd_args.len < 1) {
|
||||||
|
try cli.stderrPrint(ctx.io, "Error: 'cache' requires a subcommand (stats, clear)\n");
|
||||||
|
return error.MissingSubcommand;
|
||||||
|
}
|
||||||
|
if (cmd_args.len > 1) {
|
||||||
|
try cli.stderrPrint(ctx.io, "Error: 'cache' takes a single subcommand\n");
|
||||||
|
return error.UnexpectedArg;
|
||||||
|
}
|
||||||
|
const sub_str = cmd_args[0];
|
||||||
|
if (std.mem.eql(u8, sub_str, "stats")) {
|
||||||
|
return .{ .sub = .stats };
|
||||||
|
}
|
||||||
|
if (std.mem.eql(u8, sub_str, "clear")) {
|
||||||
|
return .{ .sub = .clear };
|
||||||
|
}
|
||||||
|
try cli.stderrPrint(ctx.io, "Error: unknown cache subcommand '");
|
||||||
|
try cli.stderrPrint(ctx.io, sub_str);
|
||||||
|
try cli.stderrPrint(ctx.io, "'. Use 'stats' or 'clear'.\n");
|
||||||
|
return error.UnknownSubcommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(ctx: *framework.RunCtx, parsed: ParsedArgs) !void {
|
||||||
|
switch (parsed.sub) {
|
||||||
|
.stats => try runStats(ctx),
|
||||||
|
.clear => try runClear(ctx),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn runStats(ctx: *framework.RunCtx) !void {
|
||||||
|
const io = ctx.io;
|
||||||
|
const allocator = ctx.allocator;
|
||||||
|
const config = ctx.config;
|
||||||
|
const out = ctx.out;
|
||||||
// Capture wall-clock once per invocation so every "X ago" display
|
// Capture wall-clock once per invocation so every "X ago" display
|
||||||
// line in the table is computed against the same reference point.
|
// line in the table is computed against the same reference point.
|
||||||
|
//
|
||||||
|
// wall-clock required: human-readable "age" rendering needs the
|
||||||
|
// current time rather than the per-invocation `today`.
|
||||||
const now_s = std.Io.Timestamp.now(io, .real).toSeconds();
|
const now_s = std.Io.Timestamp.now(io, .real).toSeconds();
|
||||||
try out.print("Cache directory: {s}\n\n", .{config.cache_dir});
|
try out.print("Cache directory: {s}\n\n", .{config.cache_dir});
|
||||||
|
|
||||||
|
|
@ -146,13 +213,12 @@ pub fn run(io: std.Io, allocator: std.mem.Allocator, config: zfin.Config, subcom
|
||||||
total_files,
|
total_files,
|
||||||
formatSize(&total_buf, total_size),
|
formatSize(&total_buf, total_size),
|
||||||
});
|
});
|
||||||
} else if (std.mem.eql(u8, subcommand, "clear")) {
|
|
||||||
var store = Store.init(io, allocator, config.cache_dir);
|
|
||||||
try store.clearAll();
|
|
||||||
try out.writeAll("Cache cleared.\n");
|
|
||||||
} else {
|
|
||||||
try cli.stderrPrint(io, "Unknown cache subcommand. Use 'stats' or 'clear'.\n");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn runClear(ctx: *framework.RunCtx) !void {
|
||||||
|
var store = Store.init(ctx.io, ctx.allocator, ctx.config.cache_dir);
|
||||||
|
try store.clearAll();
|
||||||
|
try ctx.out.writeAll("Cache cleared.\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
const FileInfo = struct {
|
const FileInfo = struct {
|
||||||
|
|
@ -253,6 +319,43 @@ fn formatAge(buf: *[24]u8, before_s: i64, after_s: i64) []const u8 {
|
||||||
|
|
||||||
// ── Tests ────────────────────────────────────────────────────
|
// ── Tests ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
test "parseArgs: 'stats' resolves to .stats" {
|
||||||
|
var ctx: framework.RunCtx = undefined;
|
||||||
|
ctx.io = std.testing.io;
|
||||||
|
const args = [_][]const u8{"stats"};
|
||||||
|
const parsed = try parseArgs(&ctx, &args);
|
||||||
|
try std.testing.expectEqual(Subcommand.stats, parsed.sub);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parseArgs: 'clear' resolves to .clear" {
|
||||||
|
var ctx: framework.RunCtx = undefined;
|
||||||
|
ctx.io = std.testing.io;
|
||||||
|
const args = [_][]const u8{"clear"};
|
||||||
|
const parsed = try parseArgs(&ctx, &args);
|
||||||
|
try std.testing.expectEqual(Subcommand.clear, parsed.sub);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parseArgs: missing subcommand errors" {
|
||||||
|
var ctx: framework.RunCtx = undefined;
|
||||||
|
ctx.io = std.testing.io;
|
||||||
|
const args = [_][]const u8{};
|
||||||
|
try std.testing.expectError(error.MissingSubcommand, parseArgs(&ctx, &args));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parseArgs: unknown subcommand errors" {
|
||||||
|
var ctx: framework.RunCtx = undefined;
|
||||||
|
ctx.io = std.testing.io;
|
||||||
|
const args = [_][]const u8{"bogus"};
|
||||||
|
try std.testing.expectError(error.UnknownSubcommand, parseArgs(&ctx, &args));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "parseArgs: extra args error" {
|
||||||
|
var ctx: framework.RunCtx = undefined;
|
||||||
|
ctx.io = std.testing.io;
|
||||||
|
const args = [_][]const u8{ "stats", "extra" };
|
||||||
|
try std.testing.expectError(error.UnexpectedArg, parseArgs(&ctx, &args));
|
||||||
|
}
|
||||||
|
|
||||||
test "formatAge: future timestamp renders 'just now'" {
|
test "formatAge: future timestamp renders 'just now'" {
|
||||||
var buf: [24]u8 = undefined;
|
var buf: [24]u8 = undefined;
|
||||||
try std.testing.expectEqualStrings("just now", formatAge(&buf, 1_700_000_100, 1_700_000_000));
|
try std.testing.expectEqualStrings("just now", formatAge(&buf, 1_700_000_100, 1_700_000_000));
|
||||||
|
|
|
||||||
67
src/main.zig
67
src/main.zig
|
|
@ -2,6 +2,37 @@ const std = @import("std");
|
||||||
const zfin = @import("root.zig");
|
const zfin = @import("root.zig");
|
||||||
const tui = @import("tui.zig");
|
const tui = @import("tui.zig");
|
||||||
const cli = @import("commands/common.zig");
|
const cli = @import("commands/common.zig");
|
||||||
|
const cmd_framework = @import("commands/framework.zig");
|
||||||
|
|
||||||
|
/// Comptime registry of CLI commands. Field name is the user-facing
|
||||||
|
/// subcommand name; value is the imported module struct. Order
|
||||||
|
/// follows the canonical group taxonomy in `framework.Group`
|
||||||
|
/// (symbol-lookup → portfolio → time-series → hygiene → infra) so
|
||||||
|
/// `zfin help` reads in workflow order. Adding a new command is one
|
||||||
|
/// edit here (after authoring the module). Validation runs at
|
||||||
|
/// comptime in the block below.
|
||||||
|
const command_modules = .{
|
||||||
|
// Per-symbol lookups
|
||||||
|
.perf = @import("commands/perf.zig"),
|
||||||
|
.quote = @import("commands/quote.zig"),
|
||||||
|
.divs = @import("commands/divs.zig"),
|
||||||
|
.splits = @import("commands/splits.zig"),
|
||||||
|
.options = @import("commands/options.zig"),
|
||||||
|
.earnings = @import("commands/earnings.zig"),
|
||||||
|
.etf = @import("commands/etf.zig"),
|
||||||
|
|
||||||
|
// Data hygiene
|
||||||
|
.lookup = @import("commands/lookup.zig"),
|
||||||
|
|
||||||
|
// Infrastructure
|
||||||
|
.cache = @import("commands/cache.zig"),
|
||||||
|
};
|
||||||
|
|
||||||
|
comptime {
|
||||||
|
for (std.meta.fields(@TypeOf(command_modules))) |f| {
|
||||||
|
cmd_framework.validateCommandModule(@field(command_modules, f.name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const usage =
|
const usage =
|
||||||
\\Usage: zfin [global options] <command> [command options]
|
\\Usage: zfin [global options] <command> [command options]
|
||||||
|
|
@ -486,12 +517,6 @@ fn runCli(init: std.process.Init) !u8 {
|
||||||
defer if (wl.resolved) |r| r.deinit(allocator);
|
defer if (wl.resolved) |r| r.deinit(allocator);
|
||||||
const wl_path: ?[]const u8 = if (globals.watchlist_path != null or wl.resolved != null) wl.path else null;
|
const wl_path: ?[]const u8 = if (globals.watchlist_path != null or wl.resolved != null) wl.path else null;
|
||||||
try commands.portfolio.run(io, allocator, &svc, pf.path, wl_path, force_refresh, today, color, out);
|
try commands.portfolio.run(io, allocator, &svc, pf.path, wl_path, force_refresh, today, color, out);
|
||||||
} else if (std.mem.eql(u8, command, "cache")) {
|
|
||||||
if (cmd_args.len < 1) {
|
|
||||||
try cli.stderrPrint(io, "Error: 'cache' requires a subcommand (stats, clear)\n");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
try commands.cache.run(io, allocator, config, cmd_args[0], out);
|
|
||||||
} else if (std.mem.eql(u8, command, "enrich")) {
|
} else if (std.mem.eql(u8, command, "enrich")) {
|
||||||
if (cmd_args.len < 1) {
|
if (cmd_args.len < 1) {
|
||||||
try cli.stderrPrint(io, "Error: 'enrich' requires a portfolio file path or symbol\n");
|
try cli.stderrPrint(io, "Error: 'enrich' requires a portfolio file path or symbol\n");
|
||||||
|
|
@ -797,36 +822,6 @@ fn globalOffender(args: []const []const u8) ?[]const u8 {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Command modules ──────────────────────────────────────────
|
|
||||||
const cmd_framework = @import("commands/framework.zig");
|
|
||||||
|
|
||||||
/// Comptime registry of CLI commands that have migrated to the
|
|
||||||
/// framework contract. The dispatcher in `runCli` walks this with
|
|
||||||
/// `inline for`; the field name is the user-facing command name
|
|
||||||
/// and the value is the imported module struct. Each registered
|
|
||||||
/// module satisfies `framework.validateCommandModule` (enforced at
|
|
||||||
/// the registry's comptime block below).
|
|
||||||
///
|
|
||||||
/// During the migration window, this registry coexists with the
|
|
||||||
/// legacy if-else chain in `runCli`. Once every command is
|
|
||||||
/// registered, the legacy chain goes away (commit 17).
|
|
||||||
const command_modules = .{
|
|
||||||
.divs = @import("commands/divs.zig"),
|
|
||||||
.earnings = @import("commands/earnings.zig"),
|
|
||||||
.etf = @import("commands/etf.zig"),
|
|
||||||
.lookup = @import("commands/lookup.zig"),
|
|
||||||
.options = @import("commands/options.zig"),
|
|
||||||
.perf = @import("commands/perf.zig"),
|
|
||||||
.quote = @import("commands/quote.zig"),
|
|
||||||
.splits = @import("commands/splits.zig"),
|
|
||||||
};
|
|
||||||
|
|
||||||
comptime {
|
|
||||||
for (std.meta.fields(@TypeOf(command_modules))) |f| {
|
|
||||||
cmd_framework.validateCommandModule(@field(command_modules, f.name));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const commands = struct {
|
const commands = struct {
|
||||||
const history = @import("commands/history.zig");
|
const history = @import("commands/history.zig");
|
||||||
const portfolio = @import("commands/portfolio.zig");
|
const portfolio = @import("commands/portfolio.zig");
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue