diff options
author | Christian Segundo | 2023-06-15 19:54:16 +0200 |
---|---|---|
committer | Christian Segundo | 2023-06-15 19:54:16 +0200 |
commit | db5d7914d82d69021cf303e0ab02b46e0730bf48 (patch) | |
tree | 0b515caee3c355f02e3a1eb5df15784b4a62179f | |
parent | 17f3a7b655938eb47efc2bbe884e68786bce6077 (diff) | |
download | zmission-db5d7914d82d69021cf303e0ab02b46e0730bf48.tar.gz |
one step closer
-rw-r--r-- | src/main.zig | 31 | ||||
-rw-r--r-- | src/request.zig | 421 | ||||
-rw-r--r-- | src/transmission.zig | 18 | ||||
-rw-r--r-- | src/util.zig | 119 | ||||
-rw-r--r-- | src/util/enum.zig | 56 | ||||
-rw-r--r-- | src/util/struct.zig | 85 |
6 files changed, 480 insertions, 250 deletions
diff --git a/src/main.zig b/src/main.zig index 074be58..bdffa31 100644 --- a/src/main.zig +++ b/src/main.zig @@ -32,6 +32,15 @@ export fn add(a: i32, b: i32) i32 { std.debug.print("body: {s}\n", .{body}); } + { + const body = transmission.session_set_raw(&client, .{ .peer_port = 51413 }) catch |err| { + std.debug.print("error: {any}\n", .{err}); + unreachable; + }; + defer allocator.free(body); + std.debug.print("body: {s}\n", .{body}); + } + return a + b; } @@ -67,17 +76,17 @@ export fn c_session_get(client: ?*anyopaque, buf: [*]u8) c_int { return 0; } +//test "c api" { +//const clientOptions = transmission.ClientOptions{ +//.host = "192.168.0.2", +//.port = 9091, +//.https = false, +//}; +//var foo = c_client_init(clientOptions); +//_ = c_session_get(foo, undefined); +//c_client_deinit(foo); +//} + test "basic add functionality" { try testing.expect(add(3, 7) == 10); } - -test "c api" { - const clientOptions = transmission.ClientOptions{ - .host = "192.168.0.2", - .port = 9091, - .https = false, - }; - var foo = c_client_init(clientOptions); - _ = c_session_get(foo, undefined); - c_client_deinit(foo); -} diff --git a/src/request.zig b/src/request.zig index 8641776..2633428 100644 --- a/src/request.zig +++ b/src/request.zig @@ -45,21 +45,204 @@ const Method = enum { const Self = @This(); - // Transmission RPC uses hyphens instead of underscores, this creates a - // table of method names using the same enum field names but with - // underscores. - pub const Fields = util.enumFieldsToStringSlice(Self); + pub const json_map = util.JsonMap(Self, util.replaceUnderscores); - pub fn str(self: Self) []const u8 { - return Fields[@enumToInt(self)]; + pub fn jsonStringify(self: Self, options: std.json.StringifyOptions, out_stream: anytype) !void { + try util.jsonStringify(self, options, out_stream); } +}; + +pub const TorrentIDs = union(enum) { + single: usize, + many: []const usize, + recently_active, + + const Self = @This(); pub fn jsonStringify(self: Self, options: std.json.StringifyOptions, out_stream: anytype) !void { - try std.json.stringify(self.str(), options, out_stream); + switch (self) { + .single => |v| try std.json.stringify(v, options, out_stream), + .many => |v| try std.json.stringify(v, options, out_stream), + .recently_active => try std.json.stringify("recently-active", options, out_stream), + } } }; -pub const SessionSetFields = struct { +// all equal just to we can switch on them at the same time +pub const TorrentStart = struct { ids: TorrentIDs }; +pub const TorrentStartNow = TorrentStart; +pub const TorrentStop = TorrentStart; +pub const TorrentVerify = TorrentStart; +pub const TorrentReannounce = TorrentStart; + +pub const TorrentGet = struct { + pub const Fields = enum { + activityDate, + addedDate, + availability, + bandwidthPriority, + comment, + corruptEver, + creator, + dateCreated, + desiredAvailable, + doneDate, + downloadDir, + downloadedEver, + downloadLimit, + downloadLimited, + editDate, + @"error", + errorString, + eta, + etaIdle, + file_count, + files, + fileStats, + group, + hashString, + haveUnchecked, + haveValid, + honorsSessionLimits, + id, + isFinished, + isPrivate, + isStalled, + labels, + leftUntilDone, + magnetLink, + manualAnnounceTime, + maxConnectedPeers, + metadataPercentComplete, + name, + peer_limit, + peers, + peersConnected, + peersFrom, + peersGettingFromUs, + peersSendingToUs, + percentComplete, + percentDone, + pieces, + pieceCount, + pieceSize, + priorities, + primary_mime_type, + queuePosition, + rateDownload, + rateUpload, + recheckProgress, + secondsDownloading, + secondsSeeding, + seedIdleLimit, + seedIdleMode, + seedRatioLimit, + seedRatioMode, + sequentialDownload, + sizeWhenDone, + startDate, + status, + trackers, + trackerList, + trackerStats, + totalSize, + torrentFile, + uploadedEver, + uploadLimit, + uploadLimited, + uploadRatio, + wanted, + webseeds, + webseedsSendingToUs, + + const Self = @This(); + + pub const json_map = util.JsonMap(Self, util.replaceUnderscores); + + pub fn jsonStringify(self: Self, options: std.json.StringifyOptions, out_stream: anytype) !void { + try util.jsonStringify(self, options, out_stream); + } + }; + + pub const all_fields = util.enumFieldsSlice(@This().Fields); + + ids: ?TorrentIDs = null, + fields: []const Fields, +}; + +pub const SessionGet = struct { + pub const Fields = enum { + alt_speed_down, + alt_speed_enabled, + alt_speed_time_begin, + alt_speed_time_day, + alt_speed_time_enabled, + alt_speed_time_end, + alt_speed_up, + blocklist_enabled, + blocklist_size, + blocklist_url, + cache_size_mb, + config_dir, + default_trackers, + dht_enabled, + download_dir, + download_dir_free_space, + download_queue_enabled, + download_queue_size, + encryption, + idle_seeding_limit_enabled, + idle_seeding_limit, + incomplete_dir_enabled, + incomplete_dir, + lpd_enabled, + peer_limit_global, + peer_limit_per_torrent, + peer_port_random_on_start, + peer_port, + pex_enabled, + port_forwarding_enabled, + queue_stalled_enabled, + queue_stalled_minutes, + rename_partial_files, + rpc_version_minimum, + rpc_version_semver, + rpc_version, + script_torrent_added_enabled, + script_torrent_added_filename, + script_torrent_done_enabled, + script_torrent_done_filename, + script_torrent_done_seeding_enabled, + script_torrent_done_seeding_filename, + seed_queue_enabled, + seed_queue_size, + seedRatioLimit, + seedRatioLimited, + speed_limit_down_enabled, + speed_limit_down, + speed_limit_up_enabled, + speed_limit_up, + start_added_torrents, + trash_original_torrent_files, + units, + utp_enabled, + version, + + const Self = @This(); + + pub const json_map = util.JsonMap(Self, util.replaceUnderscores); + + pub fn jsonStringify(self: Self, options: std.json.StringifyOptions, out_stream: anytype) !void { + try util.jsonStringify(self, options, out_stream); + } + }; + + pub const all_fields = util.enumFieldsSlice(@This().Fields); + + fields: []const Fields, +}; + +pub const SessionSet = struct { alt_speed_down: ?usize = null, alt_speed_enabled: ?bool = null, alt_speed_time_begin: ?usize = null, @@ -83,14 +266,10 @@ pub const SessionSetFields = struct { const Self = @This(); - pub const Fields = util.enumFieldsToStringSlice(Self); - - pub fn str(self: Self) []const u8 { - return Fields[@enumToInt(self)]; - } + pub const json_map = util.JsonMap(Self, util.replaceUnderscores); pub fn jsonStringify(self: Self, options: std.json.StringifyOptions, out_stream: anytype) !void { - try std.json.stringify(self.str(), options, out_stream); + try util.jsonStringify(self, options, out_stream); } } = null, idle_seeding_limit_enabled: ?bool = null, @@ -124,211 +303,14 @@ pub const SessionSetFields = struct { start_added_torrents: ?bool = null, trash_original_torrent_files: ?bool = null, utp_enabled: ?bool = null, -}; -pub const TorrentGetFields = enum { - activityDate, - addedDate, - availability, - bandwidthPriority, - comment, - corruptEver, - creator, - dateCreated, - desiredAvailable, - doneDate, - downloadDir, - downloadedEver, - downloadLimit, - downloadLimited, - editDate, - @"error", - errorString, - eta, - etaIdle, - file_count, - files, - fileStats, - group, - hashString, - haveUnchecked, - haveValid, - honorsSessionLimits, - id, - isFinished, - isPrivate, - isStalled, - labels, - leftUntilDone, - magnetLink, - manualAnnounceTime, - maxConnectedPeers, - metadataPercentComplete, - name, - peer_limit, - peers, - peersConnected, - peersFrom, - peersGettingFromUs, - peersSendingToUs, - percentComplete, - percentDone, - pieces, - pieceCount, - pieceSize, - priorities, - primary_mime_type, - queuePosition, - rateDownload, - rateUpload, - recheckProgress, - secondsDownloading, - secondsSeeding, - seedIdleLimit, - seedIdleMode, - seedRatioLimit, - seedRatioMode, - sequentialDownload, - sizeWhenDone, - startDate, - status, - trackers, - trackerList, - trackerStats, - totalSize, - torrentFile, - uploadedEver, - uploadLimit, - uploadLimited, - uploadRatio, - wanted, - webseeds, - webseedsSendingToUs, + pub const json_map = util.JsonMap(SessionSet, util.replaceUnderscores); - const Self = @This(); - - pub const Fields = util.enumFieldsToStringSlice(Self); - - pub fn str(self: Self) []const u8 { - return Fields[@enumToInt(self)]; - } - - // We have to add our own custom stringification because the default - // one will nest the enum inside the method name. - pub fn jsonStringify(self: Self, options: std.json.StringifyOptions, out_stream: anytype) !void { - try std.json.stringify(self.str(), options, out_stream); - } -}; - -pub const SessionGetFields = enum { - alt_speed_down, - alt_speed_enabled, - alt_speed_time_begin, - alt_speed_time_day, - alt_speed_time_enabled, - alt_speed_time_end, - alt_speed_up, - blocklist_enabled, - blocklist_size, - blocklist_url, - cache_size_mb, - config_dir, - default_trackers, - dht_enabled, - download_dir, - download_dir_free_space, - download_queue_enabled, - download_queue_size, - encryption, - idle_seeding_limit_enabled, - idle_seeding_limit, - incomplete_dir_enabled, - incomplete_dir, - lpd_enabled, - peer_limit_global, - peer_limit_per_torrent, - peer_port_random_on_start, - peer_port, - pex_enabled, - port_forwarding_enabled, - queue_stalled_enabled, - queue_stalled_minutes, - rename_partial_files, - rpc_version_minimum, - rpc_version_semver, - rpc_version, - script_torrent_added_enabled, - script_torrent_added_filename, - script_torrent_done_enabled, - script_torrent_done_filename, - script_torrent_done_seeding_enabled, - script_torrent_done_seeding_filename, - seed_queue_enabled, - seed_queue_size, - seedRatioLimit, - seedRatioLimited, - speed_limit_down_enabled, - speed_limit_down, - speed_limit_up_enabled, - speed_limit_up, - start_added_torrents, - trash_original_torrent_files, - units, - utp_enabled, - version, - - const Self = @This(); - - pub const Fields = util.enumFieldsToStringSlice(Self); - - pub fn str(self: Self) []const u8 { - return Fields[@enumToInt(self)]; - } - - // We have to add our own custom stringification because the default - // one will nest the enum inside the method name. - pub fn jsonStringify(self: Self, options: std.json.StringifyOptions, out_stream: anytype) !void { - try std.json.stringify(self.str(), options, out_stream); + pub fn jsonStringify(self: SessionSet, options: std.json.StringifyOptions, out_stream: anytype) !void { + try util.jsonStringify(self, options, out_stream); } }; -pub const TorrentIDs = union(enum) { - single: usize, - many: []const usize, - recently_active, - - const Self = @This(); - - pub fn jsonStringify(self: Self, options: std.json.StringifyOptions, out_stream: anytype) !void { - switch (self) { - .single => |v| try std.json.stringify(v, options, out_stream), - .many => |v| try std.json.stringify(v, options, out_stream), - .recently_active => try std.json.stringify("recently-active", options, out_stream), - } - } -}; - -pub const TorrentActionFields = struct { - ids: TorrentIDs, -}; - -pub const TorrentStart = TorrentActionFields; -pub const TorrentStartNow = TorrentActionFields; -pub const TorrentStop = TorrentActionFields; -pub const TorrentVerify = TorrentActionFields; -pub const TorrentReannounce = TorrentActionFields; - -pub const TorrentGet = struct { - ids: ?TorrentIDs = null, - fields: []const TorrentGetFields, -}; - -pub const SessionGet = struct { - fields: []const SessionGetFields, -}; - -pub const SessionSet = SessionSetFields; - pub const Request = struct { method: Method, arguments: union(Method) { @@ -385,7 +367,10 @@ test "json request encoding" { defer req.deinit(); try std.json.stringify(self.request, .{}, req.writer()); std.testing.expect(std.mem.eql(u8, req.items, self.expected)) catch { - std.debug.panic("{s}\n", .{req.items}); + std.debug.print("name: {s}\n", .{self.name}); + std.debug.print("expected: \t{s}\n", .{self.expected}); + std.debug.print("got: \t\t{s}\n", .{req.items}); + return error.InvalidOutput; }; } }; @@ -397,7 +382,7 @@ test "json request encoding" { .method = .session_get, .arguments = .{ .session_get = .{ - .fields = &[_]SessionGetFields{ + .fields = &[_]SessionGet.Fields{ .version, .utp_enabled, }, @@ -421,7 +406,7 @@ test "json request encoding" { }, }, .expected = - \\{"method":"session-set","arguments":{"encryption":"required","lpd_enabled":true}} + \\{"method":"session-set","arguments":{"encryption":"required","lpd-enabled":true}} , }, @@ -476,7 +461,7 @@ test "json request encoding" { .method = .torrent_get, .arguments = .{ .torrent_get = .{ - .fields = &[_]TorrentGetFields{ + .fields = &[_]TorrentGet.Fields{ .id, .name, }, diff --git a/src/transmission.zig b/src/transmission.zig index e25d30b..dd058bb 100644 --- a/src/transmission.zig +++ b/src/transmission.zig @@ -2,10 +2,9 @@ const std = @import("std"); const util = @import("util.zig"); const Request = @import("request.zig").Request; -const SessionGetFields = @import("request.zig").SessionGetFields; const SessionGet = @import("request.zig").SessionGet; +const SessionSet = @import("request.zig").SessionSet; const TorrentGet = @import("request.zig").TorrentGet; -const TorrentGetFields = @import("request.zig").TorrentGetFields; pub const ClientOptions = extern struct { host: [*:0]const u8, @@ -83,6 +82,8 @@ pub const Client = struct { return error.InvalidSize; } + std.debug.print("request: {s}\n", .{payload.items}); + try real_req.finish(); try real_req.wait(); @@ -103,7 +104,7 @@ pub const Client = struct { pub fn session_get_raw(client: *Client, session_get: ?SessionGet) ![]u8 { const default: SessionGet = .{ - .fields = comptime util.enumFieldsToSlice(SessionGetFields), + .fields = SessionGet.all_fields, }; const r = Request{ @@ -116,7 +117,7 @@ pub fn session_get_raw(client: *Client, session_get: ?SessionGet) ![]u8 { pub fn torrent_get_raw(client: *Client, torrent_get: ?TorrentGet) ![]u8 { const default: TorrentGet = .{ - .fields = comptime util.enumFieldsToSlice(TorrentGetFields), + .fields = TorrentGet.all_fields, }; const r = Request{ @@ -126,3 +127,12 @@ pub fn torrent_get_raw(client: *Client, torrent_get: ?TorrentGet) ![]u8 { const body = try client.do(r); return body; } + +pub fn session_set_raw(client: *Client, session_set: SessionSet) ![]u8 { + const r = Request{ + .method = .session_set, + .arguments = .{ .session_set = session_set }, + }; + const body = try client.do(r); + return body; +} diff --git a/src/util.zig b/src/util.zig index 41c3584..c46dbe7 100644 --- a/src/util.zig +++ b/src/util.zig @@ -1,23 +1,108 @@ const std = @import("std"); -/// Returns an slice of all the fields in the given type replacing all '_' with '-'. -pub fn enumFieldsToStringSlice(comptime E: type) []const []const u8 { - @setEvalBranchQuota(10000); - var names: []const []const u8 = &[_][]const u8{}; - for (std.meta.fields(E)) |field| { - var name: [field.name.len]u8 = undefined; - _ = std.mem.replace(u8, field.name, &[_]u8{'_'}, &[_]u8{'-'}, &name); - names = names ++ &[_][]const u8{&name}; - } - return names; +const struct_util = @import("util/struct.zig"); +const enum_util = @import("util/enum.zig"); +pub const enumFieldsSlice = enum_util.enumFieldsSlice; + +// Builds a static map of fields in `T` using `F`. +// to be used by our stringify +pub fn JsonMap( + comptime T: type, + comptime F: ?fn (comptime []const u8) []const u8, +) type { + return blk: { + switch (@typeInfo(T)) { + .Enum => { + break :blk struct { + const fields = enum_util.enumFieldsStringSlice(T, F); + pub fn get(field: T) []const u8 { + return fields[@enumToInt(field)]; + } + }; + }, + .Struct => { + break :blk struct { + const map = struct_util.StructFieldsMap( + T, + struct_util.StructFieldsEnum(T, F), + ); + pub fn get(field: []const u8) []const u8 { + return @tagName(map.get(field).?); + } + }; + }, + else => unreachable, + } + }; } -pub fn enumFieldsToSlice(comptime T: type) []const T { - var fields: []const T = &[_]T{}; - inline for (@typeInfo(T).Enum.fields) |enumField| { - fields = fields ++ &[_]T{ - @field(T, enumField.name), - }; +// copy of std.json.stringify to change field names +// using JsonMap when serializing +pub fn jsonStringify( + value: anytype, + options: std.json.StringifyOptions, + out_stream: anytype, +) !void { + const T = @TypeOf(value); + switch (@typeInfo(T)) { + .Enum => { + const member_name = T.json_map.get(value); + try std.json.stringify(member_name, options, out_stream); + }, + .Struct => |S| { + try out_stream.writeByte(if (S.is_tuple) '[' else '{'); + var field_output = false; + var child_options = options; + child_options.whitespace.indent_level += 1; + inline for (S.fields) |Field| { + // don't include void fields + if (Field.type == void) continue; + + var emit_field = true; + + // don't include optional fields that are null when emit_null_optional_fields is set to false + if (@typeInfo(Field.type) == .Optional) { + if (options.emit_null_optional_fields == false) { + if (@field(value, Field.name) == null) { + emit_field = false; + } + } + } + + if (emit_field) { + if (!field_output) { + field_output = true; + } else { + try out_stream.writeByte(','); + } + try child_options.whitespace.outputIndent(out_stream); + if (!S.is_tuple) { + try std.json.encodeJsonString( + T.json_map.get(Field.name), + options, + out_stream, + ); + try out_stream.writeByte(':'); + if (child_options.whitespace.separator) { + try out_stream.writeByte(' '); + } + } + try std.json.stringify(@field(value, Field.name), child_options, out_stream); + } + } + if (field_output) { + try options.whitespace.outputIndent(out_stream); + } + try out_stream.writeByte(if (S.is_tuple) ']' else '}'); + return; + }, + else => unreachable, } - return fields; +} + +pub fn replaceUnderscores(comptime o: []const u8) []const u8 { + @setEvalBranchQuota(10000); + var name: [o.len]u8 = undefined; + _ = std.mem.replace(u8, o, &[_]u8{'_'}, &[_]u8{'-'}, &name); + return &name; } diff --git a/src/util/enum.zig b/src/util/enum.zig new file mode 100644 index 0000000..216d04e --- /dev/null +++ b/src/util/enum.zig @@ -0,0 +1,56 @@ +const std = @import("std"); + +pub fn enumFieldsSlice(comptime T: type) []const T { + var fields: []const T = &[_]T{}; + inline for (@typeInfo(T).Enum.fields) |enumField| { + fields = fields ++ &[_]T{ + @field(T, enumField.name), + }; + } + return fields; +} + +/// Creates an slice of slices with with fields in the given enum. +/// Runs `F` for each field in `T` to modify the names in the resulting slice. +pub fn enumFieldsStringSlice( + comptime T: type, + comptime F: ?fn (comptime []const u8) []const u8, +) []const []const u8 { + @setEvalBranchQuota(10000); + var names: []const []const u8 = &[_][]const u8{}; + for (std.meta.fields(T)) |field| { + const name = blk: { + if (F) |f| break :blk f(field.name); + break :blk field.name; + }; + names = names ++ &[_][]const u8{name}; + } + return names; +} + +const TestEnum = enum { + A_A, + B_B, +}; + +fn replaceUnderscores(comptime o: []const u8) []const u8 { + var name: [o.len]u8 = undefined; + _ = std.mem.replace(u8, o, &[_]u8{'_'}, &[_]u8{'-'}, &name); + return &name; +} + +test "enumFieldsSlice" { + const fields = comptime enumFieldsSlice(TestEnum); + try std.testing.expect(fields.len == 2); + try std.testing.expect(fields[@enumToInt(TestEnum.A_A)] == TestEnum.A_A); + try std.testing.expect(fields[@enumToInt(TestEnum.B_B)] == TestEnum.B_B); +} + +test "enumFieldsStringSlice" { + const fields = comptime enumFieldsStringSlice(TestEnum, replaceUnderscores); + const a = TestEnum.A_A; + const b = TestEnum.B_B; + try std.testing.expect(fields.len == 2); + try std.testing.expect(std.mem.eql(u8, fields[@enumToInt(a)], "A-A")); + try std.testing.expect(std.mem.eql(u8, fields[@enumToInt(b)], "B-B")); +} diff --git a/src/util/struct.zig b/src/util/struct.zig new file mode 100644 index 0000000..db41b09 --- /dev/null +++ b/src/util/struct.zig @@ -0,0 +1,85 @@ +const std = @import("std"); + +/// Maps each field of a struct `S` to the enum value in `E` +pub fn StructFieldsMap( + comptime S: type, + comptime E: type, +) type { + const KV = struct { []const u8, E }; + var slice: []const KV = &[_]KV{}; + var idx: usize = 0; + for (std.meta.fields(S)) |field| { + slice = slice ++ &[_]KV{.{ field.name, @intToEnum(E, idx) }}; + idx += 1; + } + return std.ComptimeStringMap(E, slice); +} + +/// Creates an enum with fields matching each field in struct `S`. +/// Runs `F` for each field in `S` to modify the names in the resulting enum. +// sort of opposite to the included EnumFieldStruct in std +pub fn StructFieldsEnum( + comptime S: type, + comptime F: ?fn (comptime []const u8) []const u8, +) type { + const EnumField = std.builtin.Type.EnumField; + var fields: []const EnumField = &[_]EnumField{}; + var idx: usize = 0; + for (std.meta.fields(S)) |field| { + fields = fields ++ &[_]EnumField{.{ + .name = blk: { + if (F) |f| break :blk f(field.name); + break :blk field.name; + }, + .value = idx, + }}; + idx += 1; + } + return @Type(.{ .Enum = .{ + .tag_type = usize, + .fields = fields, + .decls = &.{}, + .is_exhaustive = true, + } }); +} + +const TestStruct = struct { + a_a: bool, + b_b: bool, +}; + +fn replaceUnderscores(comptime o: []const u8) []const u8 { + var name: [o.len]u8 = undefined; + _ = std.mem.replace(u8, o, &[_]u8{'_'}, &[_]u8{'-'}, &name); + return &name; +} + +test "StructFieldsEnum" { + { + const e = StructFieldsEnum(TestStruct, null); + try std.testing.expect(std.mem.eql(u8, std.meta.fields(e)[0].name, "a_a")); + try std.testing.expect(std.mem.eql(u8, std.meta.fields(e)[1].name, "b_b")); + } + + { + const e = StructFieldsEnum(TestStruct, replaceUnderscores); + try std.testing.expect(std.mem.eql(u8, std.meta.fields(e)[0].name, "a-a")); + try std.testing.expect(std.mem.eql(u8, std.meta.fields(e)[1].name, "b-b")); + } +} + +test "StructFieldsMap" { + { + const e = StructFieldsEnum(TestStruct, null); + const map = StructFieldsMap(TestStruct, e); + try std.testing.expect(std.mem.eql(u8, @tagName(map.get("a_a").?), "a_a")); + try std.testing.expect(std.mem.eql(u8, @tagName(map.get("b_b").?), "b_b")); + } + + { + const e = StructFieldsEnum(TestStruct, replaceUnderscores); + const map = StructFieldsMap(TestStruct, e); + try std.testing.expect(std.mem.eql(u8, @tagName(map.get("a_a").?), "a-a")); + try std.testing.expect(std.mem.eql(u8, @tagName(map.get("b_b").?), "b-b")); + } +} |