From 7ac17313d19fca604f873f546e9fae52b4783a90 Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Sat, 10 Aug 2024 00:12:45 +0300 Subject: [PATCH] add getting items in bank --- src/artifacts.zig | 242 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 179 insertions(+), 63 deletions(-) diff --git a/src/artifacts.zig b/src/artifacts.zig index 266c550..354635d 100644 --- a/src/artifacts.zig +++ b/src/artifacts.zig @@ -361,26 +361,32 @@ pub const FightResult = struct { } }; -pub const SkillDetails = struct { - pub const Item = struct { - id: ItemId, - quantity: i64 - }; +pub const ItemQuantity = struct { + id: ItemId, + quantity: u64, + pub fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !ItemQuantity { + const code = try json_utils.getStringRequired(obj, "code"); + const quantity = try json_utils.getIntegerRequired(obj, "quantity"); + + return ItemQuantity{ + .id = try api.getItemId(code), + .quantity = @intCast(quantity) + }; + } +}; + +pub const SkillDetails = struct { xp: i64, - items: std.BoundedArray(Item, 8), + items: std.BoundedArray(ItemQuantity, 8), fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !SkillDetails { - var items = std.BoundedArray(Item, 8).init(0) catch unreachable; + var items = std.BoundedArray(ItemQuantity, 8).init(0) catch unreachable; const items_obj = json_utils.getArray(obj, "items") orelse return error.MissingProperty; for (items_obj.items) |item_value| { const item_obj = json_utils.asObject(item_value) orelse return error.MissingProperty; - const code = try json_utils.getStringRequired(item_obj, "code"); - try items.append(Item{ - .id = try api.getItemId(code), - .quantity = try json_utils.getIntegerRequired(item_obj, "quantity") - }); + try items.append(ItemQuantity.parse(api, item_obj)); } return SkillDetails{ @@ -655,64 +661,142 @@ pub fn deinit(self: *ArtifactsAPI) void { const FetchOptions = struct { method: std.http.Method, path: []const u8, - payload: ?[]const u8 = null + payload: ?[]const u8 = null, + + page: ?u64 = null, + page_size: ?u64 = null, + paginated: bool = false }; +fn allocPaginationParams(allocator: Allocator, page: u64, page_size: ?u64) ![]u8 { + if (page_size) |size| { + return try std.fmt.allocPrint(allocator, "page={}&size={}", .{page, size}); + } else { + return try std.fmt.allocPrint(allocator, "page={}", .{page}); + } +} + fn fetch(self: *ArtifactsAPI, options: FetchOptions) APIError!ArtifactsFetchResult { const method = options.method; const path = options.path; const payload = options.payload; - std.log.debug("fetch {} {s}", .{method, path}); var uri = self.server_uri; uri.path = .{ .raw = path }; var arena = std.heap.ArenaAllocator.init(self.allocator); errdefer arena.deinit(); - var response_storage = std.ArrayList(u8).init(arena.allocator()); + var result_status: std.http.Status = .ok; + var result_body: ?json.Value = null; - var opts = std.http.Client.FetchOptions{ - .method = method, - .location = .{ .uri = uri }, - .payload = payload, - .response_storage = .{ .dynamic = &response_storage }, - }; + var current_page: u64 = options.page orelse 1; + var total_pages: u64 = 1; + var fetch_results = std.ArrayList(json.Value).init(arena.allocator()); - var authorization_header: ?[]u8 = null; - defer if (authorization_header) |str| self.allocator.free(str); + while (true) : (current_page += 1) { + var query_params: ?[]u8 = null; + defer if (query_params) |str| self.allocator.free(str); - if (self.token) |token| { - authorization_header = std.fmt.allocPrint(self.allocator, "Bearer {s}", .{token}) catch return APIError.OutOfMemory; - opts.headers.authorization = .{ .override = authorization_header.? }; - } + if (options.paginated) { + query_params = try allocPaginationParams(self.allocator, current_page, options.page_size); + uri.query = .{ .raw = query_params.? }; + } - const result = self.client.fetch(opts) catch return APIError.RequestFailed; - const response_body = response_storage.items; - - std.log.debug("fetch result {}", .{result.status}); - - if (result.status == .service_unavailable) { - return APIError.ServerUnavailable; - } else if (result.status != .ok) { - return ArtifactsFetchResult{ - .arena = arena, - .status = result.status + var response_storage = std.ArrayList(u8).init(arena.allocator()); + var opts = std.http.Client.FetchOptions{ + .method = method, + .location = .{ .uri = uri }, + .payload = payload, + .response_storage = .{ .dynamic = &response_storage }, }; + + var authorization_header: ?[]u8 = null; + defer if (authorization_header) |str| self.allocator.free(str); + + if (self.token) |token| { + authorization_header = std.fmt.allocPrint(self.allocator, "Bearer {s}", .{token}) catch return APIError.OutOfMemory; + opts.headers.authorization = .{ .override = authorization_header.? }; + } + + std.log.debug("fetch {} {s}", .{method, path}); + const result = self.client.fetch(opts) catch return APIError.RequestFailed; + const response_body = response_storage.items; + + std.log.debug("fetch result {}", .{result.status}); + + if (result.status == .service_unavailable) { + return APIError.ServerUnavailable; + } else if (result.status != .ok) { + return ArtifactsFetchResult{ + .arena = arena, + .status = result.status + }; + } + + const parsed = json.parseFromSliceLeaky(json.Value, arena.allocator(), response_body, .{ .allocate = .alloc_if_needed }) catch return APIError.ParseFailed; + if (parsed != json.Value.object) { + return APIError.ParseFailed; + } + + result_status = result.status; + + if (options.paginated) { + const total_pages_i64 = json_utils.getInteger(parsed.object, "pages") orelse return APIError.ParseFailed; + total_pages = @intCast(total_pages_i64); + + const page_results = json_utils.getArray(parsed.object, "data") orelse return APIError.ParseFailed; + fetch_results.appendSlice(page_results.items) catch return APIError.OutOfMemory; + + if (current_page >= total_pages) break; + } else { + result_body = parsed.object.get("data"); + break; + } } - const parsed = json.parseFromSliceLeaky(json.Value, arena.allocator(), response_body, .{ .allocate = .alloc_if_needed }) catch return APIError.ParseFailed; - if (parsed != json.Value.object) { - return APIError.ParseFailed; + if (options.paginated) { + result_body = json.Value{ .array = fetch_results }; } return ArtifactsFetchResult{ - .status = result.status, + .status = result_status, .arena = arena, - .body = parsed.object.get("data") + .body = result_body }; } +fn handleFetchError( + status: std.http.Status, + Error: type, + parseError: ?fn (status: std.http.Status) ?Error, +) ?Error { + if (status != .ok) { + if (Error != APIError) { + if (parseError == null) { + @compileError("`parseError` must be defined, if `Error` is not `APIError`"); + } + + if (parseError.?(status)) |error_value| { + return error_value; + } + } else { + if (parseError != null) { + @compileError("`parseError` must be null"); + } + } + } + + if (status == .not_found) { + return null; + } + if (status != .ok) { + return APIError.RequestFailed; + } + + return null; +} + fn fetchOptionalObject( self: *ArtifactsAPI, Error: type, @@ -729,28 +813,10 @@ fn fetchOptionalObject( const result = try self.fetch(fetchOptions); defer result.deinit(); - if (result.status != .ok) { - if (Error != APIError) { - if (parseError == null) { - @compileError("`parseError` must be defined, if `Error` is not `APIError`"); - } - - if (parseError.?(result.status)) |error_value| { - return error_value; - } - } else { - if (parseError != null) { - @compileError("`parseError` must be null"); - } - } + if (handleFetchError(result.status, Error, parseError)) |error_value| { + return error_value; } - if (result.status == .not_found) { - return null; - } - if (result.status != .ok) { - return APIError.RequestFailed; - } if (result.body == null) { return APIError.ParseFailed; } @@ -772,6 +838,45 @@ fn fetchObject( return result orelse return APIError.RequestFailed; } +fn fetchArray( + self: *ArtifactsAPI, + allocator: Allocator, + Error: type, + parseError: ?fn (status: std.http.Status) ?Error, + Object: type, + parseObject: anytype, + parseObjectArgs: anytype, + fetchOptions: FetchOptions +) Error!std.ArrayList(Object) { + if (@typeInfo(@TypeOf(parseObject)) != .Fn) { + @compileError("`parseObject` must be a function"); + } + + const result = try self.fetch(fetchOptions); + defer result.deinit(); + + if (handleFetchError(result.status, Error, parseError)) |error_value| { + return error_value; + } + + if (result.body == null) { + return APIError.ParseFailed; + } + + var array = std.ArrayList(Object).init(allocator); + errdefer array.deinit(); + + const result_data = json_utils.asArray(result.body.?) orelse return APIError.ParseFailed; + for (result_data.items) |result_item| { + const item_obj = json_utils.asObject(result_item) orelse return APIError.ParseFailed; + + const parsed_item = @call(.auto, parseObject, .{ self, item_obj } ++ parseObjectArgs) catch return APIError.ParseFailed; + array.append(parsed_item) catch return APIError.OutOfMemory; + } + + return array; +} + pub fn setServer(self: *ArtifactsAPI, url: []const u8) !void { const url_dupe = self.allocator.dupe(u8, url); errdefer self.allocator.free(url_dupe); @@ -1082,6 +1187,17 @@ pub fn getBankGold(self: *ArtifactsAPI) APIError!u64 { return @intCast(quantity); } +pub fn getBankItems(self: *ArtifactsAPI) APIError!std.ArrayList(ItemQuantity) { + return self.fetchArray( + self.allocator, + APIError, + null, + ItemQuantity, + ItemQuantity.parse, .{}, + .{ .method = .GET, .path = "/my/bank/items", .paginated = true } + ); +} + test "parse date time" { try std.testing.expectEqual(1723069394.105, parseDateTime("2024-08-07T22:23:14.105Z").?); }