diff --git a/src/artifacts.zig b/src/artifacts.zig index 7666d28..acdd5e2 100644 --- a/src/artifacts.zig +++ b/src/artifacts.zig @@ -22,9 +22,74 @@ token: ?[]u8 = null, item_codes: std.ArrayList([]u8), -pub const APIErrors = error { +pub const APIError = error { RequestFailed, - ParseFailed + ParseFailed, + OutOfMemory +}; + +pub const MoveError = APIError || error { + MapNotFound, + CharacterIsBusy, + CharacterAtDestination, + CharacterNotFound, + CharacterInCooldown +}; + +pub const FightError = APIError || error { + CharacterIsBusy, + CharacterIsFull, + CharacterNotFound, + CharacterInCooldown, + MonsterNotFound, +}; + +pub const GatheringError = APIError || error { + CharacterIsBusy, + CharacterMissingSkill, + CharacterIsFull, + CharacterNotFound, + CharacterInCooldown, + ResourceNotFound +}; + +pub const BankDepositItemError = APIError || error { + ItemNotFound, + BankIsBusy, + NotEnoughItems, + CharacterIsBusy, + CharacterNotFound, + CharacterInCooldown, + BankNotFound +}; + +pub const BankDepositGoldError = APIError || error { + BankIsBusy, + NotEnoughGold, + CharacterIsBusy, + CharacterNotFound, + CharacterInCooldown, + BankNotFound +}; + +pub const BankWithdrawGoldError = APIError || error { + BankIsBusy, + NotEnoughGold, + CharacterIsBusy, + CharacterNotFound, + CharacterInCooldown, + BankNotFound +}; + +pub const BankWithdrawItemError = APIError || error { + ItemNotFound, + BankIsBusy, + NotEnoughItems, + CharacterIsBusy, + CharacterIsFull, + CharacterNotFound, + CharacterInCooldown, + BankNotFound }; const ServerStatus = struct { @@ -33,7 +98,7 @@ const ServerStatus = struct { version: []const u8, characters_online: i64, - fn parse(api: *ArtifactsAPI, object: json.ObjectMap, allocator: Allocator) !ServerStatus { + pub fn parse(api: *ArtifactsAPI, object: json.ObjectMap, allocator: Allocator) !ServerStatus { _ = api; return ServerStatus{ @@ -152,7 +217,7 @@ pub const Cooldown = struct { expiration: f64, reason: Reason, - fn parse(obj: json.ObjectMap) !Cooldown { + pub fn parse(obj: json.ObjectMap) !Cooldown { const reason = try json_utils.getStringRequired(obj, "reason"); const expiration = try json_utils.getStringRequired(obj, "expiration"); @@ -210,7 +275,7 @@ pub const FightResult = struct { cooldown: Cooldown, fight: Details, - fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !FightResult { + pub fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !FightResult { const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty; const fight = json_utils.getObject(obj, "fight") orelse return error.MissingProperty; @@ -219,6 +284,17 @@ pub const FightResult = struct { .fight = try Details.parse(api, fight) }; } + + pub fn parseError(status: std.http.Status) ?FightError { + return switch (@intFromEnum(status)) { + 486 => return FightError.CharacterIsBusy, + 497 => return FightError.CharacterIsFull, + 498 => return FightError.CharacterNotFound, + 499 => return FightError.CharacterInCooldown, + 598 => return FightError.MonsterNotFound, + else => return null + }; + } }; pub const GatheringResult = struct { @@ -254,7 +330,7 @@ pub const GatheringResult = struct { cooldown: Cooldown, details: Details, - fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !GatheringResult { + pub fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !GatheringResult { const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty; const details = json_utils.getObject(obj, "details") orelse return error.MissingProperty; @@ -263,12 +339,24 @@ pub const GatheringResult = struct { .details = try Details.parse(api, details) }; } + + pub fn parseError(status: std.http.Status) ?GatheringError { + return switch (@intFromEnum(status)) { + 486 => return GatheringError.CharacterIsBusy, + 493 => return GatheringError.CharacterMissingSkill, + 497 => return GatheringError.CharacterIsFull, + 498 => return GatheringError.CharacterNotFound, + 499 => return GatheringError.CharacterInCooldown, + 598 => return GatheringError.ResourceNotFound, + else => null + }; + } }; pub const MoveResult = struct { cooldown: Cooldown, - fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !MoveResult { + pub fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !MoveResult { _ = api; const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty; @@ -278,6 +366,17 @@ pub const MoveResult = struct { }; } + pub fn parseError(status: std.http.Status) ?MoveError { + return switch (@intFromEnum(status)) { + 404 => return MoveError.MapNotFound, + 486 => return MoveError.CharacterIsBusy, + 490 => return MoveError.CharacterAtDestination, + 498 => return MoveError.CharacterNotFound, + 499 => return MoveError.CharacterInCooldown, + else => null + }; + } + pub fn deinit(self: MoveResult) void { _ = self; } @@ -286,7 +385,7 @@ pub const MoveResult = struct { pub const GoldTransactionResult = struct { cooldown: Cooldown, - fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !GoldTransactionResult { + pub fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !GoldTransactionResult { _ = api; const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty; @@ -295,6 +394,31 @@ pub const GoldTransactionResult = struct { }; } + pub fn parseDepositError(status: std.http.Status) ?BankDepositGoldError { + return switch (@intFromEnum(status)) { + 461 => return BankDepositGoldError.BankIsBusy, + 478 => return BankDepositGoldError.NotEnoughGold, // TODO: This should maybe be removed + 486 => return BankDepositGoldError.CharacterIsBusy, + 492 => return BankDepositGoldError.NotEnoughGold, + 498 => return BankDepositGoldError.CharacterNotFound, + 499 => return BankDepositGoldError.CharacterInCooldown, + 598 => return BankDepositGoldError.BankNotFound, + else => return null + }; + } + + pub fn parseWithdrawError(status: std.http.Status) ?BankWithdrawGoldError { + return switch (@intFromEnum(status)) { + 460 => return BankWithdrawGoldError.NotEnoughGold, + 461 => return BankWithdrawGoldError.BankIsBusy, + 486 => return BankWithdrawGoldError.CharacterIsBusy, + 498 => return BankWithdrawGoldError.CharacterNotFound, + 499 => return BankWithdrawGoldError.CharacterInCooldown, + 598 => return BankWithdrawGoldError.BankNotFound, + else => return null + }; + } + pub fn deinit(self: GoldTransactionResult) void { _ = self; } @@ -303,7 +427,7 @@ pub const GoldTransactionResult = struct { pub const ItemTransactionResult = struct { cooldown: Cooldown, - fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !ItemTransactionResult { + pub fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !ItemTransactionResult { _ = api; const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty; @@ -312,6 +436,33 @@ pub const ItemTransactionResult = struct { }; } + pub fn parseDepositError(status: std.http.Status) ?BankDepositItemError { + return switch (@intFromEnum(status)) { + 404 => return BankDepositItemError.ItemNotFound, + 461 => return BankDepositItemError.BankIsBusy, + 478 => return BankDepositItemError.NotEnoughItems, + 486 => return BankDepositItemError.CharacterIsBusy, + 498 => return BankDepositItemError.CharacterNotFound, + 499 => return BankDepositItemError.CharacterInCooldown, + 598 => return BankDepositItemError.BankNotFound, + else => return null + }; + } + + pub fn parseWithdrawError(status: std.http.Status) ?BankWithdrawItemError { + return switch (@intFromEnum(status)) { + 404 => return BankWithdrawItemError.ItemNotFound, + 461 => return BankWithdrawItemError.BankIsBusy, + 478 => return BankWithdrawItemError.NotEnoughItems, + 486 => return BankWithdrawItemError.CharacterIsBusy, + 497 => return BankWithdrawItemError.CharacterIsFull, + 498 => return BankWithdrawItemError.CharacterNotFound, + 499 => return BankWithdrawItemError.CharacterInCooldown, + 598 => return BankWithdrawItemError.BankNotFound, + else => return null + }; + } + pub fn deinit(self: ItemTransactionResult) void { _ = self; } @@ -351,7 +502,17 @@ pub fn deinit(self: *ArtifactsAPI) void { self.item_codes.deinit(); } -fn fetch(self: *ArtifactsAPI, method: std.http.Method, path: []const u8, payload: ?[]const u8) !ArtifactsFetchResult { +const FetchOptions = struct { + method: std.http.Method, + path: []const u8, + payload: ?[]const u8 = null +}; + +fn fetch(self: *ArtifactsAPI, options: FetchOptions) !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 }; @@ -390,7 +551,7 @@ fn fetch(self: *ArtifactsAPI, method: std.http.Method, path: []const u8, payload const parsed = try json.parseFromSliceLeaky(json.Value, arena.allocator(), response_body, .{ .allocate = .alloc_if_needed }); if (parsed != json.Value.object) { - return APIErrors.ParseFailed; + return APIError.ParseFailed; } return ArtifactsFetchResult{ @@ -400,20 +561,63 @@ fn fetch(self: *ArtifactsAPI, method: std.http.Method, path: []const u8, payload }; } -fn fetchAndParseObject(self: *ArtifactsAPI, Result: type, method: std.http.Method, path: []const u8, payload: ?[]const u8, args: anytype) !Result { - const result = try self.fetch(method, path, payload); +fn fetchOptionalObject( + self: *ArtifactsAPI, + Error: type, + parseError: ?fn (status: std.http.Status) ?Error, + Object: type, + parseObject: anytype, + parseObjectArgs: anytype, + fetchOptions: FetchOptions, +) Error!?Object { + if (@typeInfo(@TypeOf(parseObject)) != .Fn) { + @compileError("`parseObject` must be a function"); + } + + const result = self.fetch(fetchOptions) catch return APIError.RequestFailed; defer result.deinit(); if (result.status != .ok) { - return APIErrors.RequestFailed; - } - if (result.body == null) { - return APIErrors.ParseFailed; + 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"); + } + } } - const body = json_utils.asObject(result.body.?) orelse return APIErrors.ParseFailed; - return @call(.auto, Result.parse, .{ self, body } ++ args) catch return APIErrors.ParseFailed; - // return Result.parse(self.allocator, body) catch return APIErrors.ParseFailed; + if (result.status == .not_found) { + return null; + } + if (result.status != .ok) { + return APIError.RequestFailed; + } + if (result.body == null) { + return APIError.ParseFailed; + } + + const body = json_utils.asObject(result.body.?) orelse return APIError.ParseFailed; + return @call(.auto, parseObject, .{ self, body } ++ parseObjectArgs) catch return APIError.ParseFailed; +} + +fn fetchObject( + self: *ArtifactsAPI, + Error: type, + parseError: ?fn (status: std.http.Status) ?Error, + Object: type, + parseObject: anytype, + parseObjectArgs: anytype, + fetchOptions: FetchOptions +) Error!Object { + const result = try self.fetchOptionalObject(Error, parseError, Object, parseObject, parseObjectArgs, fetchOptions); + return result orelse return APIError.RequestFailed; } pub fn setServer(self: *ArtifactsAPI, url: []const u8) !void { @@ -465,31 +669,43 @@ pub fn getItemCode(self: *const ArtifactsAPI, id: ItemId) ?[]const u8 { // ------------------------- Endpoints ------------------------ pub fn getServerStatus(self: *ArtifactsAPI) !ServerStatus { - return try self.fetchAndParseObject(ServerStatus, .GET, "/", null, .{ self.allocator }); + return try self.fetchObject( + APIError, + null, + ServerStatus, + ServerStatus.parse, .{ self.allocator }, + .{ .method = .GET, .path = "/" } + ); } -pub fn getCharacter(self: *ArtifactsAPI, name: []const u8) !Character { +pub fn getCharacter(self: *ArtifactsAPI, name: []const u8) !?Character { const path = try std.fmt.allocPrint(self.allocator, "/characters/{s}", .{name}); defer self.allocator.free(path); - return try self.fetchAndParseObject(Character, .GET, path, null, .{ self.allocator }); + return try self.fetchOptionalObject( + APIError, + null, + Character, + Character.parse, .{ self.allocator }, + .{ .method = .GET, .path = path } + ); } pub fn listMyCharacters(self: *ArtifactsAPI) !CharacterList { const path = try std.fmt.allocPrint(self.allocator, "/my/characters", .{}); defer self.allocator.free(path); - const result = try self.fetch(.GET, path, null); + const result = try self.fetch(.{ .method = .GET, .path = path }); defer result.deinit(); if (result.status != .ok) { - return APIErrors.RequestFailed; + return APIError.RequestFailed; } if (result.body == null) { - return APIErrors.ParseFailed; + return APIError.ParseFailed; } - const body = json_utils.asArray(result.body.?) orelse return APIErrors.ParseFailed; + const body = json_utils.asArray(result.body.?) orelse return APIError.ParseFailed; var characters = try std.ArrayList(Character).initCapacity(self.allocator, body.items.len); errdefer { @@ -500,8 +716,8 @@ pub fn listMyCharacters(self: *ArtifactsAPI) !CharacterList { } for (body.items) |character_json| { - const character_obj = json_utils.asObject(character_json) orelse return APIErrors.ParseFailed; - const char = Character.parse(character_obj, self.allocator, self) catch return APIErrors.ParseFailed; + const character_obj = json_utils.asObject(character_json) orelse return APIError.ParseFailed; + const char = Character.parse(character_obj, self.allocator, self) catch return APIError.ParseFailed; characters.appendAssumeCapacity(char); } @@ -512,48 +728,128 @@ pub fn listMyCharacters(self: *ArtifactsAPI) !CharacterList { }; } -pub fn actionFight(self: *ArtifactsAPI, name: []const u8) !FightResult { +pub fn actionFight(self: *ArtifactsAPI, name: []const u8) FightError!FightResult { const path = try std.fmt.allocPrint(self.allocator, "/my/{s}/action/fight", .{name}); defer self.allocator.free(path); - return try self.fetchAndParseObject(FightResult, .POST, path, null, .{ }); + return try self.fetchObject( + FightError, + FightResult.parseError, + FightResult, + FightResult.parse, .{ }, + .{ .method = .POST, .path = path } + ); } -pub fn actionGathering(self: *ArtifactsAPI, name: []const u8) !GatheringResult { +pub fn actionGathering(self: *ArtifactsAPI, name: []const u8) GatheringError!GatheringResult { const path = try std.fmt.allocPrint(self.allocator, "/my/{s}/action/gathering", .{name}); defer self.allocator.free(path); - return try self.fetchAndParseObject(GatheringResult, .POST, path, null, .{ }); + return try self.fetchObject( + GatheringError, + GatheringResult.parseError, + GatheringResult, + GatheringResult.parse, .{ }, + .{ .method = .POST, .path = path } + ); } -pub fn actionMove(self: *ArtifactsAPI, name: []const u8, x: i64, y: i64) !MoveResult { +pub fn actionMove(self: *ArtifactsAPI, name: []const u8, x: i64, y: i64) MoveError!MoveResult { const path = try std.fmt.allocPrint(self.allocator, "/my/{s}/action/move", .{name}); defer self.allocator.free(path); const payload = try std.fmt.allocPrint(self.allocator, "{{ \"x\":{},\"y\":{} }}", .{x, y}); defer self.allocator.free(payload); - return try self.fetchAndParseObject(MoveResult, .POST, path, payload, .{}); + return try self.fetchObject( + MoveError, + MoveResult.parseError, + MoveResult, + MoveResult.parse, .{ }, + .{ .method = .POST, .path = path, .payload = payload } + ); } -pub fn actionBankDepositGold(self: *ArtifactsAPI, name: []const u8, quantity: u64) !GoldTransactionResult { +pub fn actionBankDepositGold( + self: *ArtifactsAPI, + name: []const u8, + quantity: u64 +) BankDepositGoldError!GoldTransactionResult { const path = try std.fmt.allocPrint(self.allocator, "/my/{s}/action/bank/deposit/gold", .{name}); defer self.allocator.free(path); const payload = try std.fmt.allocPrint(self.allocator, "{{ \"quantity\":{} }}", .{quantity}); defer self.allocator.free(payload); - return try self.fetchAndParseObject(GoldTransactionResult, .POST, path, payload, .{}); + return try self.fetchObject( + BankDepositGoldError, + GoldTransactionResult.parseDepositError, + GoldTransactionResult, + GoldTransactionResult.parse, .{ }, + .{ .method = .POST, .path = path, .payload = payload } + ); } -pub fn actionBankDepositItem(self: *ArtifactsAPI, name: []const u8, code: []const u8, quantity: u64) !ItemTransactionResult { +pub fn actionBankDepositItem( + self: *ArtifactsAPI, + name: []const u8, + code: []const u8, + quantity: u64 +) BankDepositItemError!ItemTransactionResult { const path = try std.fmt.allocPrint(self.allocator, "/my/{s}/action/bank/deposit", .{name}); defer self.allocator.free(path); const payload = try std.fmt.allocPrint(self.allocator, "{{ \"code\":\"{s}\",\"quantity\":{} }}", .{code, quantity}); defer self.allocator.free(payload); - return try self.fetchAndParseObject(ItemTransactionResult, .POST, path, payload, .{}); + return try self.fetchObject( + BankDepositItemError, + ItemTransactionResult.parseDepositError, + ItemTransactionResult, + ItemTransactionResult.parse, .{ }, + .{ .method = .POST, .path = path, .payload = payload } + ); +} + +pub fn actionBankWithdrawGold( + self: *ArtifactsAPI, + name: []const u8, + quantity: u64 +) BankDepositGoldError!GoldTransactionResult { + const path = try std.fmt.allocPrint(self.allocator, "/my/{s}/action/bank/withdraw/gold", .{name}); + defer self.allocator.free(path); + + const payload = try std.fmt.allocPrint(self.allocator, "{{ \"quantity\":{} }}", .{quantity}); + defer self.allocator.free(payload); + + return try self.fetchObject( + BankWithdrawGoldError, + GoldTransactionResult.parseWithdrawError, + GoldTransactionResult, + GoldTransactionResult.parse, .{ }, + .{ .method = .POST, .path = path, .payload = payload } + ); +} + +pub fn actionBankWithdrawItem( + self: *ArtifactsAPI, + name: []const u8, + code: []const u8, + quantity: u64 +) BankWithdrawItemError!ItemTransactionResult { + const path = try std.fmt.allocPrint(self.allocator, "/my/{s}/action/bank/withdraw", .{name}); + defer self.allocator.free(path); + + const payload = try std.fmt.allocPrint(self.allocator, "{{ \"code\":\"{s}\",\"quantity\":{} }}", .{code, quantity}); + defer self.allocator.free(payload); + + return try self.fetchObject( + BankWithdrawItemError, + ItemTransactionResult.parseWithdrawError, + ItemTransactionResult, + ItemTransactionResult.parse, .{ }, + .{ .method = .POST, .path = path, .payload = payload } + ); } test "parse date time" {