From 373c30d947fd7e5ab9ae2e83b7fd0eea4369a948 Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Fri, 6 Mar 2026 16:35:57 -0800 Subject: [PATCH] fix fetchQuote idiocy --- src/providers/twelvedata.zig | 172 ++++++++++------------------------- src/service.zig | 25 +---- 2 files changed, 51 insertions(+), 146 deletions(-) diff --git a/src/providers/twelvedata.zig b/src/providers/twelvedata.zig index c30a140..8f3c34f 100644 --- a/src/providers/twelvedata.zig +++ b/src/providers/twelvedata.zig @@ -12,6 +12,7 @@ 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 Quote = @import("../models/quote.zig").Quote; const json_utils = @import("json_utils.zig"); const parseJsonFloat = json_utils.parseJsonFloat; @@ -68,93 +69,11 @@ pub const TwelveData = struct { return parseTimeSeriesResponse(allocator, response.body); } - pub const QuoteResponse = struct { - body: []const u8, - allocator: std.mem.Allocator, - - pub fn deinit(self: *QuoteResponse) void { - self.allocator.free(self.body); - } - - /// Parse and print quote data. Caller should use this within the - /// lifetime of the QuoteResponse. - pub fn parse(self: QuoteResponse, allocator: std.mem.Allocator) !ParsedQuote { - return parseQuoteBody(allocator, self.body); - } - }; - - pub const ParsedQuote = struct { - parsed: std.json.Parsed(std.json.Value), - - pub fn deinit(self: *ParsedQuote) void { - self.parsed.deinit(); - } - - fn root(self: ParsedQuote) std.json.ObjectMap { - return self.parsed.value.object; - } - - pub fn symbol(self: ParsedQuote) []const u8 { - return jsonStr(self.root().get("symbol")); - } - pub fn name(self: ParsedQuote) []const u8 { - return jsonStr(self.root().get("name")); - } - pub fn exchange(self: ParsedQuote) []const u8 { - return jsonStr(self.root().get("exchange")); - } - pub fn datetime(self: ParsedQuote) []const u8 { - return jsonStr(self.root().get("datetime")); - } - pub fn close(self: ParsedQuote) f64 { - return parseJsonFloat(self.root().get("close")); - } - pub fn open(self: ParsedQuote) f64 { - return parseJsonFloat(self.root().get("open")); - } - pub fn high(self: ParsedQuote) f64 { - return parseJsonFloat(self.root().get("high")); - } - pub fn low(self: ParsedQuote) f64 { - return parseJsonFloat(self.root().get("low")); - } - pub fn volume(self: ParsedQuote) u64 { - return @intFromFloat(parseJsonFloat(self.root().get("volume"))); - } - pub fn previous_close(self: ParsedQuote) f64 { - return parseJsonFloat(self.root().get("previous_close")); - } - pub fn change(self: ParsedQuote) f64 { - return parseJsonFloat(self.root().get("change")); - } - pub fn percent_change(self: ParsedQuote) f64 { - return parseJsonFloat(self.root().get("percent_change")); - } - pub fn average_volume(self: ParsedQuote) u64 { - return @intFromFloat(parseJsonFloat(self.root().get("average_volume"))); - } - - pub fn fifty_two_week_low(self: ParsedQuote) f64 { - const ftw = self.root().get("fifty_two_week") orelse return 0; - return switch (ftw) { - .object => |o| parseJsonFloat(o.get("low")), - else => 0, - }; - } - pub fn fifty_two_week_high(self: ParsedQuote) f64 { - const ftw = self.root().get("fifty_two_week") orelse return 0; - return switch (ftw) { - .object => |o| parseJsonFloat(o.get("high")), - else => 0, - }; - } - }; - pub fn fetchQuote( self: *TwelveData, allocator: std.mem.Allocator, symbol: []const u8, - ) !QuoteResponse { + ) !Quote { self.rate_limiter.acquire(); const url = try http.buildUrl(allocator, base_url ++ "/quote", &.{ @@ -164,15 +83,9 @@ pub const TwelveData = struct { defer allocator.free(url); var response = try self.client.get(url); + defer response.deinit(); - // Transfer ownership of body to QuoteResponse - const body = response.body; - response.body = &.{}; - - return .{ - .body = body, - .allocator = allocator, - }; + return parseQuoteResponse(allocator, response.body, symbol); } }; @@ -244,28 +157,47 @@ fn parseTimeSeriesResponse(allocator: std.mem.Allocator, body: []const u8) ![]Ca return slice; } -fn parseQuoteBody(allocator: std.mem.Allocator, body: []const u8) !TwelveData.ParsedQuote { +fn parseQuoteResponse(allocator: std.mem.Allocator, body: []const u8, symbol: []const u8) !Quote { const parsed = std.json.parseFromSlice(std.json.Value, allocator, body, .{}) catch return error.ParseError; + defer parsed.deinit(); + + const root = parsed.value.object; // Check for error - if (parsed.value.object.get("status")) |status| { + if (root.get("status")) |status| { if (status == .string and std.mem.eql(u8, status.string, "error")) { - var p = parsed; - p.deinit(); + if (root.get("code")) |code| { + if (code == .integer and code.integer == 429) return error.RateLimited; + } return error.RequestFailed; } } - return .{ .parsed = parsed }; -} + const ftw = root.get("fifty_two_week"); -/// TwelveData-specific: returns empty string instead of null for quote display convenience. -fn jsonStr(val: ?std.json.Value) []const u8 { - const v = val orelse return ""; - return switch (v) { - .string => |s| s, - else => "", + return .{ + .symbol = symbol, + .name = symbol, + .exchange = "", + .datetime = "", + .close = parseJsonFloat(root.get("close")), + .open = parseJsonFloat(root.get("open")), + .high = parseJsonFloat(root.get("high")), + .low = parseJsonFloat(root.get("low")), + .volume = @intFromFloat(parseJsonFloat(root.get("volume"))), + .previous_close = parseJsonFloat(root.get("previous_close")), + .change = parseJsonFloat(root.get("change")), + .percent_change = parseJsonFloat(root.get("percent_change")), + .average_volume = @intFromFloat(parseJsonFloat(root.get("average_volume"))), + .fifty_two_week_low = if (ftw) |f| switch (f) { + .object => |o| parseJsonFloat(o.get("low")), + else => 0, + } else 0, + .fifty_two_week_high = if (ftw) |f| switch (f) { + .object => |o| parseJsonFloat(o.get("high")), + else => 0, + } else 0, }; } @@ -371,7 +303,7 @@ test "parseTimeSeriesResponse empty values" { try std.testing.expectEqual(@as(usize, 0), candles.len); } -test "parseQuoteBody basic" { +test "parseQuoteResponse basic" { const body = \\{ \\ "symbol": "AAPL", @@ -395,26 +327,22 @@ test "parseQuoteBody basic" { ; const allocator = std.testing.allocator; - var quote = try parseQuoteBody(allocator, body); - defer quote.deinit(); + const quote = try parseQuoteResponse(allocator, body, "AAPL"); - try std.testing.expectEqualStrings("AAPL", quote.symbol()); - try std.testing.expectEqualStrings("Apple Inc", quote.name()); - try std.testing.expectEqualStrings("NASDAQ", quote.exchange()); - try std.testing.expectApproxEqAbs(@as(f64, 183.63), quote.close(), 0.01); - try std.testing.expectApproxEqAbs(@as(f64, 182.15), quote.open(), 0.01); - try std.testing.expectApproxEqAbs(@as(f64, 185.0), quote.high(), 0.01); - try std.testing.expectApproxEqAbs(@as(f64, 181.5), quote.low(), 0.01); - try std.testing.expectEqual(@as(u64, 65000000), quote.volume()); - try std.testing.expectApproxEqAbs(@as(f64, 181.18), quote.previous_close(), 0.01); - try std.testing.expectApproxEqAbs(@as(f64, 2.45), quote.change(), 0.01); - try std.testing.expectApproxEqAbs(@as(f64, 1.35), quote.percent_change(), 0.01); - try std.testing.expectEqual(@as(u64, 55000000), quote.average_volume()); - try std.testing.expectApproxEqAbs(@as(f64, 140.0), quote.fifty_two_week_low(), 0.01); - try std.testing.expectApproxEqAbs(@as(f64, 200.0), quote.fifty_two_week_high(), 0.01); + try std.testing.expectApproxEqAbs(@as(f64, 183.63), quote.close, 0.01); + try std.testing.expectApproxEqAbs(@as(f64, 182.15), quote.open, 0.01); + try std.testing.expectApproxEqAbs(@as(f64, 185.0), quote.high, 0.01); + try std.testing.expectApproxEqAbs(@as(f64, 181.5), quote.low, 0.01); + try std.testing.expectEqual(@as(u64, 65000000), quote.volume); + try std.testing.expectApproxEqAbs(@as(f64, 181.18), quote.previous_close, 0.01); + try std.testing.expectApproxEqAbs(@as(f64, 2.45), quote.change, 0.01); + try std.testing.expectApproxEqAbs(@as(f64, 1.35), quote.percent_change, 0.01); + try std.testing.expectEqual(@as(u64, 55000000), quote.average_volume); + try std.testing.expectApproxEqAbs(@as(f64, 140.0), quote.fifty_two_week_low, 0.01); + try std.testing.expectApproxEqAbs(@as(f64, 200.0), quote.fifty_two_week_high, 0.01); } -test "parseQuoteBody error response" { +test "parseQuoteResponse error response" { const body = \\{ \\ "status": "error", @@ -424,7 +352,7 @@ test "parseQuoteBody error response" { ; const allocator = std.testing.allocator; - const result = parseQuoteBody(allocator, body); + const result = parseQuoteResponse(allocator, body, "BAD"); try std.testing.expectError(error.RequestFailed, result); } diff --git a/src/service.zig b/src/service.zig index 8121f63..4372c6b 100644 --- a/src/service.zig +++ b/src/service.zig @@ -325,31 +325,8 @@ pub const DataService = struct { /// No cache -- always fetches fresh from TwelveData. pub fn getQuote(self: *DataService, symbol: []const u8) DataError!Quote { var td = try self.getTwelveData(); - var resp = td.fetchQuote(self.allocator, symbol) catch + return td.fetchQuote(self.allocator, symbol) catch return DataError.FetchFailed; - defer resp.deinit(); - - var parsed = resp.parse(self.allocator) catch - return DataError.ParseError; - defer parsed.deinit(); - - return .{ - .symbol = symbol, - .name = symbol, // name is in parsed JSON but lifetime is tricky; use symbol - .exchange = "", - .datetime = "", - .close = parsed.close(), - .open = parsed.open(), - .high = parsed.high(), - .low = parsed.low(), - .volume = parsed.volume(), - .previous_close = parsed.previous_close(), - .change = parsed.change(), - .percent_change = parsed.percent_change(), - .average_volume = parsed.average_volume(), - .fifty_two_week_low = parsed.fifty_two_week_low(), - .fifty_two_week_high = parsed.fifty_two_week_high(), - }; } /// Fetch company overview (sector, industry, country, market cap) from Alpha Vantage.