remove unused provider interface stuff

This commit is contained in:
Emil Lerch 2026-03-06 15:56:09 -08:00
parent 6a680a2381
commit 26b831631d
Signed by: lobo
GPG key ID: A7B62D657EF764F8
7 changed files with 123 additions and 404 deletions

View file

@ -13,10 +13,8 @@ const Date = @import("../models/date.zig").Date;
const EtfProfile = @import("../models/etf_profile.zig").EtfProfile; const EtfProfile = @import("../models/etf_profile.zig").EtfProfile;
const Holding = @import("../models/etf_profile.zig").Holding; const Holding = @import("../models/etf_profile.zig").Holding;
const SectorWeight = @import("../models/etf_profile.zig").SectorWeight; const SectorWeight = @import("../models/etf_profile.zig").SectorWeight;
const provider = @import("provider.zig");
const json_utils = @import("json_utils.zig"); const json_utils = @import("json_utils.zig");
const jsonStr = json_utils.jsonStr; const jsonStr = json_utils.jsonStr;
const mapHttpError = json_utils.mapHttpError;
const base_url = "https://www.alphavantage.co/query"; const base_url = "https://www.alphavantage.co/query";
@ -116,7 +114,7 @@ test "parseEtfProfileResponse error response" {
const allocator = std.testing.allocator; const allocator = std.testing.allocator;
const result = parseEtfProfileResponse(allocator, body, "BAD"); 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" { test "parseEtfProfileResponse rate limited" {
@ -126,7 +124,7 @@ test "parseEtfProfileResponse rate limited" {
const allocator = std.testing.allocator; const allocator = std.testing.allocator;
const result = parseEtfProfileResponse(allocator, body, "SPY"); const result = parseEtfProfileResponse(allocator, body, "SPY");
try std.testing.expectError(provider.ProviderError.RateLimited, result); try std.testing.expectError(error.RateLimited, result);
} }
test "parseCompanyOverview basic" { test "parseCompanyOverview basic" {
@ -201,17 +199,17 @@ pub const AlphaVantage = struct {
self: *AlphaVantage, self: *AlphaVantage,
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
symbol: []const u8, symbol: []const u8,
) provider.ProviderError!CompanyOverview { ) !CompanyOverview {
self.rate_limiter.acquire(); self.rate_limiter.acquire();
const url = http.buildUrl(allocator, base_url, &.{ const url = try http.buildUrl(allocator, base_url, &.{
.{ "function", "OVERVIEW" }, .{ "function", "OVERVIEW" },
.{ "symbol", symbol }, .{ "symbol", symbol },
.{ "apikey", self.api_key }, .{ "apikey", self.api_key },
}) catch return provider.ProviderError.OutOfMemory; });
defer allocator.free(url); 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(); defer response.deinit();
return parseCompanyOverview(allocator, response.body, symbol); return parseCompanyOverview(allocator, response.body, symbol);
@ -222,41 +220,21 @@ pub const AlphaVantage = struct {
self: *AlphaVantage, self: *AlphaVantage,
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
symbol: []const u8, symbol: []const u8,
) provider.ProviderError!EtfProfile { ) !EtfProfile {
self.rate_limiter.acquire(); self.rate_limiter.acquire();
const url = http.buildUrl(allocator, base_url, &.{ const url = try http.buildUrl(allocator, base_url, &.{
.{ "function", "ETF_PROFILE" }, .{ "function", "ETF_PROFILE" },
.{ "symbol", symbol }, .{ "symbol", symbol },
.{ "apikey", self.api_key }, .{ "apikey", self.api_key },
}) catch return provider.ProviderError.OutOfMemory; });
defer allocator.free(url); 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(); defer response.deinit();
return parseEtfProfileResponse(allocator, response.body, symbol); 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 -- // -- JSON parsing --
@ -265,17 +243,17 @@ fn parseEtfProfileResponse(
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
body: []const u8, body: []const u8,
symbol: []const u8, symbol: []const u8,
) provider.ProviderError!EtfProfile { ) !EtfProfile {
const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch
return provider.ProviderError.ParseError; return error.ParseError;
defer parsed.deinit(); defer parsed.deinit();
const root = parsed.value.object; const root = parsed.value.object;
// Alpha Vantage returns {"Error Message": "..."} or {"Note": "..."} on error/rate limit // Alpha Vantage returns {"Error Message": "..."} or {"Note": "..."} on error/rate limit
if (root.get("Error Message")) |_| return provider.ProviderError.RequestFailed; if (root.get("Error Message")) |_| return error.RequestFailed;
if (root.get("Note")) |_| return provider.ProviderError.RateLimited; if (root.get("Note")) |_| return error.RateLimited;
if (root.get("Information")) |_| return provider.ProviderError.RateLimited; if (root.get("Information")) |_| return error.RateLimited;
var profile = EtfProfile{ var profile = EtfProfile{
.symbol = symbol, .symbol = symbol,
@ -318,13 +296,13 @@ fn parseEtfProfileResponse(
const name = jsonStr(obj.get("sector")) orelse continue; const name = jsonStr(obj.get("sector")) orelse continue;
const weight = parseStrFloat(obj.get("weight") orelse continue) orelse continue; const weight = parseStrFloat(obj.get("weight") orelse continue) orelse continue;
const duped_name = allocator.dupe(u8, name) catch return provider.ProviderError.OutOfMemory; const duped_name = try allocator.dupe(u8, name);
sectors.append(allocator, .{ try sectors.append(allocator, .{
.name = duped_name, .name = duped_name,
.weight = weight, .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 weight = parseStrFloat(obj.get("weight") orelse continue) orelse continue;
const duped_sym = if (jsonStr(obj.get("symbol"))) |s| const duped_sym = if (jsonStr(obj.get("symbol"))) |s|
(allocator.dupe(u8, s) catch return provider.ProviderError.OutOfMemory) (try allocator.dupe(u8, s))
else else
null; 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, .symbol = duped_sym,
.name = duped_name, .name = duped_name,
.weight = weight, .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, allocator: std.mem.Allocator,
body: []const u8, body: []const u8,
symbol: []const u8, symbol: []const u8,
) provider.ProviderError!CompanyOverview { ) !CompanyOverview {
const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch
return provider.ProviderError.ParseError; return error.ParseError;
defer parsed.deinit(); defer parsed.deinit();
const root = parsed.value.object; const root = parsed.value.object;
if (root.get("Error Message")) |_| return provider.ProviderError.RequestFailed; if (root.get("Error Message")) |_| return error.RequestFailed;
if (root.get("Note")) |_| return provider.ProviderError.RateLimited; if (root.get("Note")) |_| return error.RateLimited;
if (root.get("Information")) |_| return provider.ProviderError.RateLimited; if (root.get("Information")) |_| return error.RateLimited;
return .{ return .{
.symbol = symbol, .symbol = symbol,

View file

@ -11,11 +11,9 @@ const Date = @import("../models/date.zig").Date;
const OptionContract = @import("../models/option.zig").OptionContract; const OptionContract = @import("../models/option.zig").OptionContract;
const OptionsChain = @import("../models/option.zig").OptionsChain; const OptionsChain = @import("../models/option.zig").OptionsChain;
const ContractType = @import("../models/option.zig").ContractType; const ContractType = @import("../models/option.zig").ContractType;
const provider = @import("provider.zig");
const json_utils = @import("json_utils.zig"); const json_utils = @import("json_utils.zig");
const optFloat = json_utils.optFloat; const optFloat = json_utils.optFloat;
const optUint = json_utils.optUint; const optUint = json_utils.optUint;
const mapHttpError = json_utils.mapHttpError;
const base_url = "https://cdn.cboe.com/api/global/delayed_quotes/options"; const base_url = "https://cdn.cboe.com/api/global/delayed_quotes/options";
@ -42,14 +40,14 @@ pub const Cboe = struct {
self: *Cboe, self: *Cboe,
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
symbol: []const u8, symbol: []const u8,
) provider.ProviderError![]OptionsChain { ) ![]OptionsChain {
self.rate_limiter.acquire(); self.rate_limiter.acquire();
// Build URL: {base_url}/{SYMBOL}.json // 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); 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(); defer response.deinit();
return parseResponse(allocator, response.body, symbol); return parseResponse(allocator, response.body, symbol);
@ -73,26 +71,26 @@ fn parseResponse(
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
body: []const u8, body: []const u8,
symbol: []const u8, symbol: []const u8,
) provider.ProviderError![]OptionsChain { ) ![]OptionsChain {
const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch
return provider.ProviderError.ParseError; return error.ParseError;
defer parsed.deinit(); defer parsed.deinit();
const root = switch (parsed.value) { const root = switch (parsed.value) {
.object => |o| o, .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, .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 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, .array => |a| a.items,
else => return provider.ProviderError.ParseError, else => return error.ParseError,
}; };
// Parse all contracts and group by expiration. // Parse all contracts and group by expiration.
@ -129,12 +127,11 @@ fn parseResponse(
}; };
// Find or create the expiration bucket // Find or create the expiration bucket
const bucket = exp_map.getOrPut(allocator, occ.expiration) catch const bucket = try exp_map.getOrPut(allocator, occ.expiration);
return provider.ProviderError.OutOfMemory;
switch (occ.contract_type) { switch (occ.contract_type) {
.call => bucket.calls.append(allocator, contract) catch return provider.ProviderError.OutOfMemory, .call => try bucket.calls.append(allocator, contract),
.put => bucket.puts.append(allocator, contract) catch return provider.ProviderError.OutOfMemory, .put => try bucket.puts.append(allocator, contract),
} }
} }
@ -220,7 +217,7 @@ const ExpMap = struct {
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
symbol: []const u8, symbol: []const u8,
underlying_price: ?f64, underlying_price: ?f64,
) provider.ProviderError![]OptionsChain { ) ![]OptionsChain {
// Sort entries by expiration // Sort entries by expiration
std.mem.sort(Entry, self.entries.items, {}, struct { std.mem.sort(Entry, self.entries.items, {}, struct {
fn lessThan(_: void, a: Entry, b: Entry) bool { fn lessThan(_: void, a: Entry, b: Entry) bool {
@ -228,8 +225,7 @@ const ExpMap = struct {
} }
}.lessThan); }.lessThan);
var chains = allocator.alloc(OptionsChain, self.entries.items.len) catch var chains = try allocator.alloc(OptionsChain, self.entries.items.len);
return provider.ProviderError.OutOfMemory;
var initialized: usize = 0; var initialized: usize = 0;
errdefer { errdefer {
@ -242,14 +238,11 @@ const ExpMap = struct {
} }
for (self.entries.items, 0..) |*entry, i| { for (self.entries.items, 0..) |*entry, i| {
const owned_symbol = allocator.dupe(u8, symbol) catch const owned_symbol = try allocator.dupe(u8, symbol);
return provider.ProviderError.OutOfMemory;
errdefer allocator.free(owned_symbol); errdefer allocator.free(owned_symbol);
const calls = entry.calls.toOwnedSlice(allocator) catch const calls = try entry.calls.toOwnedSlice(allocator);
return provider.ProviderError.OutOfMemory;
errdefer allocator.free(calls); errdefer allocator.free(calls);
const puts = entry.puts.toOwnedSlice(allocator) catch const puts = try entry.puts.toOwnedSlice(allocator);
return provider.ProviderError.OutOfMemory;
chains[i] = .{ chains[i] = .{
.underlying_symbol = owned_symbol, .underlying_symbol = owned_symbol,
@ -351,5 +344,5 @@ test "parseResponse missing data" {
const allocator = std.testing.allocator; const allocator = std.testing.allocator;
const result = parseResponse(allocator, body, "AAPL"); const result = parseResponse(allocator, body, "AAPL");
try std.testing.expectError(provider.ProviderError.ParseError, result); try std.testing.expectError(error.ParseError, result);
} }

View file

@ -12,11 +12,9 @@ const RateLimiter = @import("../net/RateLimiter.zig");
const Date = @import("../models/date.zig").Date; const Date = @import("../models/date.zig").Date;
const EarningsEvent = @import("../models/earnings.zig").EarningsEvent; const EarningsEvent = @import("../models/earnings.zig").EarningsEvent;
const ReportTime = @import("../models/earnings.zig").ReportTime; const ReportTime = @import("../models/earnings.zig").ReportTime;
const provider = @import("provider.zig");
const json_utils = @import("json_utils.zig"); const json_utils = @import("json_utils.zig");
const optFloat = json_utils.optFloat; const optFloat = json_utils.optFloat;
const jsonStr = json_utils.jsonStr; const jsonStr = json_utils.jsonStr;
const mapHttpError = json_utils.mapHttpError;
const base_url = "https://finnhub.io/api/v1"; const base_url = "https://finnhub.io/api/v1";
@ -47,7 +45,7 @@ pub const Finnhub = struct {
symbol: []const u8, symbol: []const u8,
from: ?Date, from: ?Date,
to: ?Date, to: ?Date,
) provider.ProviderError![]EarningsEvent { ) ![]EarningsEvent {
self.rate_limiter.acquire(); self.rate_limiter.acquire();
var params: [4][2][]const u8 = undefined; var params: [4][2][]const u8 = undefined;
@ -70,35 +68,14 @@ pub const Finnhub = struct {
n += 1; n += 1;
} }
const url = http.buildUrl(allocator, base_url ++ "/calendar/earnings", params[0..n]) catch const url = try http.buildUrl(allocator, base_url ++ "/calendar/earnings", params[0..n]);
return provider.ProviderError.OutOfMemory;
defer allocator.free(url); 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(); defer response.deinit();
return parseEarningsResponse(allocator, response.body, symbol); 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 -- // -- JSON parsing --
@ -107,24 +84,22 @@ fn parseEarningsResponse(
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
body: []const u8, body: []const u8,
symbol: []const u8, symbol: []const u8,
) provider.ProviderError![]EarningsEvent { ) ![]EarningsEvent {
const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch
return provider.ProviderError.ParseError; return error.ParseError;
defer parsed.deinit(); defer parsed.deinit();
const root = parsed.value.object; 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 cal = root.get("earningsCalendar") orelse {
const empty = allocator.alloc(EarningsEvent, 0) catch return provider.ProviderError.OutOfMemory; return try allocator.alloc(EarningsEvent, 0);
return empty;
}; };
const items = switch (cal) { const items = switch (cal) {
.array => |a| a.items, .array => |a| a.items,
else => { else => {
const empty = allocator.alloc(EarningsEvent, 0) catch return provider.ProviderError.OutOfMemory; return try allocator.alloc(EarningsEvent, 0);
return empty;
}, },
}; };
@ -151,7 +126,7 @@ fn parseEarningsResponse(
else else
null; null;
events.append(allocator, .{ try events.append(allocator, .{
.symbol = symbol, .symbol = symbol,
.date = date, .date = date,
.estimate = estimate, .estimate = estimate,
@ -163,10 +138,10 @@ fn parseEarningsResponse(
.revenue_actual = optFloat(obj.get("revenueActual")), .revenue_actual = optFloat(obj.get("revenueActual")),
.revenue_estimate = optFloat(obj.get("revenueEstimate")), .revenue_estimate = optFloat(obj.get("revenueEstimate")),
.report_time = parseReportTime(obj.get("hour")), .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 -- // -- Helpers --
@ -255,7 +230,7 @@ test "parseEarningsResponse error" {
const allocator = std.testing.allocator; const allocator = std.testing.allocator;
const result = parseEarningsResponse(allocator, body, "AAPL"); const result = parseEarningsResponse(allocator, body, "AAPL");
try std.testing.expectError(provider.ProviderError.RequestFailed, result); try std.testing.expectError(error.RequestFailed, result);
} }
test "parseEarningsResponse empty" { test "parseEarningsResponse empty" {

View file

@ -1,10 +1,8 @@
//! Shared JSON parsing helpers used by all API providers. //! Shared JSON parsing helpers used by all API providers.
//! Centralises the common patterns: extracting floats, strings, //! 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 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). /// Extract a required float from a JSON value (string, float, or integer).
/// Returns 0 for null, missing, or unparseable values. /// Returns 0 for null, missing, or unparseable values.
@ -49,13 +47,3 @@ pub fn jsonStr(val: ?std.json.Value) ?[]const u8 {
else => null, 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,
};
}

View file

@ -13,11 +13,9 @@ const Candle = @import("../models/candle.zig").Candle;
const Dividend = @import("../models/dividend.zig").Dividend; const Dividend = @import("../models/dividend.zig").Dividend;
const DividendType = @import("../models/dividend.zig").DividendType; const DividendType = @import("../models/dividend.zig").DividendType;
const Split = @import("../models/split.zig").Split; const Split = @import("../models/split.zig").Split;
const provider = @import("provider.zig");
const json_utils = @import("json_utils.zig"); const json_utils = @import("json_utils.zig");
const parseJsonFloat = json_utils.parseJsonFloat; const parseJsonFloat = json_utils.parseJsonFloat;
const jsonStr = json_utils.jsonStr; const jsonStr = json_utils.jsonStr;
const mapHttpError = json_utils.mapHttpError;
const base_url = "https://api.polygon.io"; const base_url = "https://api.polygon.io";
@ -48,7 +46,7 @@ pub const Polygon = struct {
symbol: []const u8, symbol: []const u8,
from: ?Date, from: ?Date,
to: ?Date, to: ?Date,
) provider.ProviderError![]Dividend { ) ![]Dividend {
var all_dividends: std.ArrayList(Dividend) = .empty; var all_dividends: std.ArrayList(Dividend) = .empty;
errdefer { errdefer {
for (all_dividends.items) |d| d.deinit(allocator); for (all_dividends.items) |d| d.deinit(allocator);
@ -84,15 +82,13 @@ pub const Polygon = struct {
n += 1; n += 1;
} }
const url = http.buildUrl(allocator, base_url ++ "/v3/reference/dividends", params[0..n]) catch const url = try http.buildUrl(allocator, base_url ++ "/v3/reference/dividends", params[0..n]);
return provider.ProviderError.OutOfMemory;
defer allocator.free(url); defer allocator.free(url);
const authed = appendApiKey(allocator, url, self.api_key) catch const authed = try appendApiKey(allocator, url, self.api_key);
return provider.ProviderError.OutOfMemory;
defer allocator.free(authed); 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(); defer response.deinit();
next_url = try parseDividendsPage(allocator, response.body, &all_dividends); next_url = try parseDividendsPage(allocator, response.body, &all_dividends);
@ -102,18 +98,17 @@ pub const Polygon = struct {
while (next_url) |cursor_url| { while (next_url) |cursor_url| {
self.rate_limiter.acquire(); self.rate_limiter.acquire();
const authed = appendApiKey(allocator, cursor_url, self.api_key) catch const authed = try appendApiKey(allocator, cursor_url, self.api_key);
return provider.ProviderError.OutOfMemory;
defer allocator.free(authed); 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(); defer response.deinit();
allocator.free(cursor_url); allocator.free(cursor_url);
next_url = try parseDividendsPage(allocator, response.body, &all_dividends); 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. /// Fetch split history for a ticker. Results sorted oldest-first.
@ -122,21 +117,20 @@ pub const Polygon = struct {
self: *Polygon, self: *Polygon,
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
symbol: []const u8, symbol: []const u8,
) provider.ProviderError![]Split { ) ![]Split {
self.rate_limiter.acquire(); 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 }, .{ "ticker", symbol },
.{ "limit", "1000" }, .{ "limit", "1000" },
.{ "sort", "execution_date" }, .{ "sort", "execution_date" },
}) catch return provider.ProviderError.OutOfMemory; });
defer allocator.free(url); defer allocator.free(url);
const authed = appendApiKey(allocator, url, self.api_key) catch const authed = try appendApiKey(allocator, url, self.api_key);
return provider.ProviderError.OutOfMemory;
defer allocator.free(authed); 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(); defer response.deinit();
return parseSplitsResponse(allocator, response.body); return parseSplitsResponse(allocator, response.body);
@ -150,7 +144,7 @@ pub const Polygon = struct {
symbol: []const u8, symbol: []const u8,
from: Date, from: Date,
to: Date, to: Date,
) provider.ProviderError![]Candle { ) ![]Candle {
self.rate_limiter.acquire(); self.rate_limiter.acquire();
var from_buf: [10]u8 = undefined; var from_buf: [10]u8 = undefined;
@ -159,46 +153,21 @@ pub const Polygon = struct {
const to_str = to.format(&to_buf); const to_str = to.format(&to_buf);
// Build URL manually since the path contains the date range // Build URL manually since the path contains the date range
const path = std.fmt.allocPrint( const path = try std.fmt.allocPrint(
allocator, allocator,
"{s}/v2/aggs/ticker/{s}/range/1/day/{s}/{s}?adjusted=true&sort=asc&limit=5000", "{s}/v2/aggs/ticker/{s}/range/1/day/{s}/{s}?adjusted=true&sort=asc&limit=5000",
.{ base_url, symbol, from_str, to_str }, .{ base_url, symbol, from_str, to_str },
) catch return provider.ProviderError.OutOfMemory; );
defer allocator.free(path); defer allocator.free(path);
const authed = appendApiKey(allocator, path, self.api_key) catch const authed = try appendApiKey(allocator, path, self.api_key);
return provider.ProviderError.OutOfMemory;
defer allocator.free(authed); 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(); defer response.deinit();
return parseCandlesResponse(allocator, response.body); 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 -- // -- JSON parsing --
@ -207,9 +176,9 @@ fn parseDividendsPage(
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
body: []const u8, body: []const u8,
out: *std.ArrayList(Dividend), out: *std.ArrayList(Dividend),
) provider.ProviderError!?[]const u8 { ) !?[]const u8 {
const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch
return provider.ProviderError.ParseError; return error.ParseError;
defer parsed.deinit(); defer parsed.deinit();
const root = parsed.value.object; const root = parsed.value.object;
@ -217,7 +186,7 @@ fn parseDividendsPage(
// Check status // Check status
if (root.get("status")) |s| { if (root.get("status")) |s| {
if (s == .string and std.mem.eql(u8, s.string, "ERROR")) 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; const results = root.get("results") orelse return null;
@ -244,7 +213,7 @@ fn parseDividendsPage(
const amount = parseJsonFloat(obj.get("cash_amount")); const amount = parseJsonFloat(obj.get("cash_amount"));
if (amount <= 0) continue; if (amount <= 0) continue;
out.append(allocator, .{ try out.append(allocator, .{
.ex_date = ex_date, .ex_date = ex_date,
.amount = amount, .amount = amount,
.pay_date = parseDateField(obj, "pay_date"), .pay_date = parseDateField(obj, "pay_date"),
@ -255,7 +224,7 @@ fn parseDividendsPage(
(allocator.dupe(u8, s) catch null) (allocator.dupe(u8, s) catch null)
else else
null, null,
}) catch return provider.ProviderError.OutOfMemory; });
} }
// Check for next_url (pagination cursor) // Check for next_url (pagination cursor)
@ -264,34 +233,32 @@ fn parseDividendsPage(
.string => |s| s, .string => |s| s,
else => return null, 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 duped;
} }
return null; 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 const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch
return provider.ProviderError.ParseError; return error.ParseError;
defer parsed.deinit(); defer parsed.deinit();
const root = parsed.value.object; const root = parsed.value.object;
if (root.get("status")) |s| { if (root.get("status")) |s| {
if (s == .string and std.mem.eql(u8, s.string, "ERROR")) 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 results = root.get("results") orelse {
const empty = allocator.alloc(Split, 0) catch return provider.ProviderError.OutOfMemory; return try allocator.alloc(Split, 0);
return empty;
}; };
const items = switch (results) { const items = switch (results) {
.array => |a| a.items, .array => |a| a.items,
else => { else => {
const empty = allocator.alloc(Split, 0) catch return provider.ProviderError.OutOfMemory; return try allocator.alloc(Split, 0);
return empty;
}, },
}; };
@ -313,37 +280,35 @@ fn parseSplitsResponse(allocator: std.mem.Allocator, body: []const u8) provider.
break :blk Date.parse(s) catch continue; break :blk Date.parse(s) catch continue;
}; };
splits.append(allocator, .{ try splits.append(allocator, .{
.date = date, .date = date,
.numerator = parseJsonFloat(obj.get("split_to")), .numerator = parseJsonFloat(obj.get("split_to")),
.denominator = parseJsonFloat(obj.get("split_from")), .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 const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch
return provider.ProviderError.ParseError; return error.ParseError;
defer parsed.deinit(); defer parsed.deinit();
const root = parsed.value.object; const root = parsed.value.object;
if (root.get("status")) |s| { if (root.get("status")) |s| {
if (s == .string and std.mem.eql(u8, s.string, "ERROR")) 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 results = root.get("results") orelse {
const empty = allocator.alloc(Candle, 0) catch return provider.ProviderError.OutOfMemory; return try allocator.alloc(Candle, 0);
return empty;
}; };
const items = switch (results) { const items = switch (results) {
.array => |a| a.items, .array => |a| a.items,
else => { else => {
const empty = allocator.alloc(Candle, 0) catch return provider.ProviderError.OutOfMemory; return try allocator.alloc(Candle, 0);
return empty;
}, },
}; };
@ -370,7 +335,7 @@ fn parseCandlesResponse(allocator: std.mem.Allocator, body: []const u8) provider
const close = parseJsonFloat(obj.get("c")); const close = parseJsonFloat(obj.get("c"));
candles.append(allocator, .{ try candles.append(allocator, .{
.date = date, .date = date,
.open = parseJsonFloat(obj.get("o")), .open = parseJsonFloat(obj.get("o")),
.high = parseJsonFloat(obj.get("h")), .high = parseJsonFloat(obj.get("h")),
@ -378,10 +343,10 @@ fn parseCandlesResponse(allocator: std.mem.Allocator, body: []const u8) provider
.close = close, .close = close,
.adj_close = close, // Polygon adjusted=true gives adjusted values .adj_close = close, // Polygon adjusted=true gives adjusted values
.volume = @intFromFloat(parseJsonFloat(obj.get("v"))), .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 -- // -- Helpers --
@ -506,7 +471,7 @@ test "parseDividendsPage error status" {
} }
const result = parseDividendsPage(allocator, body, &out); const result = parseDividendsPage(allocator, body, &out);
try std.testing.expectError(provider.ProviderError.RequestFailed, result); try std.testing.expectError(error.RequestFailed, result);
} }
test "parseSplitsResponse basic" { test "parseSplitsResponse basic" {

View file

@ -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;
}
};

View file

@ -12,7 +12,6 @@ const http = @import("../net/http.zig");
const RateLimiter = @import("../net/RateLimiter.zig"); const RateLimiter = @import("../net/RateLimiter.zig");
const Date = @import("../models/date.zig").Date; const Date = @import("../models/date.zig").Date;
const Candle = @import("../models/candle.zig").Candle; const Candle = @import("../models/candle.zig").Candle;
const provider = @import("provider.zig");
const json_utils = @import("json_utils.zig"); const json_utils = @import("json_utils.zig");
const parseJsonFloat = json_utils.parseJsonFloat; const parseJsonFloat = json_utils.parseJsonFloat;
@ -45,7 +44,7 @@ pub const TwelveData = struct {
symbol: []const u8, symbol: []const u8,
from: Date, from: Date,
to: Date, to: Date,
) provider.ProviderError![]Candle { ) ![]Candle {
self.rate_limiter.acquire(); self.rate_limiter.acquire();
var from_buf: [10]u8 = undefined; var from_buf: [10]u8 = undefined;
@ -53,22 +52,17 @@ pub const TwelveData = struct {
const from_str = from.format(&from_buf); const from_str = from.format(&from_buf);
const to_str = to.format(&to_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 }, .{ "symbol", symbol },
.{ "interval", "1day" }, .{ "interval", "1day" },
.{ "start_date", from_str }, .{ "start_date", from_str },
.{ "end_date", to_str }, .{ "end_date", to_str },
.{ "outputsize", "5000" }, .{ "outputsize", "5000" },
.{ "apikey", self.api_key }, .{ "apikey", self.api_key },
}) catch return provider.ProviderError.OutOfMemory; });
defer allocator.free(url); defer allocator.free(url);
var response = self.client.get(url) catch |err| return switch (err) { var response = try self.client.get(url);
error.RateLimited => provider.ProviderError.RateLimited,
error.Unauthorized => provider.ProviderError.Unauthorized,
error.NotFound => provider.ProviderError.NotFound,
else => provider.ProviderError.RequestFailed,
};
defer response.deinit(); defer response.deinit();
return parseTimeSeriesResponse(allocator, response.body); return parseTimeSeriesResponse(allocator, response.body);
@ -84,7 +78,7 @@ pub const TwelveData = struct {
/// Parse and print quote data. Caller should use this within the /// Parse and print quote data. Caller should use this within the
/// lifetime of the QuoteResponse. /// 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); return parseQuoteBody(allocator, self.body);
} }
}; };
@ -160,21 +154,16 @@ pub const TwelveData = struct {
self: *TwelveData, self: *TwelveData,
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
symbol: []const u8, symbol: []const u8,
) provider.ProviderError!QuoteResponse { ) !QuoteResponse {
self.rate_limiter.acquire(); self.rate_limiter.acquire();
const url = http.buildUrl(allocator, base_url ++ "/quote", &.{ const url = try http.buildUrl(allocator, base_url ++ "/quote", &.{
.{ "symbol", symbol }, .{ "symbol", symbol },
.{ "apikey", self.api_key }, .{ "apikey", self.api_key },
}) catch return provider.ProviderError.OutOfMemory; });
defer allocator.free(url); defer allocator.free(url);
var response = self.client.get(url) catch |err| return switch (err) { var response = try self.client.get(url);
error.RateLimited => provider.ProviderError.RateLimited,
error.Unauthorized => provider.ProviderError.Unauthorized,
error.NotFound => provider.ProviderError.NotFound,
else => provider.ProviderError.RequestFailed,
};
// Transfer ownership of body to QuoteResponse // Transfer ownership of body to QuoteResponse
const body = response.body; const body = response.body;
@ -185,35 +174,13 @@ pub const TwelveData = struct {
.allocator = allocator, .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 -- // -- 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 const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch
return provider.ProviderError.ParseError; return error.ParseError;
defer parsed.deinit(); defer parsed.deinit();
const root = parsed.value; 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")) { if (std.mem.eql(u8, status.string, "error")) {
// Check error code // Check error code
if (root.object.get("code")) |code| { if (root.object.get("code")) |code| {
if (code == .integer and code.integer == 429) return provider.ProviderError.RateLimited; if (code == .integer and code.integer == 429) return error.RateLimited;
if (code == .integer and code.integer == 401) return provider.ProviderError.Unauthorized; 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) { const values = switch (values_json) {
.array => |a| a.items, .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. // 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; break :blk Date.parse(date_part) catch continue;
}; };
candles.append(allocator, .{ try candles.append(allocator, .{
.date = date, .date = date,
.open = parseJsonFloat(obj.get("open")), .open = parseJsonFloat(obj.get("open")),
.high = parseJsonFloat(obj.get("high")), .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 // Twelve Data close is split-adjusted only, not dividend-adjusted
.adj_close = parseJsonFloat(obj.get("close")), .adj_close = parseJsonFloat(obj.get("close")),
.volume = @intFromFloat(parseJsonFloat(obj.get("volume"))), .volume = @intFromFloat(parseJsonFloat(obj.get("volume"))),
}) catch return provider.ProviderError.OutOfMemory; });
} }
// Reverse to get oldest-first ordering // 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); std.mem.reverse(Candle, slice);
return 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 const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch
return provider.ProviderError.ParseError; return error.ParseError;
// Check for error // Check for error
if (parsed.value.object.get("status")) |status| { if (parsed.value.object.get("status")) |status| {
if (status == .string and std.mem.eql(u8, status.string, "error")) { if (status == .string and std.mem.eql(u8, status.string, "error")) {
var p = parsed; var p = parsed;
p.deinit(); p.deinit();
return provider.ProviderError.RequestFailed; return error.RequestFailed;
} }
} }