From 26b831631deedc08da50701664c46e9827639b05 Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Fri, 6 Mar 2026 15:56:09 -0800 Subject: [PATCH] remove unused provider interface stuff --- src/providers/alphavantage.zig | 80 +++++++----------- src/providers/cboe.zig | 45 +++++----- src/providers/finnhub.zig | 49 +++-------- src/providers/json_utils.zig | 14 +--- src/providers/polygon.zig | 115 +++++++++----------------- src/providers/provider.zig | 147 --------------------------------- src/providers/twelvedata.zig | 77 +++++------------ 7 files changed, 123 insertions(+), 404 deletions(-) delete mode 100644 src/providers/provider.zig diff --git a/src/providers/alphavantage.zig b/src/providers/alphavantage.zig index f23dbbe..a1ce919 100644 --- a/src/providers/alphavantage.zig +++ b/src/providers/alphavantage.zig @@ -13,10 +13,8 @@ const Date = @import("../models/date.zig").Date; const EtfProfile = @import("../models/etf_profile.zig").EtfProfile; const Holding = @import("../models/etf_profile.zig").Holding; const SectorWeight = @import("../models/etf_profile.zig").SectorWeight; -const provider = @import("provider.zig"); const json_utils = @import("json_utils.zig"); const jsonStr = json_utils.jsonStr; -const mapHttpError = json_utils.mapHttpError; const base_url = "https://www.alphavantage.co/query"; @@ -116,7 +114,7 @@ test "parseEtfProfileResponse error response" { const allocator = std.testing.allocator; const result = parseEtfProfileResponse(allocator, body, "BAD"); - try std.testing.expectError(provider.ProviderError.RequestFailed, result); + try std.testing.expectError(error.RequestFailed, result); } test "parseEtfProfileResponse rate limited" { @@ -126,7 +124,7 @@ test "parseEtfProfileResponse rate limited" { const allocator = std.testing.allocator; const result = parseEtfProfileResponse(allocator, body, "SPY"); - try std.testing.expectError(provider.ProviderError.RateLimited, result); + try std.testing.expectError(error.RateLimited, result); } test "parseCompanyOverview basic" { @@ -201,17 +199,17 @@ pub const AlphaVantage = struct { self: *AlphaVantage, allocator: std.mem.Allocator, symbol: []const u8, - ) provider.ProviderError!CompanyOverview { + ) !CompanyOverview { self.rate_limiter.acquire(); - const url = http.buildUrl(allocator, base_url, &.{ + const url = try http.buildUrl(allocator, base_url, &.{ .{ "function", "OVERVIEW" }, .{ "symbol", symbol }, .{ "apikey", self.api_key }, - }) catch return provider.ProviderError.OutOfMemory; + }); defer allocator.free(url); - var response = self.client.get(url) catch |err| return mapHttpError(err); + var response = try self.client.get(url); defer response.deinit(); return parseCompanyOverview(allocator, response.body, symbol); @@ -222,41 +220,21 @@ pub const AlphaVantage = struct { self: *AlphaVantage, allocator: std.mem.Allocator, symbol: []const u8, - ) provider.ProviderError!EtfProfile { + ) !EtfProfile { self.rate_limiter.acquire(); - const url = http.buildUrl(allocator, base_url, &.{ + const url = try http.buildUrl(allocator, base_url, &.{ .{ "function", "ETF_PROFILE" }, .{ "symbol", symbol }, .{ "apikey", self.api_key }, - }) catch return provider.ProviderError.OutOfMemory; + }); defer allocator.free(url); - var response = self.client.get(url) catch |err| return mapHttpError(err); + var response = try self.client.get(url); defer response.deinit(); return parseEtfProfileResponse(allocator, response.body, symbol); } - - pub fn asProvider(self: *AlphaVantage) provider.Provider { - return .{ - .ptr = @ptrCast(self), - .vtable = &vtable, - }; - } - - const vtable = provider.Provider.VTable{ - .fetchEtfProfile = @ptrCast(&fetchEtfProfileVtable), - .name = .alphavantage, - }; - - fn fetchEtfProfileVtable( - ptr: *AlphaVantage, - allocator: std.mem.Allocator, - symbol: []const u8, - ) provider.ProviderError!EtfProfile { - return ptr.fetchEtfProfile(allocator, symbol); - } }; // -- JSON parsing -- @@ -265,17 +243,17 @@ fn parseEtfProfileResponse( allocator: std.mem.Allocator, body: []const u8, symbol: []const u8, -) provider.ProviderError!EtfProfile { +) !EtfProfile { const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch - return provider.ProviderError.ParseError; + return error.ParseError; defer parsed.deinit(); const root = parsed.value.object; // Alpha Vantage returns {"Error Message": "..."} or {"Note": "..."} on error/rate limit - if (root.get("Error Message")) |_| return provider.ProviderError.RequestFailed; - if (root.get("Note")) |_| return provider.ProviderError.RateLimited; - if (root.get("Information")) |_| return provider.ProviderError.RateLimited; + if (root.get("Error Message")) |_| return error.RequestFailed; + if (root.get("Note")) |_| return error.RateLimited; + if (root.get("Information")) |_| return error.RateLimited; var profile = EtfProfile{ .symbol = symbol, @@ -318,13 +296,13 @@ fn parseEtfProfileResponse( const name = jsonStr(obj.get("sector")) orelse continue; const weight = parseStrFloat(obj.get("weight") orelse continue) orelse continue; - const duped_name = allocator.dupe(u8, name) catch return provider.ProviderError.OutOfMemory; - sectors.append(allocator, .{ + const duped_name = try allocator.dupe(u8, name); + try sectors.append(allocator, .{ .name = duped_name, .weight = weight, - }) catch return provider.ProviderError.OutOfMemory; + }); } - profile.sectors = sectors.toOwnedSlice(allocator) catch return provider.ProviderError.OutOfMemory; + profile.sectors = try sectors.toOwnedSlice(allocator); } } @@ -348,18 +326,18 @@ fn parseEtfProfileResponse( const weight = parseStrFloat(obj.get("weight") orelse continue) orelse continue; const duped_sym = if (jsonStr(obj.get("symbol"))) |s| - (allocator.dupe(u8, s) catch return provider.ProviderError.OutOfMemory) + (try allocator.dupe(u8, s)) else null; - const duped_name = allocator.dupe(u8, desc) catch return provider.ProviderError.OutOfMemory; + const duped_name = try allocator.dupe(u8, desc); - holdings.append(allocator, .{ + try holdings.append(allocator, .{ .symbol = duped_sym, .name = duped_name, .weight = weight, - }) catch return provider.ProviderError.OutOfMemory; + }); } - profile.holdings = holdings.toOwnedSlice(allocator) catch return provider.ProviderError.OutOfMemory; + profile.holdings = try holdings.toOwnedSlice(allocator); } } @@ -383,16 +361,16 @@ fn parseCompanyOverview( allocator: std.mem.Allocator, body: []const u8, symbol: []const u8, -) provider.ProviderError!CompanyOverview { +) !CompanyOverview { const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch - return provider.ProviderError.ParseError; + return error.ParseError; defer parsed.deinit(); const root = parsed.value.object; - if (root.get("Error Message")) |_| return provider.ProviderError.RequestFailed; - if (root.get("Note")) |_| return provider.ProviderError.RateLimited; - if (root.get("Information")) |_| return provider.ProviderError.RateLimited; + if (root.get("Error Message")) |_| return error.RequestFailed; + if (root.get("Note")) |_| return error.RateLimited; + if (root.get("Information")) |_| return error.RateLimited; return .{ .symbol = symbol, diff --git a/src/providers/cboe.zig b/src/providers/cboe.zig index bb09e97..1cdbc03 100644 --- a/src/providers/cboe.zig +++ b/src/providers/cboe.zig @@ -11,11 +11,9 @@ const Date = @import("../models/date.zig").Date; const OptionContract = @import("../models/option.zig").OptionContract; const OptionsChain = @import("../models/option.zig").OptionsChain; const ContractType = @import("../models/option.zig").ContractType; -const provider = @import("provider.zig"); const json_utils = @import("json_utils.zig"); const optFloat = json_utils.optFloat; const optUint = json_utils.optUint; -const mapHttpError = json_utils.mapHttpError; const base_url = "https://cdn.cboe.com/api/global/delayed_quotes/options"; @@ -42,14 +40,14 @@ pub const Cboe = struct { self: *Cboe, allocator: std.mem.Allocator, symbol: []const u8, - ) provider.ProviderError![]OptionsChain { + ) ![]OptionsChain { self.rate_limiter.acquire(); // Build URL: {base_url}/{SYMBOL}.json - const url = buildCboeUrl(allocator, symbol) catch return provider.ProviderError.OutOfMemory; + const url = try buildCboeUrl(allocator, symbol); defer allocator.free(url); - var response = self.client.get(url) catch |err| return mapHttpError(err); + var response = try self.client.get(url); defer response.deinit(); return parseResponse(allocator, response.body, symbol); @@ -73,26 +71,26 @@ fn parseResponse( allocator: std.mem.Allocator, body: []const u8, symbol: []const u8, -) provider.ProviderError![]OptionsChain { +) ![]OptionsChain { const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch - return provider.ProviderError.ParseError; + return error.ParseError; defer parsed.deinit(); const root = switch (parsed.value) { .object => |o| o, - else => return provider.ProviderError.ParseError, + else => return error.ParseError, }; - const data_obj = switch (root.get("data") orelse return provider.ProviderError.ParseError) { + const data_obj = switch (root.get("data") orelse return error.ParseError) { .object => |o| o, - else => return provider.ProviderError.ParseError, + else => return error.ParseError, }; const underlying_price: ?f64 = if (data_obj.get("current_price")) |v| optFloat(v) else null; - const options_arr = switch (data_obj.get("options") orelse return provider.ProviderError.ParseError) { + const options_arr = switch (data_obj.get("options") orelse return error.ParseError) { .array => |a| a.items, - else => return provider.ProviderError.ParseError, + else => return error.ParseError, }; // Parse all contracts and group by expiration. @@ -129,12 +127,11 @@ fn parseResponse( }; // Find or create the expiration bucket - const bucket = exp_map.getOrPut(allocator, occ.expiration) catch - return provider.ProviderError.OutOfMemory; + const bucket = try exp_map.getOrPut(allocator, occ.expiration); switch (occ.contract_type) { - .call => bucket.calls.append(allocator, contract) catch return provider.ProviderError.OutOfMemory, - .put => bucket.puts.append(allocator, contract) catch return provider.ProviderError.OutOfMemory, + .call => try bucket.calls.append(allocator, contract), + .put => try bucket.puts.append(allocator, contract), } } @@ -220,7 +217,7 @@ const ExpMap = struct { allocator: std.mem.Allocator, symbol: []const u8, underlying_price: ?f64, - ) provider.ProviderError![]OptionsChain { + ) ![]OptionsChain { // Sort entries by expiration std.mem.sort(Entry, self.entries.items, {}, struct { fn lessThan(_: void, a: Entry, b: Entry) bool { @@ -228,8 +225,7 @@ const ExpMap = struct { } }.lessThan); - var chains = allocator.alloc(OptionsChain, self.entries.items.len) catch - return provider.ProviderError.OutOfMemory; + var chains = try allocator.alloc(OptionsChain, self.entries.items.len); var initialized: usize = 0; errdefer { @@ -242,14 +238,11 @@ const ExpMap = struct { } for (self.entries.items, 0..) |*entry, i| { - const owned_symbol = allocator.dupe(u8, symbol) catch - return provider.ProviderError.OutOfMemory; + const owned_symbol = try allocator.dupe(u8, symbol); errdefer allocator.free(owned_symbol); - const calls = entry.calls.toOwnedSlice(allocator) catch - return provider.ProviderError.OutOfMemory; + const calls = try entry.calls.toOwnedSlice(allocator); errdefer allocator.free(calls); - const puts = entry.puts.toOwnedSlice(allocator) catch - return provider.ProviderError.OutOfMemory; + const puts = try entry.puts.toOwnedSlice(allocator); chains[i] = .{ .underlying_symbol = owned_symbol, @@ -351,5 +344,5 @@ test "parseResponse missing data" { const allocator = std.testing.allocator; const result = parseResponse(allocator, body, "AAPL"); - try std.testing.expectError(provider.ProviderError.ParseError, result); + try std.testing.expectError(error.ParseError, result); } diff --git a/src/providers/finnhub.zig b/src/providers/finnhub.zig index 13f8494..6e56a2f 100644 --- a/src/providers/finnhub.zig +++ b/src/providers/finnhub.zig @@ -12,11 +12,9 @@ const RateLimiter = @import("../net/RateLimiter.zig"); const Date = @import("../models/date.zig").Date; const EarningsEvent = @import("../models/earnings.zig").EarningsEvent; const ReportTime = @import("../models/earnings.zig").ReportTime; -const provider = @import("provider.zig"); const json_utils = @import("json_utils.zig"); const optFloat = json_utils.optFloat; const jsonStr = json_utils.jsonStr; -const mapHttpError = json_utils.mapHttpError; const base_url = "https://finnhub.io/api/v1"; @@ -47,7 +45,7 @@ pub const Finnhub = struct { symbol: []const u8, from: ?Date, to: ?Date, - ) provider.ProviderError![]EarningsEvent { + ) ![]EarningsEvent { self.rate_limiter.acquire(); var params: [4][2][]const u8 = undefined; @@ -70,35 +68,14 @@ pub const Finnhub = struct { n += 1; } - const url = http.buildUrl(allocator, base_url ++ "/calendar/earnings", params[0..n]) catch - return provider.ProviderError.OutOfMemory; + const url = try http.buildUrl(allocator, base_url ++ "/calendar/earnings", params[0..n]); defer allocator.free(url); - var response = self.client.get(url) catch |err| return mapHttpError(err); + var response = try self.client.get(url); defer response.deinit(); return parseEarningsResponse(allocator, response.body, symbol); } - - pub fn asProvider(self: *Finnhub) provider.Provider { - return .{ - .ptr = @ptrCast(self), - .vtable = &vtable, - }; - } - - const vtable = provider.Provider.VTable{ - .fetchEarnings = @ptrCast(&fetchEarningsVtable), - .name = .finnhub, - }; - - fn fetchEarningsVtable( - ptr: *Finnhub, - allocator: std.mem.Allocator, - symbol: []const u8, - ) provider.ProviderError![]EarningsEvent { - return ptr.fetchEarnings(allocator, symbol, null, null); - } }; // -- JSON parsing -- @@ -107,24 +84,22 @@ fn parseEarningsResponse( allocator: std.mem.Allocator, body: []const u8, symbol: []const u8, -) provider.ProviderError![]EarningsEvent { +) ![]EarningsEvent { const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch - return provider.ProviderError.ParseError; + return error.ParseError; defer parsed.deinit(); const root = parsed.value.object; - if (root.get("error")) |_| return provider.ProviderError.RequestFailed; + if (root.get("error")) |_| return error.RequestFailed; const cal = root.get("earningsCalendar") orelse { - const empty = allocator.alloc(EarningsEvent, 0) catch return provider.ProviderError.OutOfMemory; - return empty; + return try allocator.alloc(EarningsEvent, 0); }; const items = switch (cal) { .array => |a| a.items, else => { - const empty = allocator.alloc(EarningsEvent, 0) catch return provider.ProviderError.OutOfMemory; - return empty; + return try allocator.alloc(EarningsEvent, 0); }, }; @@ -151,7 +126,7 @@ fn parseEarningsResponse( else null; - events.append(allocator, .{ + try events.append(allocator, .{ .symbol = symbol, .date = date, .estimate = estimate, @@ -163,10 +138,10 @@ fn parseEarningsResponse( .revenue_actual = optFloat(obj.get("revenueActual")), .revenue_estimate = optFloat(obj.get("revenueEstimate")), .report_time = parseReportTime(obj.get("hour")), - }) catch return provider.ProviderError.OutOfMemory; + }); } - return events.toOwnedSlice(allocator) catch return provider.ProviderError.OutOfMemory; + return try events.toOwnedSlice(allocator); } // -- Helpers -- @@ -255,7 +230,7 @@ test "parseEarningsResponse error" { const allocator = std.testing.allocator; const result = parseEarningsResponse(allocator, body, "AAPL"); - try std.testing.expectError(provider.ProviderError.RequestFailed, result); + try std.testing.expectError(error.RequestFailed, result); } test "parseEarningsResponse empty" { diff --git a/src/providers/json_utils.zig b/src/providers/json_utils.zig index c06b08c..3ebcf0c 100644 --- a/src/providers/json_utils.zig +++ b/src/providers/json_utils.zig @@ -1,10 +1,8 @@ //! Shared JSON parsing helpers used by all API providers. //! Centralises the common patterns: extracting floats, strings, -//! unsigned ints, and mapping HTTP errors to provider errors. +//! unsigned ints, and mapping HTTP errors. const std = @import("std"); -const http = @import("../net/http.zig"); -const provider = @import("provider.zig"); /// Extract a required float from a JSON value (string, float, or integer). /// Returns 0 for null, missing, or unparseable values. @@ -49,13 +47,3 @@ pub fn jsonStr(val: ?std.json.Value) ?[]const u8 { else => null, }; } - -/// Map an HTTP-level error to the corresponding provider error. -pub fn mapHttpError(err: http.HttpError) provider.ProviderError { - return switch (err) { - error.RateLimited => provider.ProviderError.RateLimited, - error.Unauthorized => provider.ProviderError.Unauthorized, - error.NotFound => provider.ProviderError.NotFound, - else => provider.ProviderError.RequestFailed, - }; -} diff --git a/src/providers/polygon.zig b/src/providers/polygon.zig index 06e5a64..3b6a578 100644 --- a/src/providers/polygon.zig +++ b/src/providers/polygon.zig @@ -13,11 +13,9 @@ const Candle = @import("../models/candle.zig").Candle; const Dividend = @import("../models/dividend.zig").Dividend; const DividendType = @import("../models/dividend.zig").DividendType; const Split = @import("../models/split.zig").Split; -const provider = @import("provider.zig"); const json_utils = @import("json_utils.zig"); const parseJsonFloat = json_utils.parseJsonFloat; const jsonStr = json_utils.jsonStr; -const mapHttpError = json_utils.mapHttpError; const base_url = "https://api.polygon.io"; @@ -48,7 +46,7 @@ pub const Polygon = struct { symbol: []const u8, from: ?Date, to: ?Date, - ) provider.ProviderError![]Dividend { + ) ![]Dividend { var all_dividends: std.ArrayList(Dividend) = .empty; errdefer { for (all_dividends.items) |d| d.deinit(allocator); @@ -84,15 +82,13 @@ pub const Polygon = struct { n += 1; } - const url = http.buildUrl(allocator, base_url ++ "/v3/reference/dividends", params[0..n]) catch - return provider.ProviderError.OutOfMemory; + const url = try http.buildUrl(allocator, base_url ++ "/v3/reference/dividends", params[0..n]); defer allocator.free(url); - const authed = appendApiKey(allocator, url, self.api_key) catch - return provider.ProviderError.OutOfMemory; + const authed = try appendApiKey(allocator, url, self.api_key); defer allocator.free(authed); - var response = self.client.get(authed) catch |err| return mapHttpError(err); + var response = try self.client.get(authed); defer response.deinit(); next_url = try parseDividendsPage(allocator, response.body, &all_dividends); @@ -102,18 +98,17 @@ pub const Polygon = struct { while (next_url) |cursor_url| { self.rate_limiter.acquire(); - const authed = appendApiKey(allocator, cursor_url, self.api_key) catch - return provider.ProviderError.OutOfMemory; + const authed = try appendApiKey(allocator, cursor_url, self.api_key); defer allocator.free(authed); - var response = self.client.get(authed) catch |err| return mapHttpError(err); + var response = try self.client.get(authed); defer response.deinit(); allocator.free(cursor_url); next_url = try parseDividendsPage(allocator, response.body, &all_dividends); } - return all_dividends.toOwnedSlice(allocator) catch return provider.ProviderError.OutOfMemory; + return try all_dividends.toOwnedSlice(allocator); } /// Fetch split history for a ticker. Results sorted oldest-first. @@ -122,21 +117,20 @@ pub const Polygon = struct { self: *Polygon, allocator: std.mem.Allocator, symbol: []const u8, - ) provider.ProviderError![]Split { + ) ![]Split { self.rate_limiter.acquire(); - const url = http.buildUrl(allocator, base_url ++ "/v3/reference/splits", &.{ + const url = try http.buildUrl(allocator, base_url ++ "/v3/reference/splits", &.{ .{ "ticker", symbol }, .{ "limit", "1000" }, .{ "sort", "execution_date" }, - }) catch return provider.ProviderError.OutOfMemory; + }); defer allocator.free(url); - const authed = appendApiKey(allocator, url, self.api_key) catch - return provider.ProviderError.OutOfMemory; + const authed = try appendApiKey(allocator, url, self.api_key); defer allocator.free(authed); - var response = self.client.get(authed) catch |err| return mapHttpError(err); + var response = try self.client.get(authed); defer response.deinit(); return parseSplitsResponse(allocator, response.body); @@ -150,7 +144,7 @@ pub const Polygon = struct { symbol: []const u8, from: Date, to: Date, - ) provider.ProviderError![]Candle { + ) ![]Candle { self.rate_limiter.acquire(); var from_buf: [10]u8 = undefined; @@ -159,46 +153,21 @@ pub const Polygon = struct { const to_str = to.format(&to_buf); // Build URL manually since the path contains the date range - const path = std.fmt.allocPrint( + const path = try std.fmt.allocPrint( allocator, "{s}/v2/aggs/ticker/{s}/range/1/day/{s}/{s}?adjusted=true&sort=asc&limit=5000", .{ base_url, symbol, from_str, to_str }, - ) catch return provider.ProviderError.OutOfMemory; + ); defer allocator.free(path); - const authed = appendApiKey(allocator, path, self.api_key) catch - return provider.ProviderError.OutOfMemory; + const authed = try appendApiKey(allocator, path, self.api_key); defer allocator.free(authed); - var response = self.client.get(authed) catch |err| return mapHttpError(err); + var response = try self.client.get(authed); defer response.deinit(); return parseCandlesResponse(allocator, response.body); } - - pub fn asProvider(self: *Polygon) provider.Provider { - return .{ - .ptr = @ptrCast(self), - .vtable = &vtable, - }; - } - - const vtable = provider.Provider.VTable{ - .fetchDividends = @ptrCast(&fetchDividendsVtable), - .fetchSplits = @ptrCast(&fetchSplitsVtable), - .fetchCandles = @ptrCast(&fetchCandlesVtable), - .name = .polygon, - }; - - fn fetchDividendsVtable(ptr: *Polygon, allocator: std.mem.Allocator, symbol: []const u8, from: ?Date, to: ?Date) provider.ProviderError![]Dividend { - return ptr.fetchDividends(allocator, symbol, from, to); - } - fn fetchSplitsVtable(ptr: *Polygon, allocator: std.mem.Allocator, symbol: []const u8) provider.ProviderError![]Split { - return ptr.fetchSplits(allocator, symbol); - } - fn fetchCandlesVtable(ptr: *Polygon, allocator: std.mem.Allocator, symbol: []const u8, from: Date, to: Date) provider.ProviderError![]Candle { - return ptr.fetchCandles(allocator, symbol, from, to); - } }; // -- JSON parsing -- @@ -207,9 +176,9 @@ fn parseDividendsPage( allocator: std.mem.Allocator, body: []const u8, out: *std.ArrayList(Dividend), -) provider.ProviderError!?[]const u8 { +) !?[]const u8 { const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch - return provider.ProviderError.ParseError; + return error.ParseError; defer parsed.deinit(); const root = parsed.value.object; @@ -217,7 +186,7 @@ fn parseDividendsPage( // Check status if (root.get("status")) |s| { if (s == .string and std.mem.eql(u8, s.string, "ERROR")) - return provider.ProviderError.RequestFailed; + return error.RequestFailed; } const results = root.get("results") orelse return null; @@ -244,7 +213,7 @@ fn parseDividendsPage( const amount = parseJsonFloat(obj.get("cash_amount")); if (amount <= 0) continue; - out.append(allocator, .{ + try out.append(allocator, .{ .ex_date = ex_date, .amount = amount, .pay_date = parseDateField(obj, "pay_date"), @@ -255,7 +224,7 @@ fn parseDividendsPage( (allocator.dupe(u8, s) catch null) else null, - }) catch return provider.ProviderError.OutOfMemory; + }); } // Check for next_url (pagination cursor) @@ -264,34 +233,32 @@ fn parseDividendsPage( .string => |s| s, else => return null, }; - const duped = allocator.dupe(u8, url_str) catch return provider.ProviderError.OutOfMemory; + const duped = try allocator.dupe(u8, url_str); return duped; } return null; } -fn parseSplitsResponse(allocator: std.mem.Allocator, body: []const u8) provider.ProviderError![]Split { +fn parseSplitsResponse(allocator: std.mem.Allocator, body: []const u8) ![]Split { const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch - return provider.ProviderError.ParseError; + return error.ParseError; defer parsed.deinit(); const root = parsed.value.object; if (root.get("status")) |s| { if (s == .string and std.mem.eql(u8, s.string, "ERROR")) - return provider.ProviderError.RequestFailed; + return error.RequestFailed; } const results = root.get("results") orelse { - const empty = allocator.alloc(Split, 0) catch return provider.ProviderError.OutOfMemory; - return empty; + return try allocator.alloc(Split, 0); }; const items = switch (results) { .array => |a| a.items, else => { - const empty = allocator.alloc(Split, 0) catch return provider.ProviderError.OutOfMemory; - return empty; + return try allocator.alloc(Split, 0); }, }; @@ -313,37 +280,35 @@ fn parseSplitsResponse(allocator: std.mem.Allocator, body: []const u8) provider. break :blk Date.parse(s) catch continue; }; - splits.append(allocator, .{ + try splits.append(allocator, .{ .date = date, .numerator = parseJsonFloat(obj.get("split_to")), .denominator = parseJsonFloat(obj.get("split_from")), - }) catch return provider.ProviderError.OutOfMemory; + }); } - return splits.toOwnedSlice(allocator) catch return provider.ProviderError.OutOfMemory; + return try splits.toOwnedSlice(allocator); } -fn parseCandlesResponse(allocator: std.mem.Allocator, body: []const u8) provider.ProviderError![]Candle { +fn parseCandlesResponse(allocator: std.mem.Allocator, body: []const u8) ![]Candle { const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch - return provider.ProviderError.ParseError; + return error.ParseError; defer parsed.deinit(); const root = parsed.value.object; if (root.get("status")) |s| { if (s == .string and std.mem.eql(u8, s.string, "ERROR")) - return provider.ProviderError.RequestFailed; + return error.RequestFailed; } const results = root.get("results") orelse { - const empty = allocator.alloc(Candle, 0) catch return provider.ProviderError.OutOfMemory; - return empty; + return try allocator.alloc(Candle, 0); }; const items = switch (results) { .array => |a| a.items, else => { - const empty = allocator.alloc(Candle, 0) catch return provider.ProviderError.OutOfMemory; - return empty; + return try allocator.alloc(Candle, 0); }, }; @@ -370,7 +335,7 @@ fn parseCandlesResponse(allocator: std.mem.Allocator, body: []const u8) provider const close = parseJsonFloat(obj.get("c")); - candles.append(allocator, .{ + try candles.append(allocator, .{ .date = date, .open = parseJsonFloat(obj.get("o")), .high = parseJsonFloat(obj.get("h")), @@ -378,10 +343,10 @@ fn parseCandlesResponse(allocator: std.mem.Allocator, body: []const u8) provider .close = close, .adj_close = close, // Polygon adjusted=true gives adjusted values .volume = @intFromFloat(parseJsonFloat(obj.get("v"))), - }) catch return provider.ProviderError.OutOfMemory; + }); } - return candles.toOwnedSlice(allocator) catch return provider.ProviderError.OutOfMemory; + return try candles.toOwnedSlice(allocator); } // -- Helpers -- @@ -506,7 +471,7 @@ test "parseDividendsPage error status" { } const result = parseDividendsPage(allocator, body, &out); - try std.testing.expectError(provider.ProviderError.RequestFailed, result); + try std.testing.expectError(error.RequestFailed, result); } test "parseSplitsResponse basic" { diff --git a/src/providers/provider.zig b/src/providers/provider.zig deleted file mode 100644 index 0e595a8..0000000 --- a/src/providers/provider.zig +++ /dev/null @@ -1,147 +0,0 @@ -const std = @import("std"); -const Date = @import("../models/date.zig").Date; -const Candle = @import("../models/candle.zig").Candle; -const Dividend = @import("../models/dividend.zig").Dividend; -const Split = @import("../models/split.zig").Split; -const OptionContract = @import("../models/option.zig").OptionContract; -const EarningsEvent = @import("../models/earnings.zig").EarningsEvent; -const EtfProfile = @import("../models/etf_profile.zig").EtfProfile; - -pub const ProviderError = error{ - ApiKeyMissing, - RequestFailed, - RateLimited, - ParseError, - NotSupported, - OutOfMemory, - Unauthorized, - NotFound, - ServerError, - InvalidResponse, - ConnectionRefused, -}; - -/// Identifies which upstream data source a result came from. -pub const ProviderName = enum { - twelvedata, - polygon, - finnhub, - cboe, - alphavantage, -}; - -/// Common interface for all data providers. -/// Each provider implements the capabilities it supports and returns -/// `error.NotSupported` for those it doesn't. -pub const Provider = struct { - ptr: *anyopaque, - vtable: *const VTable, - - pub const VTable = struct { - fetchCandles: ?*const fn ( - ptr: *anyopaque, - allocator: std.mem.Allocator, - symbol: []const u8, - from: Date, - to: Date, - ) ProviderError![]Candle = null, - - fetchDividends: ?*const fn ( - ptr: *anyopaque, - allocator: std.mem.Allocator, - symbol: []const u8, - from: ?Date, - to: ?Date, - ) ProviderError![]Dividend = null, - - fetchSplits: ?*const fn ( - ptr: *anyopaque, - allocator: std.mem.Allocator, - symbol: []const u8, - ) ProviderError![]Split = null, - - fetchOptions: ?*const fn ( - ptr: *anyopaque, - allocator: std.mem.Allocator, - symbol: []const u8, - expiration: ?Date, - ) ProviderError![]OptionContract = null, - - fetchEarnings: ?*const fn ( - ptr: *anyopaque, - allocator: std.mem.Allocator, - symbol: []const u8, - ) ProviderError![]EarningsEvent = null, - - fetchEtfProfile: ?*const fn ( - ptr: *anyopaque, - allocator: std.mem.Allocator, - symbol: []const u8, - ) ProviderError!EtfProfile = null, - - name: ProviderName, - }; - - pub fn fetchCandles( - self: Provider, - allocator: std.mem.Allocator, - symbol: []const u8, - from: Date, - to: Date, - ) ProviderError![]Candle { - const func = self.vtable.fetchCandles orelse return ProviderError.NotSupported; - return func(self.ptr, allocator, symbol, from, to); - } - - pub fn fetchDividends( - self: Provider, - allocator: std.mem.Allocator, - symbol: []const u8, - from: ?Date, - to: ?Date, - ) ProviderError![]Dividend { - const func = self.vtable.fetchDividends orelse return ProviderError.NotSupported; - return func(self.ptr, allocator, symbol, from, to); - } - - pub fn fetchSplits( - self: Provider, - allocator: std.mem.Allocator, - symbol: []const u8, - ) ProviderError![]Split { - const func = self.vtable.fetchSplits orelse return ProviderError.NotSupported; - return func(self.ptr, allocator, symbol); - } - - pub fn fetchOptions( - self: Provider, - allocator: std.mem.Allocator, - symbol: []const u8, - expiration: ?Date, - ) ProviderError![]OptionContract { - const func = self.vtable.fetchOptions orelse return ProviderError.NotSupported; - return func(self.ptr, allocator, symbol, expiration); - } - - pub fn fetchEarnings( - self: Provider, - allocator: std.mem.Allocator, - symbol: []const u8, - ) ProviderError![]EarningsEvent { - const func = self.vtable.fetchEarnings orelse return ProviderError.NotSupported; - return func(self.ptr, allocator, symbol); - } - - pub fn fetchEtfProfile( - self: Provider, - allocator: std.mem.Allocator, - symbol: []const u8, - ) ProviderError!EtfProfile { - const func = self.vtable.fetchEtfProfile orelse return ProviderError.NotSupported; - return func(self.ptr, allocator, symbol); - } - - pub fn providerName(self: Provider) ProviderName { - return self.vtable.name; - } -}; diff --git a/src/providers/twelvedata.zig b/src/providers/twelvedata.zig index 228f133..c30a140 100644 --- a/src/providers/twelvedata.zig +++ b/src/providers/twelvedata.zig @@ -12,7 +12,6 @@ const http = @import("../net/http.zig"); const RateLimiter = @import("../net/RateLimiter.zig"); const Date = @import("../models/date.zig").Date; const Candle = @import("../models/candle.zig").Candle; -const provider = @import("provider.zig"); const json_utils = @import("json_utils.zig"); const parseJsonFloat = json_utils.parseJsonFloat; @@ -45,7 +44,7 @@ pub const TwelveData = struct { symbol: []const u8, from: Date, to: Date, - ) provider.ProviderError![]Candle { + ) ![]Candle { self.rate_limiter.acquire(); var from_buf: [10]u8 = undefined; @@ -53,22 +52,17 @@ pub const TwelveData = struct { const from_str = from.format(&from_buf); const to_str = to.format(&to_buf); - const url = http.buildUrl(allocator, base_url ++ "/time_series", &.{ + const url = try http.buildUrl(allocator, base_url ++ "/time_series", &.{ .{ "symbol", symbol }, .{ "interval", "1day" }, .{ "start_date", from_str }, .{ "end_date", to_str }, .{ "outputsize", "5000" }, .{ "apikey", self.api_key }, - }) catch return provider.ProviderError.OutOfMemory; + }); defer allocator.free(url); - var response = self.client.get(url) catch |err| return switch (err) { - error.RateLimited => provider.ProviderError.RateLimited, - error.Unauthorized => provider.ProviderError.Unauthorized, - error.NotFound => provider.ProviderError.NotFound, - else => provider.ProviderError.RequestFailed, - }; + var response = try self.client.get(url); defer response.deinit(); return parseTimeSeriesResponse(allocator, response.body); @@ -84,7 +78,7 @@ pub const TwelveData = struct { /// Parse and print quote data. Caller should use this within the /// lifetime of the QuoteResponse. - pub fn parse(self: QuoteResponse, allocator: std.mem.Allocator) provider.ProviderError!ParsedQuote { + pub fn parse(self: QuoteResponse, allocator: std.mem.Allocator) !ParsedQuote { return parseQuoteBody(allocator, self.body); } }; @@ -160,21 +154,16 @@ pub const TwelveData = struct { self: *TwelveData, allocator: std.mem.Allocator, symbol: []const u8, - ) provider.ProviderError!QuoteResponse { + ) !QuoteResponse { self.rate_limiter.acquire(); - const url = http.buildUrl(allocator, base_url ++ "/quote", &.{ + const url = try http.buildUrl(allocator, base_url ++ "/quote", &.{ .{ "symbol", symbol }, .{ "apikey", self.api_key }, - }) catch return provider.ProviderError.OutOfMemory; + }); defer allocator.free(url); - var response = self.client.get(url) catch |err| return switch (err) { - error.RateLimited => provider.ProviderError.RateLimited, - error.Unauthorized => provider.ProviderError.Unauthorized, - error.NotFound => provider.ProviderError.NotFound, - else => provider.ProviderError.RequestFailed, - }; + var response = try self.client.get(url); // Transfer ownership of body to QuoteResponse const body = response.body; @@ -185,35 +174,13 @@ pub const TwelveData = struct { .allocator = allocator, }; } - - pub fn asProvider(self: *TwelveData) provider.Provider { - return .{ - .ptr = @ptrCast(self), - .vtable = &vtable, - }; - } - - const vtable = provider.Provider.VTable{ - .fetchCandles = @ptrCast(&fetchCandlesVtable), - .name = .twelvedata, - }; - - fn fetchCandlesVtable( - ptr: *TwelveData, - allocator: std.mem.Allocator, - symbol: []const u8, - from: Date, - to: Date, - ) provider.ProviderError![]Candle { - return ptr.fetchCandles(allocator, symbol, from, to); - } }; // -- JSON parsing -- -fn parseTimeSeriesResponse(allocator: std.mem.Allocator, body: []const u8) provider.ProviderError![]Candle { +fn parseTimeSeriesResponse(allocator: std.mem.Allocator, body: []const u8) ![]Candle { const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch - return provider.ProviderError.ParseError; + return error.ParseError; defer parsed.deinit(); const root = parsed.value; @@ -224,18 +191,18 @@ fn parseTimeSeriesResponse(allocator: std.mem.Allocator, body: []const u8) provi if (std.mem.eql(u8, status.string, "error")) { // Check error code if (root.object.get("code")) |code| { - if (code == .integer and code.integer == 429) return provider.ProviderError.RateLimited; - if (code == .integer and code.integer == 401) return provider.ProviderError.Unauthorized; + if (code == .integer and code.integer == 429) return error.RateLimited; + if (code == .integer and code.integer == 401) return error.Unauthorized; } - return provider.ProviderError.RequestFailed; + return error.RequestFailed; } } } - const values_json = root.object.get("values") orelse return provider.ProviderError.ParseError; + const values_json = root.object.get("values") orelse return error.ParseError; const values = switch (values_json) { .array => |a| a.items, - else => return provider.ProviderError.ParseError, + else => return error.ParseError, }; // Twelve Data returns newest first. We'll parse into a list and reverse. @@ -259,7 +226,7 @@ fn parseTimeSeriesResponse(allocator: std.mem.Allocator, body: []const u8) provi break :blk Date.parse(date_part) catch continue; }; - candles.append(allocator, .{ + try candles.append(allocator, .{ .date = date, .open = parseJsonFloat(obj.get("open")), .high = parseJsonFloat(obj.get("high")), @@ -268,25 +235,25 @@ fn parseTimeSeriesResponse(allocator: std.mem.Allocator, body: []const u8) provi // Twelve Data close is split-adjusted only, not dividend-adjusted .adj_close = parseJsonFloat(obj.get("close")), .volume = @intFromFloat(parseJsonFloat(obj.get("volume"))), - }) catch return provider.ProviderError.OutOfMemory; + }); } // Reverse to get oldest-first ordering - const slice = candles.toOwnedSlice(allocator) catch return provider.ProviderError.OutOfMemory; + const slice = try candles.toOwnedSlice(allocator); std.mem.reverse(Candle, slice); return slice; } -fn parseQuoteBody(allocator: std.mem.Allocator, body: []const u8) provider.ProviderError!TwelveData.ParsedQuote { +fn parseQuoteBody(allocator: std.mem.Allocator, body: []const u8) !TwelveData.ParsedQuote { const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch - return provider.ProviderError.ParseError; + return error.ParseError; // Check for error if (parsed.value.object.get("status")) |status| { if (status == .string and std.mem.eql(u8, status.string, "error")) { var p = parsed; p.deinit(); - return provider.ProviderError.RequestFailed; + return error.RequestFailed; } }