summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Segundo2023-06-15 19:54:16 +0200
committerChristian Segundo2023-06-15 19:54:16 +0200
commitdb5d7914d82d69021cf303e0ab02b46e0730bf48 (patch)
tree0b515caee3c355f02e3a1eb5df15784b4a62179f
parent17f3a7b655938eb47efc2bbe884e68786bce6077 (diff)
downloadzmission-db5d7914d82d69021cf303e0ab02b46e0730bf48.tar.gz
one step closer
-rw-r--r--src/main.zig31
-rw-r--r--src/request.zig421
-rw-r--r--src/transmission.zig18
-rw-r--r--src/util.zig119
-rw-r--r--src/util/enum.zig56
-rw-r--r--src/util/struct.zig85
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"));
+ }
+}