//! zfin-server — HTTP data service backed by zfin's provider infrastructure. //! //! Two modes: //! zfin-server serve [--port=8080] Start the HTTP server //! zfin-server refresh Refresh cache for all tracked symbols (for cron) //! //! See GET /help for endpoint documentation. const std = @import("std"); const httpz = @import("httpz"); const build_options = @import("build_options"); const version = build_options.version; const log = std.log.scoped(.@"zfin-server"); const App = @import("App.zig").App; const handlers = @import("handlers.zig"); const refresh_cmd = @import("refresh.zig"); // ── Main ───────────────────────────────────────────────────── pub fn main(init: std.process.Init) !u8 { const allocator = init.gpa; const io = init.io; const environ = init.environ_map; const args = try init.minimal.args.toSlice(allocator); defer allocator.free(args); if (args.len < 2) { try printUsage(io); return 1; } const command = args[1]; if (std.mem.eql(u8, command, "serve")) { var port: u16 = 8080; for (args[2..]) |arg| { if (std.mem.startsWith(u8, arg, "--port=")) { port = std.fmt.parseInt(u16, arg["--port=".len..], 10) catch 8080; } } var app = App.init(io, allocator, environ); defer app.deinit(); var server = try httpz.Server(*App).init(io, allocator, .{ .address = .all(port), }, &app); defer { server.stop(); server.deinit(); } var router = try server.router(.{}); // Static routes router.get("/", handlers.handleIndex, .{}); router.get("/help", handlers.handleHelp, .{}); router.get("/symbols", handlers.handleSymbols, .{}); // POST, not GET: this spends provider quota and writes the cache, so it is // not `safe` in the HTTP sense and must not sit on a verb that proxies, // prefetchers and link checkers feel free to replay. (`/:symbol/watch` // stays GET only because LibreOffice's WEBSERVICE cannot issue anything // else - a constraint, not a precedent.) router.post("/refresh", handlers.handleRefresh, .{}); // Symbol routes router.get("/:symbol/returns", handlers.handleReturns, .{}); // Authenticated explicit watchlist add (API key required - not on // the public allowlist). Distinct from the UA-gated // /:symbol/returns?watch=true path used by LibreOffice. router.get("/:symbol/watch", handlers.handleWatch, .{}); router.get("/:symbol/quote", handlers.handleQuote, .{}); router.get("/:symbol/candles", handlers.handleCandles, .{}); router.get("/:symbol/candles_meta", handlers.handleCandlesMeta, .{}); router.get("/:symbol/diagnostics", handlers.handleDiagnostics, .{}); router.get("/:symbol/dividends", handlers.handleDividends, .{}); router.get("/:symbol/splits", handlers.handleSplits, .{}); router.get("/:symbol/earnings", handlers.handleEarnings, .{}); router.get("/:symbol/options", handlers.handleOptions, .{}); // Wikidata + EDGAR derived data — populated by `refresh`. router.get("/:symbol/classification", handlers.handleClassification, .{}); router.get("/:symbol/etf_metrics", handlers.handleEtfMetrics, .{}); router.get("/:cik/entity_facts", handlers.handleEntityFacts, .{}); // EDGAR shared ticker maps (~3-5 MB each, refreshed // every 30 days). Static-key routes — single file // shared across every symbol lookup. router.get("/_edgar/tickers_funds", handlers.handleTickersFunds, .{}); router.get("/_edgar/tickers_companies", handlers.handleTickersCompanies, .{}); log.info("zfin-server {s}", .{version}); log.info("listening on port {d}", .{port}); try server.listen(); } else if (std.mem.eql(u8, command, "refresh")) { return try refresh_cmd.refresh(io, init.arena.allocator(), environ); } else { try printUsage(io); } return 0; } fn printUsage(io: std.Io) !void { var buf: [2048]u8 = undefined; var fw = std.Io.File.stderr().writer(io, &buf); const w = &fw.interface; try w.print("zfin-server {s}\n", .{version}); try w.writeAll( \\Usage: zfin-server \\ \\Commands: \\ serve [--port=8080] Start the HTTP server \\ refresh Refresh cache for all tracked symbols \\ \\Environment: \\ ZFIN_PORTFOLIO Path to portfolio SRF file (default: portfolio.srf) \\ ZFIN_SERVER_API_KEY If set, require this key on every endpoint \\ except /, /help, and /:symbol/returns. Supply it \\ as an X-API-Key header or ?api_key= query param. \\ TWELVEDATA_API_KEY TwelveData API key \\ POLYGON_API_KEY Polygon API key \\ FINNHUB_API_KEY Finnhub API key \\ ALPHAVANTAGE_API_KEY Alpha Vantage API key \\ \\refresh exit codes: \\ 0 all tracked symbols current \\ 75 provider data lag (a just-closed bar not yet posted) - retry soon \\ 1 one or more hard failures \\ ); try w.flush(); } // ── Tests ──────────────────────────────────────────────────── test { // Pulls each module's tests into the build. NOT optional bookkeeping: a test // build never analyses `main`, so nothing else references these files, and // without this the runner reports "test success" having executed ZERO tests. // That is the worst possible failure - a green tick read as coverage - and it // is exactly what the first attempt at this split produced. _ = @import("App.zig"); _ = @import("handlers.zig"); _ = @import("refresh.zig"); }