From c35b80739c628cb7f4d9d16447192d8374d63528 Mon Sep 17 00:00:00 2001 From: Yuzu Date: Sat, 14 Dec 2024 22:25:37 -0500 Subject: [PATCH] further simplified the code and added some comments --- src/json.zig | 166 +++++++++++++++++++++++++++++++++----- src/shard.zig | 8 +- src/structures/shared.zig | 60 -------------- 3 files changed, 148 insertions(+), 86 deletions(-) diff --git a/src/json.zig b/src/json.zig index 6299763..d5526cf 100644 --- a/src/json.zig +++ b/src/json.zig @@ -433,18 +433,19 @@ pub fn jsonObject(str: []const u8, allocator: mem.Allocator) ParseResult(JsonRaw return .{ out, .{} }; }; defer allocator.free(pairs); - errdefer for (pairs) |p| { - allocator.free(p[0]); - p[1].deinit(allocator); + errdefer for (pairs) |kv| { + const k, const v = kv; + allocator.free(k); + v.deinit(allocator); }; const str4, _ = try closingCurlyBrace(str3, allocator); var obj: JsonRawHashMap = .{}; errdefer obj.deinit(allocator); - for (pairs) |entry| { - const name, const value = entry; - try obj.put(allocator, name, value); + for (pairs) |kv| { + const k, const v = kv; + try obj.put(allocator, k, v); } return .{ str4, obj }; @@ -457,23 +458,38 @@ test jsonObject { const rem, var obj = try jsonObject(data, std.testing.allocator); defer obj.deinit(std.testing.allocator); + var iterator = obj.iterator(); while (iterator.next()) |kv| { const k = kv.key_ptr.*; const v = kv.value_ptr.*; + defer std.testing.allocator.free(k); + defer v.deinit(std.testing.allocator); std.debug.print("key: {s}, value: {any} and rem: {s}\n", .{ k, v, rem }); } + try std.testing.expect(rem.len == 1); } pub const JsonType = union(enum) { + /// the identifier `null` null, + + /// either the identifier `true` or the identifier `false` bool: bool, + + /// the utf-8 string type, surrounded by backquotes string: []const u8, + /// either a float or an int /// may be casted number: JsonNumber, + + /// JsonType surrounded by brackets, separated by commas array: []JsonType, + + /// the Object type, an unmanaged array hashmap, as keeping order might be useful for when printing + /// usually a string which represents the name of the property, followed by a colon, then a JsonType, separated by commas object: JsonRawHashMap, pub fn is(self: JsonType, tag: std.meta.Tag(JsonType)) bool { @@ -491,11 +507,9 @@ pub const JsonType = union(enum) { defer @constCast(&obj).deinit(allocator); var it = obj.iterator(); while (it.next()) |entry| { - //std.debug.print("freeing {*}\n", .{entry.key_ptr}); allocator.free(entry.key_ptr.*); entry.value_ptr.*.deinit(allocator); } - //allocator.destroy(&self); }, else => {}, } @@ -504,7 +518,7 @@ pub const JsonType = union(enum) { /// entry point of the library pub const ultimateParser: Parser(JsonType) = alternation(JsonType, .{ - jsonNull, + jsonNull, // attempts to parse each type from top to bottom jsonBool, jsonString, jsonNumber, @@ -823,7 +837,7 @@ pub fn optional(comptime T: type, parser: Parser(T)) Parser(?T) { const rest, const parsed = parser(str, allocator) catch |err| return switch (err) { error.UnexpectedCharacter, error.Empty, error.NumberCastFail => .{ str, null }, error.OutOfMemory => error.OutOfMemory, - else => return err, + else => err, }; return .{ rest, parsed }; } @@ -907,7 +921,8 @@ pub const BailingAllocator = struct { /// general join /// useful for joining more than 2 parsers pub fn sequence(comptime Struct: type, comptime parsers: FieldParsers(Struct)) Parser(Struct) { - if (@typeInfo(Struct) != .@"struct") @compileError("expected a `struct` type"); + if (@typeInfo(Struct) != .@"struct") + @compileError("expected a `struct` type"); return struct { fn f(str: []const u8, allocator: std.mem.Allocator) ParseResult(Struct) { @@ -954,6 +969,36 @@ pub fn repetition(comptime T: type, parser: Parser(T)) Parser([]T) { pub const Error = std.mem.Allocator.Error || ParserError; +/// assumes that you are casting a type against its JsonType counterpart +/// +/// u8, u16, u32... -> assumes JsonNumber == .integer +/// i8, i16, i32... -> assumes JsonNumber == .integer +/// f16, f32, f64 -> assumes JsonNumber == .float +/// array or slice of u8 -> JsonType.string +/// array or slice of T -> JsonType.array +/// array of N elements -> assumes JsonType.array.len == N +/// null -> JsonType.null +/// void -> ignores JsonType +/// ?T -> JsonType +/// *T -> JsonType, which always allocates memory +/// struct{..} -> JsonType.object +/// enum(..) -> if JsonNumber, assumes JsonNumber == .integer otherwise JsonType.string +/// union -> JsonType.string | JsonType.bool | JsonType.number +/// (empty) union(enum) -> JsonType.string, the equivalent of `"a" | "b" | "c"` since values are `void` +/// tagged union -> panics, use DiscriminatedUnion(U, key) instead +/// Record(T) -> JsonType.object +/// AssociativeArray(E, V) -> JsonType.object (where keys are interpreted as enum 'E' members) +/// DiscriminatedUnion(U, key) -> JsonType.object (where a `key` that maps to `E` is present). Assumes that `U` is tagged by `E` +/// +/// All other types are unsupported. +/// (Packed structs must be parsed manually, as the default parser of a `packed struct` is mirrored to one of an enum's) +/// +/// If you must forcibly cast a type, you shall write a `json` method +/// the signature of the `json` function is as follows +/// ``` +/// pub fn json(allocator: std.mem.Allocator, value: zjson.JsonType) !T +/// ``` +/// where T is the type you must cast into pub fn parseInto(comptime T: type, allocator: mem.Allocator, value: JsonType) Error!T { switch (@typeInfo(T)) { .void => return {}, @@ -1041,6 +1086,9 @@ pub fn parseInto(comptime T: type, allocator: mem.Allocator, value: JsonType) Er if (std.meta.hasFn(T, "json")) return try T.json(allocator, value); + if (structInfo.layout == .@"packed") + return @bitCast(value.number.cast(structInfo.backing_integer.?)); // forcibly casted + if (!value.is(.object)) @panic("tried to cast a non-object into: " ++ @typeName(T)); @@ -1064,16 +1112,19 @@ pub fn parseInto(comptime T: type, allocator: mem.Allocator, value: JsonType) Er switch (value) { .string => |string| { if (arrayInfo.child != u8) return error.TypeMismatch; // attempting to cast an array of T against a string + std.debug.assert(arrayInfo.len == string.len); + var r: T = undefined; - var i: usize = 0; - while (i < arrayInfo.len) : (i += 1) + comptime var i: usize = 0; + inline while (i < arrayInfo.len) : (i += 1) r[i] = try parseInto(arrayInfo.child, allocator, string[i]); return r; }, .array => |array| { + std.debug.assert(arrayInfo.len == array.len); var r: T = undefined; - var i: usize = 0; - while (i < arrayInfo.len) : (i += 1) + comptime var i: usize = 0; + inline while (i < arrayInfo.len) : (i += 1) r[i] = try parseInto(arrayInfo.child, allocator, array[i]); return r; }, @@ -1174,14 +1225,14 @@ pub fn OwnedEither(comptime L: type, comptime R: type) type { }; } -/// parse any string containing a JSON object root `{...}` -/// casts the value into `T` +/// parse any string containing either a JSON object root `{...}` or a JSON array `[...]` +/// casts the value into `T`, read `parseInto` for detailed instructions pub fn parse(comptime T: type, child_allocator: mem.Allocator, data: []const u8) ParserError!Owned(T) { var owned: Owned(T) = .{ .arena = try child_allocator.create(std.heap.ArenaAllocator), .value = undefined, }; - owned.arena.* = std.heap.ArenaAllocator.init(child_allocator); + owned.arena.* = .init(child_allocator); const allocator = owned.arena.allocator(); const value = try ultimateParserAssert(data, allocator); owned.value = try parseInto(T, allocator, value); @@ -1196,7 +1247,7 @@ pub fn parseLeft(comptime L: type, comptime R: type, child_allocator: mem.Alloca .arena = try child_allocator.create(std.heap.ArenaAllocator), .value = undefined, }; - owned.arena.* = std.heap.ArenaAllocator.init(child_allocator); + owned.arena.* = .init(child_allocator); const allocator = owned.arena.allocator(); const value = try ultimateParserAssert(data, allocator); owned.value = .{ .left = try parseInto(L, allocator, value) }; @@ -1211,7 +1262,7 @@ pub fn parseRight(comptime L: type, comptime R: type, child_allocator: mem.Alloc .arena = try child_allocator.create(std.heap.ArenaAllocator), .value = undefined, }; - owned.arena.* = std.heap.ArenaAllocator.init(child_allocator); + owned.arena.* = .init(child_allocator); const allocator = owned.arena.allocator(); const value = try ultimateParserAssert(data, allocator); owned.value = .{ .right = try parseInto(R, allocator, value) }; @@ -1291,3 +1342,78 @@ pub fn AssociativeArray(comptime E: type, comptime V: type) type { } }; } + +/// assumes object.value[key] is of type `E` and the result thereof maps to the tagged union value +/// assumes `key` is a field present in all members of type `U` +/// eg: `type` is a property of type E and E.User maps to a User struct, whereas E.Admin maps to an Admin struct +pub fn DiscriminatedUnion(comptime U: type, comptime key: []const u8) type { + if (@typeInfo(U) != .@"union") + @compileError("may only cast a union"); + + if (@typeInfo(U).@"union".tag_type == null) + @compileError("cannot cast untagged union"); + + const E = comptime @typeInfo(U).@"union".tag_type.?; + + if (@typeInfo(U).@"union".tag_type.? != E) + @compileError("enum tag type of union(" ++ @typeName(U.@"union".tag_type) ++ ") doesn't match " ++ @typeName(E)); + + return struct { + t: U, + pub fn json(allocator: mem.Allocator, value: JsonType) !@This() { + if (!value.is(.object)) + @panic("coulnd't match against non-object type"); + + const discriminator = value.object.get(key) orelse + @panic("couldn't find property " ++ key ++ "in raw object"); + + var u: U = undefined; + + const tag = discriminator.number.cast(@typeInfo(E).@"enum".tag_type); + + inline for (@typeInfo(E).@"enum".fields) |field| { + if (field.value == tag) { + const T = comptime std.meta.fields(U)[field.value].type; + comptime std.debug.assert(@hasField(T, key)); + u = @unionInit(U, field.name, try parseInto(T, allocator, value)); + } + } + + return .{ .t = u }; + } + }; +} + +test DiscriminatedUnion { + // pretty simple stuff, in a real app you'd have thousands of these + const AccountTypes = enum { user, admin }; + + const User = struct { + type: AccountTypes, + id: u64, + name: []const u8, + }; + + const Admin = struct { + type: AccountTypes, + id: u64, + name: []const u8, + admin: bool, + }; + + const Account = union(AccountTypes) { + user: User, + admin: Admin, + }; + + const payload = + \\ {"type": 1, "id": 100000, "name": "Mathew", "admin": true} + ; + + const data = try parse(DiscriminatedUnion(Account, "type"), std.testing.allocator, payload); + defer data.deinit(); + + try std.testing.expect(data.value.t == .admin); + try std.testing.expect(data.value.t.admin.type == .admin); + // we don't care otherwise about the data +} diff --git a/src/shard.zig b/src/shard.zig index ee8cfe2..98898d6 100644 --- a/src/shard.zig +++ b/src/shard.zig @@ -47,7 +47,7 @@ const GatewayPayload = Types.GatewayPayload; const Opcode = Types.GatewayOpcodes; const Intents = Types.Intents; -const Snowflake = @import("./structures/snowflake.zig").Snowflake; +const Snowflake = Types.Snowflake; const FetchReq = @import("http.zig").FetchReq; const MakeRequestError = @import("http.zig").MakeRequestError; const Partial = Types.Partial; @@ -190,11 +190,7 @@ inline fn calculateSafeRequests(options: RatelimitOptions) usize { @as(f64, @floatFromInt(options.max_requests_per_ratelimit_tick orelse 120)) - @ceil(@as(f64, @floatFromInt(options.ratelimit_reset_interval)) / 30000.0) * 2; - if (safe_requests < 0) { - return 0; - } - - return @intFromFloat(safe_requests); + return if (safe_requests < 0) 0 else @intFromFloat(safe_requests); } inline fn _connect_ws(allocator: mem.Allocator, url: []const u8) !ws.Client { diff --git a/src/structures/shared.zig b/src/structures/shared.zig index 7f5b6c3..63f372f 100644 --- a/src/structures/shared.zig +++ b/src/structures/shared.zig @@ -16,7 +16,6 @@ const std = @import("std"); const Snowflake = @import("snowflake.zig").Snowflake; -const zjson = @import("../json.zig"); pub const PresenceStatus = enum { online, @@ -43,10 +42,6 @@ pub const UserFlags = packed struct { return @bitCast(raw); } - pub fn json(_: std.mem.Allocator, value: zjson.JsonType) !@This() { - return @bitCast(value.number.cast(u34)); - } - DiscordEmployee: bool = false, PartneredServerOwner: bool = false, HypeSquadEventsMember: bool = false, @@ -83,10 +78,6 @@ pub const PremiumUsageFlags = packed struct { return @bitCast(raw); } - pub fn json(_: std.mem.Allocator, value: zjson.JsonType) !@This() { - return @bitCast(value.number.cast(u8)); - } - PremiumDiscriminator: bool = false, AnimatedAvatar: bool = false, ProfileBanner: bool = false, @@ -102,10 +93,6 @@ pub const PurchasedFlags = packed struct { return @bitCast(raw); } - pub fn json(_: std.mem.Allocator, value: zjson.JsonType) !@This() { - return @bitCast(value.number.cast(u8)); - } - NitroClassic: bool = false, Nitro: bool = false, GuildBoost: bool = false, @@ -122,10 +109,6 @@ pub const MemberFlags = packed struct { return @bitCast(raw); } - pub fn json(_: std.mem.Allocator, value: zjson.JsonType) !@This() { - return @bitCast(value.number.cast(u16)); - } - /// /// Member has left and rejoined the guild /// @@ -198,10 +181,6 @@ pub const ChannelFlags = packed struct { return @bitCast(raw); } - pub fn json(_: std.mem.Allocator, value: zjson.JsonType) !@This() { - return @bitCast(value.number.cast(u32)); - } - None: bool = false, /// this thread is pinned to the top of its parent `GUILD_FORUM` channel Pinned: bool = false, @@ -224,10 +203,6 @@ pub const RoleFlags = packed struct { return @bitCast(raw); } - pub fn json(_: std.mem.Allocator, value: zjson.JsonType) !@This() { - return @bitCast(value.number.cast(u2)); - } - None: bool = false, /// Role can be selected by members in an onboarding prompt InPrompt: bool = false, @@ -242,10 +217,6 @@ pub const AttachmentFlags = packed struct { return @bitCast(raw); } - pub fn json(_: std.mem.Allocator, value: zjson.JsonType) !@This() { - return @bitCast(value.number.cast(u8)); - } - None: bool = false, _pad: u1 = 0, /// This attachment has been edited using the remix feature on mobile @@ -263,10 +234,6 @@ pub const SkuFlags = packed struct { return @bitCast(raw); } - pub fn json(_: std.mem.Allocator, value: zjson.JsonType) !@This() { - return @bitCast(value.number.cast(u16)); - } - _pad: u2 = 0, /// SKU is available for purchase Available: bool = false, @@ -288,10 +255,6 @@ pub const MessageFlags = packed struct { return @bitCast(raw); } - pub fn json(_: std.mem.Allocator, value: zjson.JsonType) !@This() { - return @bitCast(value.number.cast(u16)); - } - /// This message has been published to subscribed channels (via Channel Following) Crossposted: bool = false, /// This message originated from a message in another channel (via Channel Following) @@ -328,10 +291,6 @@ pub const ActivityFlags = packed struct { return @bitCast(raw); } - pub fn json(_: std.mem.Allocator, value: zjson.JsonType) !@This() { - return @bitCast(value.number.cast(u16)); - } - Instance: bool = false, Join: bool = false, Spectate: bool = false, @@ -366,10 +325,6 @@ pub const ApplicationFlags = packed struct { return @bitCast(raw); } - pub fn json(_: std.mem.Allocator, value: zjson.JsonType) !@This() { - return @bitCast(value.number.cast(u32)); - } - _pad: u5 = 0, /// Indicates if an app uses the Auto Moderation API. ApplicationAutoModerationRuleCreateBadge: bool = false, @@ -577,10 +532,6 @@ pub const SystemChannelFlags = packed struct { return @bitCast(raw); } - pub fn json(_: std.mem.Allocator, value: zjson.JsonType) !@This() { - return @bitCast(value.number.cast(u8)); - } - /// Suppress member join notifications SuppressJoinNotifications: bool = false, /// Suppress server boost notifications @@ -622,10 +573,6 @@ pub const ChannelTypes = packed struct { return @bitCast(raw); } - pub fn json(_: std.mem.Allocator, value: zjson.JsonType) !@This() { - return @bitCast(value.number.cast(u32)); - } - /// A text channel within a server GuildText: bool = false, /// A direct message between users @@ -949,10 +896,6 @@ pub const BitwisePermissionFlags = packed struct { return @bitCast(raw); } - pub fn json(_: std.mem.Allocator, value: zjson.JsonType) !@This() { - return @bitCast(value.number.cast(u64)); - } - /// Allows creation of instant invites CREATE_INSTANT_INVITE: bool = false, /// Allows kicking members @@ -1296,9 +1239,6 @@ pub const GatewayIntents = packed struct { return @bitCast(raw); } - pub fn json(_: std.mem.Allocator, value: zjson.JsonType) !@This() { - return @bitCast(value.number.cast(u32)); - } /// /// - GUILD_CREATE /// - GUILD_UPDATE