summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Segundo2023-07-24 10:28:58 +0200
committerChristian Segundo2023-07-24 10:28:58 +0200
commitf743e05e207046073999ee7234a157359d6a4f57 (patch)
tree0ceb01ffdcdc3a094e488c28e80c80d89f8309c5
parentb02b7f71978a172848322f0671d580e425634916 (diff)
downloadzmission-master.tar.gz
-rw-r--r--DockerStep.zig65
-rw-r--r--XCFrameworkStep.zig59
-rw-r--r--build.zig31
-rw-r--r--src/main.zig242
-rw-r--r--src/request.zig389
-rw-r--r--src/response.zig93
-rw-r--r--src/test_int.zig55
-rw-r--r--src/transmission.zig122
-rw-r--r--src/types.zig45
-rw-r--r--src/util.zig188
10 files changed, 809 insertions, 480 deletions
diff --git a/DockerStep.zig b/DockerStep.zig
new file mode 100644
index 0000000..9673c09
--- /dev/null
+++ b/DockerStep.zig
@@ -0,0 +1,65 @@
+//! A zig builder step that runs "docker run ..."
+//! This is primarily meant to do integration tests.
+const DockerStep = @This();
+
+const std = @import("std");
+const Step = std.build.Step;
+const RunStep = std.build.RunStep;
+
+pub const Options = struct {
+ /// Container name
+ name: []const u8,
+
+ /// The name of the image to run
+ image: []const u8,
+
+ /// The ports to expose
+ ports: []const []const u8,
+};
+
+step: *Step,
+
+pub fn create(b: *std.Build, opts: Options) *DockerStep {
+ const self = b.allocator.create(DockerStep) catch @panic("OOM");
+
+ const run_delete = remove(b, opts.name);
+
+ const run_create = run: {
+ const run = RunStep.create(b, b.fmt("docker run {s}", .{opts.name}));
+ run.has_side_effects = true;
+ run.addArgs(&.{
+ "docker",
+ "run",
+ "-d",
+ "--rm",
+ "--name",
+ opts.name,
+ });
+ for (opts.ports) |p| run.addArgs(&.{ "-p", p });
+ run.addArg(opts.image);
+ break :run run;
+ };
+
+ run_create.step.dependOn(run_delete.step);
+
+ self.* = .{
+ .step = &run_create.step,
+ };
+ return self;
+}
+
+pub fn remove(b: *std.Build, name: []const u8) *DockerStep {
+ const self = b.allocator.create(DockerStep) catch @panic("OOM");
+
+ const run_delete = run: {
+ const run = RunStep.create(b, b.fmt("docker rm {s}", .{name}));
+ run.has_side_effects = true;
+ run.addArgs(&.{ "docker", "rm", name, "-f" });
+ break :run run;
+ };
+
+ self.* = .{
+ .step = &run_delete.step,
+ };
+ return self;
+}
diff --git a/XCFrameworkStep.zig b/XCFrameworkStep.zig
new file mode 100644
index 0000000..36fdbeb
--- /dev/null
+++ b/XCFrameworkStep.zig
@@ -0,0 +1,59 @@
+//! A zig builder step that runs "swift build" in the context of
+//! a Swift project managed with SwiftPM. This is primarily meant to build
+//! executables currently since that is what we build.
+const XCFrameworkStep = @This();
+
+const std = @import("std");
+const Step = std.build.Step;
+const RunStep = std.build.RunStep;
+const FileSource = std.build.FileSource;
+
+pub const Options = struct {
+ /// The name of the xcframework to create.
+ name: []const u8,
+
+ /// The path to write the framework
+ out_path: []const u8,
+
+ /// Library file (dylib, a) to package.
+ library: std.build.FileSource,
+
+ /// Path to a directory with the headers.
+ headers: std.build.FileSource,
+};
+
+step: *Step,
+
+pub fn create(b: *std.Build, opts: Options) *XCFrameworkStep {
+ const self = b.allocator.create(XCFrameworkStep) catch @panic("OOM");
+
+ // We have to delete the old xcframework first since we're writing
+ // to a static path.
+ const run_delete = run: {
+ const run = RunStep.create(b, b.fmt("xcframework delete {s}", .{opts.name}));
+ run.has_side_effects = true;
+ run.addArgs(&.{ "rm", "-rf", opts.out_path });
+ break :run run;
+ };
+
+ // Then we run xcodebuild to create the framework.
+ const run_create = run: {
+ const run = RunStep.create(b, b.fmt("xcframework {s}", .{opts.name}));
+ run.has_side_effects = true;
+ run.addArgs(&.{ "xcodebuild", "-create-xcframework" });
+ run.addArg("-library");
+ run.addFileSourceArg(opts.library);
+ run.addArg("-headers");
+ run.addFileSourceArg(opts.headers);
+ run.addArg("-output");
+ run.addArg(opts.out_path);
+ break :run run;
+ };
+ run_create.step.dependOn(&run_delete.step);
+
+ self.* = .{
+ .step = &run_create.step,
+ };
+
+ return self;
+}
diff --git a/build.zig b/build.zig
index 7379c49..f29317f 100644
--- a/build.zig
+++ b/build.zig
@@ -1,5 +1,5 @@
const std = @import("std");
-//const DockerStep = @import("DockerStep.zig");
+const DockerStep = @import("DockerStep.zig");
// Although this function looks imperative, note that its job is to
// declaratively construct a build graph that will be executed by an external
@@ -30,11 +30,11 @@ pub fn build(b: *std.Build) void {
// running `zig build`).
b.installArtifact(lib);
- //const docker_run = DockerStep.create(b, .{
- //.name = "transmission-zig",
- //.image = "docker.io/linuxserver/transmission:4.0.3",
- //.ports = &[_][]const u8{"9091:9091"},
- //});
+ const docker_run_4_0_3 = DockerStep.create(b, .{
+ .name = "transmission-zig",
+ .image = "docker.io/linuxserver/transmission:4.0.3",
+ .ports = &[_][]const u8{"9091:9091"},
+ });
// Creates a step for unit testing. This only builds the test executable
// but does not run it.
@@ -52,11 +52,22 @@ pub fn build(b: *std.Build) void {
const test_step_unit = b.step("test", "Run library unit tests");
test_step_unit.dependOn(&run_main_tests.step);
- const test_step_int = b.step("test-int", "Run library integration tests");
- test_step_int.dependOn(&run_main_tests.step);
- //test_step_int.dependOn(docker_run.step);
+ const integration_tests = b.addTest(.{
+ .root_source_file = .{ .path = "src/test_int.zig" },
+ .target = target,
+ .optimize = optimize,
+ });
- //const docker_rm = DockerStep.remove(b, "transmission-zig");
+ const run_integration_tests = b.addRunArtifact(integration_tests);
+
+ const test_step_int_4_0_3 = b.step(
+ "test-int-4-0-3",
+ "Run library integration tests against Transmission 4.0.3",
+ );
+ //test_step_int.dependOn(&run_main_tests.step);
+ test_step_int_4_0_3.dependOn(docker_run_4_0_3.step);
+ test_step_int_4_0_3.dependOn(&run_integration_tests.step);
+ //const docker_rm = DockerStep.remove(b, "transmission-zig");
//docker_rm.step.dependOn(test_step_int);
}
diff --git a/src/main.zig b/src/main.zig
index a88bf8a..704d6c6 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -1,124 +1,130 @@
const std = @import("std");
const testing = std.testing;
-const transmission = @import("transmission.zig");
-
-var gpa = std.heap.GeneralPurposeAllocator(.{}){};
-const allocator = gpa.allocator();
-
-export fn add(a: i32, b: i32) i32 {
- const clientOptions = transmission.ClientOptions{
- .host = "192.168.0.2",
- .port = 9091,
- .https = false,
- };
- var client = transmission.Client.init(allocator, clientOptions);
- defer client.deinit();
-
- {
- const body = transmission.session_get_raw(&client, null) catch |err| {
- std.debug.print("error: {any}\n", .{err});
- unreachable;
- };
- defer allocator.free(body);
- std.debug.print("body: {s}\n", .{body});
- }
-
- //{
- //const body = transmission.torrent_get_(&client, null) catch |err| {
- //std.debug.print("error: {any}\n", .{err});
- //unreachable;
- //};
- ////defer allocator.free(body);
- //for (body.arguments.torrent_get.torrents.?) |t| {
- //std.debug.print("name: {any}\n", .{t});
- //}
- //}
-
- //{
- //const body = transmission.torrent_get_(&client, null) catch |err| {
- //std.debug.print("error: {any}\n", .{err});
- //unreachable;
- //};
- ////defer allocator.free(body);
- //std.debug.print("body: {any}\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;
-}
-
-export fn c_client_init(clientOptions: transmission.ClientOptions) ?*anyopaque {
- var client = allocator.create(transmission.Client) catch unreachable;
- client.* = transmission.Client.init(allocator, clientOptions);
- return @ptrCast(*anyopaque, client);
-}
-
-export fn c_client_deinit(client: ?*anyopaque) void {
- var real_client: *transmission.Client = @ptrCast(*transmission.Client, @alignCast(
- @alignOf(transmission.Client),
- client.?,
- ));
- real_client.*.deinit();
- allocator.destroy(real_client);
-}
-
-export fn c_session_get(client: ?*anyopaque, buf: [*]u8) c_int {
- var real_client: *transmission.Client = @ptrCast(*transmission.Client, @alignCast(
- @alignOf(transmission.Client),
- client.?,
- ));
-
- const body = transmission.session_get_raw(real_client, null) catch |err| {
- std.debug.print("error: {any}\n", .{err});
- unreachable;
- };
- defer allocator.free(body);
-
- std.debug.print("body: {s}\n", .{body});
- _ = buf;
- return 0;
-}
-
-export fn c_torrent_get(client: ?*anyopaque) [*:0]u8 {
- var real_client: *transmission.Client = @ptrCast(*transmission.Client, @alignCast(
- @alignOf(transmission.Client),
- client.?,
- ));
-
- const body = transmission.torrent_get_raw(real_client, null) catch |err| {
- std.debug.print("error: {any}\n", .{err});
- unreachable;
- };
- defer allocator.free(body);
- // TODO: use the same pointer of body but gotta go fast :(
- var foo = std.ArrayList(u8).init(allocator);
- foo.insertSlice(0, body) catch unreachable;
+pub const Request = @import("request.zig").Request;
+pub const Types = @import("types.zig");
- //std.debug.print("body: {s}\n", .{body});
- return foo.toOwnedSliceSentinel(0) catch unreachable;
+test {
+ testing.refAllDecls(@This());
}
-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_torrent_get(foo);
- c_client_deinit(foo);
-}
-
-test "basic add functionality" {
- try testing.expect(add(3, 7) == 10);
-}
+//var gpa = std.heap.GeneralPurposeAllocator(.{}){};
+//const allocator = gpa.allocator();
+
+//export fn add(a: i32, b: i32) i32 {
+//const clientOptions = transmission.ClientOptions{
+//.host = "192.168.0.2",
+//.port = 9091,
+//.https = false,
+//};
+//var client = transmission.Client.init(allocator, clientOptions);
+//defer client.deinit();
+
+//{
+//const body = transmission.session_get_raw(&client, null) catch |err| {
+//std.debug.print("error: {any}\n", .{err});
+//unreachable;
+//};
+//defer allocator.free(body);
+//std.debug.print("body: {s}\n", .{body});
+//}
+
+//{
+//const body = transmission.torrent_get_(&client, null) catch |err| {
+//std.debug.print("error: {any}\n", .{err});
+//unreachable;
+//};
+////defer allocator.free(body);
+//for (body.arguments.torrent_get.torrents.?) |t| {
+//std.debug.print("name: {any}\n", .{t});
+//}
+//}
+
+//{
+//const body = transmission.torrent_get_(&client, null) catch |err| {
+//std.debug.print("error: {any}\n", .{err});
+//unreachable;
+//};
+////defer allocator.free(body);
+//std.debug.print("body: {any}\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;
+//}
+
+//export fn c_client_init(clientOptions: transmission.ClientOptions) ?*anyopaque {
+//var client = allocator.create(transmission.Client) catch unreachable;
+//client.* = transmission.Client.init(allocator, clientOptions);
+//return @ptrCast(*anyopaque, client);
+//}
+
+//export fn c_client_deinit(client: ?*anyopaque) void {
+//var real_client: *transmission.Client = @ptrCast(*transmission.Client, @alignCast(
+//@alignOf(transmission.Client),
+//client.?,
+//));
+//real_client.*.deinit();
+//allocator.destroy(real_client);
+//}
+
+//export fn c_session_get(client: ?*anyopaque, buf: [*]u8) c_int {
+//var real_client: *transmission.Client = @ptrCast(*transmission.Client, @alignCast(
+//@alignOf(transmission.Client),
+//client.?,
+//));
+
+//const body = transmission.session_get_raw(real_client, null) catch |err| {
+//std.debug.print("error: {any}\n", .{err});
+//unreachable;
+//};
+//defer allocator.free(body);
+
+//std.debug.print("body: {s}\n", .{body});
+//_ = buf;
+//return 0;
+//}
+
+//export fn c_torrent_get(client: ?*anyopaque) [*:0]u8 {
+//var real_client: *transmission.Client = @ptrCast(*transmission.Client, @alignCast(
+//@alignOf(transmission.Client),
+//client.?,
+//));
+
+//const body = transmission.torrent_get_raw(real_client, null) catch |err| {
+//std.debug.print("error: {any}\n", .{err});
+//unreachable;
+//};
+//defer allocator.free(body);
+
+//// TODO: use the same pointer of body but gotta go fast :(
+//var foo = std.ArrayList(u8).init(allocator);
+//foo.insertSlice(0, body) catch unreachable;
+
+////std.debug.print("body: {s}\n", .{body});
+//return foo.toOwnedSliceSentinel(0) catch unreachable;
+//}
+
+//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_torrent_get(foo);
+//c_client_deinit(foo);
+//}
+
+//test "basic add functionality" {
+//try testing.expect(add(3, 7) == 10);
+//}
diff --git a/src/request.zig b/src/request.zig
index 214f997..a306baf 100644
--- a/src/request.zig
+++ b/src/request.zig
@@ -6,64 +6,62 @@ pub const Request = @This();
pub const Method = enum {
// Torrent requests: Torrent action
// https://github.com/transmission/transmission/blob/main/docs/rpc-spec.md#31-torrent-action-requests
- torrent_start,
- torrent_start_now,
- torrent_stop,
- torrent_verify,
- torrent_reannounce,
+ @"torrent-start",
+ @"torrent-start-now",
+ @"torrent-stop",
+ @"torrent-verify",
+ @"torrent-reannounce",
// Torrent requests: Mutator
// https://github.com/transmission/transmission/blob/main/docs/rpc-spec.md#32-torrent-mutator-torrent-set
- torrent_set,
+ @"torrent-set",
// Torrent requests: Torrent accessor
// https://github.com/transmission/transmission/blob/main/docs/rpc-spec.md#33-torrent-accessor-torrent-get
- torrent_get,
+ @"torrent-get",
// Torrent requests: Torrent adding
// https://github.com/transmission/transmission/blob/main/docs/rpc-spec.md#34-adding-a-torrent
- torrent_add,
+ @"torrent-add",
// Torrent requests: Torrent removing
// https://github.com/transmission/transmission/blob/main/docs/rpc-spec.md#35-removing-a-torrent
- torrent_remove,
+ @"torrent-remove",
// Torrent requests: Torrent moving
// https://github.com/transmission/transmission/blob/main/docs/rpc-spec.md#36-moving-a-torrent
- torrent_set_location,
+ @"torrent-set-location",
// Torrent requests: Torrent moving
// https://github.com/transmission/transmission/blob/main/docs/rpc-spec.md#37-renaming-a-torrents-path
- torrent_rename_path,
+ @"torrent-rename-path",
// Session requests: Get
// https://github.com/transmission/transmission/blob/main/docs/rpc-spec.md#4--session-requests
- session_get,
+ @"session-get",
// Session requests: Mutator
// https://github.com/transmission/transmission/blob/main/docs/rpc-spec.md#411-mutators
- session_set,
- //session_stats,
+ @"session-set",
- const Self = @This();
+ // TODO
+ //@"session-stats",
- 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 fn jsonStringify(self: Method, options: std.json.StringifyOptions, out_stream: anytype) !void {
+ try std.json.stringify(@tagName(self), options, out_stream);
}
};
pub const TorrentIDs = union(enum) {
single: i32,
many: []const i32,
- recently_active,
+ @"recently-active",
pub fn jsonStringify(self: TorrentIDs, 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),
+ .@"recently-active" => try std.json.stringify("recently-active", options, out_stream),
}
}
};
@@ -75,6 +73,34 @@ pub const TorrentStop = TorrentStart;
pub const TorrentVerify = TorrentStart;
pub const TorrentReannounce = TorrentStart;
+pub const TorrentAdd = struct {
+
+ // path to download the torrent to
+ @"download-dir": ?[]const u8 = null,
+
+ filename: ?[]const u8 = null,
+
+ labels: ?[][]const u8 = null,
+
+ // base64-encoded .torrent content
+ metainfo: ?[]const u8 = null,
+
+ // if true, don't start the torrent
+ paused: ?bool = null,
+
+ // TODO:
+ // maximum number of peers
+ //@"peer-limit": ?i32 = null,
+ //cookies string pointer to a string of one or more cookies.
+ //bandwidthPriority number torrent's bandwidth tr_priority_t
+ //files-wanted array indices of file(s) to download
+ //files-unwanted array indices of file(s) to not download
+ //priority-high array indices of high-priority file(s)
+ //priority-low array indices of low-priority file(s)
+ //priority-normal array indices of normal-priority file(s)
+
+};
+
pub const TorrentGet = struct {
pub const Fields = enum {
activityDate,
@@ -96,7 +122,7 @@ pub const TorrentGet = struct {
errorString,
eta,
etaIdle,
- file_count,
+ @"file-count",
files,
fileStats,
group,
@@ -115,7 +141,7 @@ pub const TorrentGet = struct {
maxConnectedPeers,
metadataPercentComplete,
name,
- peer_limit,
+ @"peer-limit",
peers,
peersConnected,
peersFrom,
@@ -127,7 +153,7 @@ pub const TorrentGet = struct {
pieceCount,
pieceSize,
priorities,
- primary_mime_type,
+ @"primary-mime-type",
queuePosition,
rateDownload,
rateUpload,
@@ -155,12 +181,8 @@ pub const TorrentGet = struct {
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 fn jsonStringify(self: @This(), options: std.json.StringifyOptions, out_stream: anytype) !void {
+ try std.json.stringify(@tagName(self), options, out_stream);
}
};
@@ -172,68 +194,64 @@ pub const TorrentGet = struct {
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,
+ @"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,
+ @"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,
+ @"speed-limit-down-enabled",
+ @"speed-limit-down",
+ @"speed-limit-up-enabled",
+ @"speed-limit-up",
+ @"start-added-torrents",
+ @"trash-original-torrent-files",
units,
- utp_enabled,
+ @"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 fn jsonStringify(self: @This(), options: std.json.StringifyOptions, out_stream: anytype) !void {
+ try std.json.stringify(@tagName(self), options, out_stream);
}
};
@@ -243,92 +261,82 @@ pub const SessionGet = struct {
};
pub const SessionSet = struct {
- alt_speed_down: ?usize = null,
- alt_speed_enabled: ?bool = null,
- alt_speed_time_begin: ?usize = null,
- alt_speed_time_day: ?usize = null,
- alt_speed_time_enabled: ?bool = null,
- alt_speed_time_end: ?usize = null,
- alt_speed_up: ?usize = null,
- blocklist_enabled: ?bool = null,
- blocklist_url: ?[]u8 = null,
- cache_size_mb: ?usize = null,
- default_trackers: ?[]u8 = null,
- dht_enabled: ?bool = null,
- download_dir: ?[]u8 = null,
- download_dir_free_space: ?usize = null,
- download_queue_enabled: ?bool = null,
- download_queue_size: ?usize = null,
+ @"alt-speed-down": ?usize = null,
+ @"alt-speed-enabled": ?bool = null,
+ @"alt-speed-time-begin": ?usize = null,
+ @"alt-speed-time-day": ?usize = null,
+ @"alt-speed-time-enabled": ?bool = null,
+ @"alt-speed-time-end": ?usize = null,
+ @"alt-speed-up": ?usize = null,
+ @"blocklist-enabled": ?bool = null,
+ @"blocklist-url": ?[]u8 = null,
+ @"cache-size-mb": ?usize = null,
+ @"default-trackers": ?[]u8 = null,
+ @"dht-enabled": ?bool = null,
+ @"download-dir": ?[]u8 = null,
+ @"download-dir-free-space": ?usize = null,
+ @"download-queue-enabled": ?bool = null,
+ @"download-queue-size": ?usize = null,
encryption: ?enum {
required,
preferred,
tolerated,
- 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 fn jsonStringify(self: @This(), options: std.json.StringifyOptions, out_stream: anytype) !void {
+ try std.json.stringify(@tagName(self), options, out_stream);
}
} = null,
- idle_seeding_limit_enabled: ?bool = null,
- idle_seeding_limit: ?usize = null,
- incomplete_dir_enabled: ?bool = null,
- incomplete_dir: ?[]u8 = null,
- lpd_enabled: ?bool = null,
- peer_limit_global: ?usize = null,
- peer_limit_per_torrent: ?usize = null,
- peer_port_random_on_start: ?bool = null,
- peer_port: ?usize = null,
- pex_enabled: ?bool = null,
- port_forwarding_enabled: ?bool = null,
- queue_stalled_enabled: ?bool = null,
- queue_stalled_minutes: ?usize = null,
- rename_partial_files: ?bool = null,
- script_torrent_added_enabled: ?bool = null,
- script_torrent_added_filename: ?[]u8 = null,
- script_torrent_done_enabled: ?bool = null,
- script_torrent_done_filename: ?[]u8 = null,
- script_torrent_done_seeding_enabled: ?bool = null,
- script_torrent_done_seeding_filename: ?[]u8 = null,
- seed_queue_enabled: ?bool = null,
- seed_queue_size: ?usize = null,
+ @"idle-seeding-limit-enabled": ?bool = null,
+ @"idle-seeding-limit": ?usize = null,
+ @"incomplete-dir-enabled": ?bool = null,
+ @"incomplete-dir": ?[]u8 = null,
+ @"lpd-enabled": ?bool = null,
+ @"peer-limit-global": ?usize = null,
+ @"peer-limit-per-torrent": ?usize = null,
+ @"peer-port-random-on-start": ?bool = null,
+ @"peer-port": ?usize = null,
+ @"pex-enabled": ?bool = null,
+ @"port-forwarding-enabled": ?bool = null,
+ @"queue-stalled-enabled": ?bool = null,
+ @"queue-stalled-minutes": ?usize = null,
+ @"rename-partial-files": ?bool = null,
+ @"script-torrent-added-enabled": ?bool = null,
+ @"script-torrent-added-filename": ?[]u8 = null,
+ @"script-torrent-done-enabled": ?bool = null,
+ @"script-torrent-done-filename": ?[]u8 = null,
+ @"script-torrent-done-seeding-enabled": ?bool = null,
+ @"script-torrent-done-seeding-filename": ?[]u8 = null,
+ @"seed-queue-enabled": ?bool = null,
+ @"seed-queue-size": ?usize = null,
seedRatioLimit: ?f64 = null,
seedRatioLimited: ?bool = null,
- speed_limit_down_enabled: ?bool = null,
- speed_limit_down: ?usize = null,
- speed_limit_up_enabled: ?bool = null,
- speed_limit_up: ?usize = null,
- start_added_torrents: ?bool = null,
- trash_original_torrent_files: ?bool = null,
- utp_enabled: ?bool = null,
-
- pub const json_map = util.JsonMap(SessionSet, util.replaceUnderscores);
-
- pub fn jsonStringify(self: SessionSet, options: std.json.StringifyOptions, out_stream: anytype) !void {
- try util.jsonStringify(self, options, out_stream);
- }
+ @"speed-limit-down-enabled": ?bool = null,
+ @"speed-limit-down": ?usize = null,
+ @"speed-limit-up-enabled": ?bool = null,
+ @"speed-limit-up": ?usize = null,
+ @"start-added-torrents": ?bool = null,
+ @"trash-original-torrent-files": ?bool = null,
+ @"utp-enabled": ?bool = null,
};
pub const Object = struct {
method: Method,
arguments: union(Method) {
- torrent_start: TorrentStart,
- torrent_start_now: TorrentStartNow,
- torrent_stop: TorrentStop,
- torrent_verify: TorrentVerify,
- torrent_reannounce: TorrentReannounce,
-
- torrent_set: u8,
- torrent_get: TorrentGet,
- torrent_add: u8,
- torrent_remove: u8,
- torrent_set_location: u8,
- torrent_rename_path: u8,
-
- session_get: SessionGet,
- session_set: SessionSet,
+ @"torrent-start": TorrentStart,
+ @"torrent-start-now": TorrentStartNow,
+ @"torrent-stop": TorrentStop,
+ @"torrent-verify": TorrentVerify,
+ @"torrent-reannounce": TorrentReannounce,
+
+ @"torrent-set": u8,
+ @"torrent-get": TorrentGet,
+ @"torrent-add": TorrentAdd,
+ @"torrent-remove": u8,
+ @"torrent-set-location": u8,
+ @"torrent-rename-path": u8,
+
+ @"session-get": SessionGet,
+ @"session-set": SessionSet,
const Self = @This();
@@ -338,18 +346,18 @@ pub const Object = struct {
};
switch (self) {
- .torrent_start,
- .torrent_start_now,
- .torrent_stop,
- .torrent_verify,
- .torrent_reannounce,
+ .@"torrent-start",
+ .@"torrent-start-now",
+ .@"torrent-stop",
+ .@"torrent-verify",
+ .@"torrent-reannounce",
=> |v| try std.json.stringify(v, options, out_stream),
- .torrent_get => |v| try std.json.stringify(v, options, out_stream),
+ .@"torrent-get" => |v| try std.json.stringify(v, options, out_stream),
+ .@"torrent-add" => |v| try std.json.stringify(v, options, out_stream),
- .session_set => |v| try std.json.stringify(v, options, out_stream),
-
- .session_get => |v| try std.json.stringify(v, options, out_stream),
+ .@"session-set" => |v| try std.json.stringify(v, options, out_stream),
+ .@"session-get" => |v| try std.json.stringify(v, options, out_stream),
else => unreachable,
}
}
@@ -377,14 +385,29 @@ test "json request encoding" {
const test_cases = [_]test_case{
.{
+ .name = "torrent-add",
+ .request = .{
+ .method = .@"torrent-add",
+ .arguments = .{
+ .@"torrent-add" = .{
+ .filename = "foobar",
+ },
+ },
+ },
+ .expected =
+ \\{"method":"torrent-add","arguments":{"filename":"foobar"}}
+ ,
+ },
+
+ .{
.name = "session-get",
.request = .{
- .method = .session_get,
+ .method = .@"session-get",
.arguments = .{
- .session_get = .{
+ .@"session-get" = .{
.fields = &[_]SessionGet.Fields{
.version,
- .utp_enabled,
+ .@"utp-enabled",
},
},
},
@@ -397,10 +420,10 @@ test "json request encoding" {
.{
.name = "session-set",
.request = .{
- .method = .session_set,
+ .method = .@"session-set",
.arguments = .{
- .session_set = .{
- .lpd_enabled = true,
+ .@"session-set" = .{
+ .@"lpd-enabled" = true,
.encryption = .required,
},
},
@@ -413,9 +436,9 @@ test "json request encoding" {
.{
.name = "torrent-reannounce single id",
.request = .{
- .method = .torrent_reannounce,
+ .method = .@"torrent-reannounce",
.arguments = .{
- .torrent_reannounce = .{
+ .@"torrent-reannounce" = .{
.ids = .{ .single = 1 },
},
},
@@ -428,9 +451,9 @@ test "json request encoding" {
.{
.name = "torrent-reannounce multiple id",
.request = .{
- .method = .torrent_reannounce,
+ .method = .@"torrent-reannounce",
.arguments = .{
- .torrent_reannounce = .{
+ .@"torrent-reannounce" = .{
.ids = .{ .many = &[_]i32{ 1, 2 } },
},
},
@@ -443,10 +466,10 @@ test "json request encoding" {
.{
.name = "torrent-reannounce recently-active",
.request = .{
- .method = .torrent_reannounce,
+ .method = .@"torrent-reannounce",
.arguments = .{
- .torrent_reannounce = .{
- .ids = .recently_active,
+ .@"torrent-reannounce" = .{
+ .ids = .@"recently-active",
},
},
},
@@ -458,14 +481,14 @@ test "json request encoding" {
.{
.name = "torrent-get",
.request = .{
- .method = .torrent_get,
+ .method = .@"torrent-get",
.arguments = .{
- .torrent_get = .{
+ .@"torrent-get" = .{
.fields = &[_]TorrentGet.Fields{
.id,
.name,
},
- .ids = .recently_active,
+ .ids = .@"recently-active",
},
},
},
diff --git a/src/response.zig b/src/response.zig
index 8964de4..a370133 100644
--- a/src/response.zig
+++ b/src/response.zig
@@ -4,44 +4,75 @@ const Types = @import("types.zig");
pub const Response = @This();
+pub const default_parse_options = std.json.ParseOptions{
+ .ignore_unknown_fields = true,
+ .duplicate_field_behavior = .@"error",
+};
+
+allocator: std.mem.Allocator,
+data: Data,
+
+pub const Data = union(enum) {
+ TorrentGet: TorrentGet,
+ TorrentAdd: TorrentAdd,
+};
+
+pub fn parseBody(
+ allocator: std.mem.Allocator,
+ method: Request.Method,
+ body: []const u8,
+ parseOptions: ?std.json.ParseOptions,
+) !Response {
+ @setEvalBranchQuota(10000);
+ switch (method) {
+ .@"torrent-get" => {
+ const decoded = try std.json.parseFromSlice(
+ TorrentGet,
+ allocator,
+ body,
+ parseOptions orelse default_parse_options,
+ );
+ return Response{
+ .data = Data{ .TorrentGet = decoded },
+ .allocator = allocator,
+ };
+ },
+ .@"torrent-add" => {
+ const decoded = try std.json.parseFromSlice(
+ TorrentAdd,
+ allocator,
+ body,
+ parseOptions orelse default_parse_options,
+ );
+ return Response{
+ .data = Data{ .TorrentAdd = decoded },
+ .allocator = allocator,
+ };
+ },
+ else => unreachable,
+ }
+}
+
+pub fn deinit(self: *Response) void {
+ std.json.parseFree(@TypeOf(self.data), self.allocator, self.*.data);
+}
+
pub const TorrentGet = struct {
pub const Arguments = struct {
torrents: ?[]Types.Torrent = null,
};
-
result: []const u8,
- arguments: Arguments,
+ arguments: @This().Arguments,
};
-pub const Object = struct {
- pub const Arguments = union(Request.Method) {
- torrent_start,
- torrent_start_now,
- torrent_stop,
- torrent_verify,
- torrent_reannounce,
- torrent_set,
- torrent_get: TorrentGet.Arguments,
- torrent_add,
- torrent_remove,
- torrent_set_location,
- torrent_rename_path,
- session_get,
- session_set,
+pub const TorrentAdd = struct {
+ pub const Arguments = struct {
+ @"torrent-added": ?struct {
+ hashString: []const u8,
+ id: u64,
+ name: []const u8,
+ } = null,
};
-
result: []const u8,
- arguments: Arguments,
-
- pub fn init(arguments: Arguments, result: []const u8) Object {
- switch (arguments) {
- .torrent_get => {
- return Object{
- .result = result,
- .arguments = arguments,
- };
- },
- else => unreachable,
- }
- }
+ arguments: @This().Arguments,
};
diff --git a/src/test_int.zig b/src/test_int.zig
new file mode 100644
index 0000000..331296a
--- /dev/null
+++ b/src/test_int.zig
@@ -0,0 +1,55 @@
+const std = @import("std");
+const testing = std.testing;
+
+const transmission = @import("transmission.zig");
+const Request = transmission.Request;
+const Response = transmission.Response;
+
+test "integration" {
+ var client = transmission.Client.init(
+ std.testing.allocator,
+ transmission.ClientOptions{
+ .host = "127.0.0.1",
+ .port = 9091,
+ .https = false,
+ },
+ );
+ defer client.deinit();
+
+ const add_torrent_request = Request.TorrentAdd{
+ .metainfo = "ZDg6YW5ub3VuY2U0MDp1ZHA6Ly90cmFja2VyLmxlZWNoZXJzLXBhcmFkaXNlLm9yZzo2OTY5MTM6YW5ub3VuY2UtbGlzdGxsNDA6dWRwOi8vdHJhY2tlci5sZWVjaGVycy1wYXJhZGlzZS5vcmc6Njk2OWVsMzQ6dWRwOi8vdHJhY2tlci5jb3BwZXJzdXJmZXIudGs6Njk2OWVsMzM6dWRwOi8vdHJhY2tlci5vcGVudHJhY2tyLm9yZzoxMzM3ZWwyMzp1ZHA6Ly9leHBsb2RpZS5vcmc6Njk2OWVsMzE6dWRwOi8vdHJhY2tlci5lbXBpcmUtanMudXM6MTMzN2VsMjY6d3NzOi8vdHJhY2tlci5idG9ycmVudC54eXplbDMyOndzczovL3RyYWNrZXIub3BlbndlYnRvcnJlbnQuY29tZWwyNTp3c3M6Ly90cmFja2VyLmZhc3RjYXN0Lm56ZWU3OmNvbW1lbnQzNDpXZWJUb3JyZW50IDxodHRwczovL3dlYnRvcnJlbnQuaW8+MTA6Y3JlYXRlZCBieTM0OldlYlRvcnJlbnQgPGh0dHBzOi8vd2VidG9ycmVudC5pbz4xMzpjcmVhdGlvbiBkYXRlaTE0OTA5MTY2MDFlODplbmNvZGluZzU6VVRGLTg0OmluZm9kNTpmaWxlc2xkNjpsZW5ndGhpMTQwZTQ6cGF0aGwyMTpCaWcgQnVjayBCdW5ueS5lbi5zcnRlZWQ2Omxlbmd0aGkyNzYxMzQ5NDdlNDpwYXRobDE4OkJpZyBCdWNrIEJ1bm55Lm1wNGVlZDY6bGVuZ3RoaTMxMDM4MGU0OnBhdGhsMTA6cG9zdGVyLmpwZ2VlZTQ6bmFtZTE0OkJpZyBCdWNrIEJ1bm55MTI6cGllY2UgbGVuZ3RoaTI2MjE0NGU2OnBpZWNlczIxMTAwOiAgp3idb4sYYjstyh3lJ98/JyMMPf/WNhoGtrdjuusnMx0a9GVizVfwK7nrmbLHEz6WcIrHiULOm8SUYdyfrq0PwN+T+Zr5tqCcQLGxs7fA+0/dCsHp6W8Jx/qlDBM6xNkHdg9YI2Z9tb8i6mrb5kEGX44fVQJMWLPEJuTp0RA+i52Nhm1Y79wGdfve7wd3C5pQyNIqLoJ0caUU6eayEVY4JhiYM0KiWc6iqArXI5/VC7dOYDqo/zH8wZourwF0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",
+ };
+
+ var add_torrent = try transmission.torrentAdd(&client, add_torrent_request);
+ defer add_torrent.deinit();
+
+ try testing.expectEqualStrings("success", add_torrent.data.TorrentAdd.result);
+ try testing.expectEqualStrings(
+ "dd8255ecdc7ca55fb0bbf81323d87062db1f6d1c",
+ add_torrent.data.TorrentAdd.arguments.@"torrent-added".?.hashString,
+ );
+ try testing.expectEqualStrings(
+ "Big Buck Bunny",
+ add_torrent.data.TorrentAdd.arguments.@"torrent-added".?.name,
+ );
+ try testing.expect(
+ 1 == add_torrent.data.TorrentAdd.arguments.@"torrent-added".?.id,
+ );
+
+ var get_torrent_null = try transmission.torrentGet(&client, null);
+ defer get_torrent_null.deinit();
+
+ try testing.expectEqualStrings("success", get_torrent_null.data.TorrentGet.result);
+ try testing.expect(1 == get_torrent_null.data.TorrentGet.arguments.torrents.?.len);
+ try testing.expectEqualStrings(
+ "dd8255ecdc7ca55fb0bbf81323d87062db1f6d1c",
+ get_torrent_null.data.TorrentGet.arguments.torrents.?[0].hashString.?,
+ );
+ try testing.expectEqualStrings(
+ "Big Buck Bunny",
+ get_torrent_null.data.TorrentGet.arguments.torrents.?[0].name.?,
+ );
+ try testing.expect(
+ 1 == get_torrent_null.data.TorrentGet.arguments.torrents.?[0].id.?,
+ );
+}
diff --git a/src/transmission.zig b/src/transmission.zig
index 9f0f5f0..239151a 100644
--- a/src/transmission.zig
+++ b/src/transmission.zig
@@ -3,8 +3,8 @@ const http = std.http;
const util = @import("util.zig");
-const Request = @import("request.zig");
-const Response = @import("response.zig");
+pub const Request = @import("request.zig");
+pub const Response = @import("response.zig").Response;
pub const ClientOptions = extern struct {
host: [*:0]const u8,
@@ -100,58 +100,92 @@ pub const Client = struct {
}
};
-pub fn session_get_raw(client: *Client, session_get: ?Request.SessionGet) ![]u8 {
- const default: Request.SessionGet = .{
- .fields = Request.SessionGet.all_fields,
- };
-
- const r = Request.Object{
- .method = .session_get,
- .arguments = .{ .session_get = session_get orelse default },
- };
- const body = try client.do(r);
- return body;
+pub fn torrentAdd(client: *Client, r: Request.TorrentAdd) !Response {
+ const body = try torrentAddRaw(client, r);
+ defer client.allocator.free(body);
+ return Response.parseBody(
+ client.allocator,
+ Request.Method.@"torrent-add",
+ body,
+ null,
+ );
}
-pub fn torrent_get_raw(client: *Client, torrent_get: ?Request.TorrentGet) ![]u8 {
- const default: Request.TorrentGet = .{
- .fields = Request.TorrentGet.all_fields,
- };
- //const default: Request.TorrentGet = .{
- //.fields = &[_]Request.TorrentGet.Fields{.name},
- //};
-
- const r = Request.Object{
- .method = .torrent_get,
- .arguments = .{ .torrent_get = torrent_get orelse default },
- };
- const body = try client.do(r);
- std.debug.print("{s}\n", .{body});
- if (1 == 1)
- @panic("");
+pub fn torrentAddRaw(client: *Client, r: Request.TorrentAdd) ![]u8 {
+ const body = try client.do(Request.Object{
+ .method = .@"torrent-add",
+ .arguments = .{ .@"torrent-add" = r },
+ });
return body;
}
-pub fn torrent_get_(client: *Client, torrent_get: ?Request.TorrentGet) !Response.Object {
- @setEvalBranchQuota(100000);
- const body = try torrent_get_raw(client, torrent_get);
- const decoded: Response.TorrentGet = try std.json.parseFromSlice(
- Response.TorrentGet,
+pub fn torrentGet(client: *Client, r: ?Request.TorrentGet) !Response {
+ const body = try torrentGetRaw(client, r);
+ defer client.allocator.free(body);
+ return Response.parseBody(
client.allocator,
+ Request.Method.@"torrent-get",
body,
- std.json.ParseOptions{
- .ignore_unknown_fields = true,
- .duplicate_field_behavior = .@"error",
- },
+ null,
);
- return Response.Object.init(.{ .torrent_get = decoded.arguments }, decoded.result);
}
-pub fn session_set_raw(client: *Client, session_set: Request.SessionSet) ![]u8 {
- const r = Request.Object{
- .method = .session_set,
- .arguments = .{ .session_set = session_set },
+pub fn torrentGetRaw(client: *Client, r: ?Request.TorrentGet) ![]u8 {
+ const request = blk: {
+ if (r == null) {
+ break :blk Request.Object{
+ .method = .@"torrent-get",
+ .arguments = .{
+ .@"torrent-get" = Request.TorrentGet{
+ .fields = Request.TorrentGet.all_fields,
+ },
+ },
+ };
+ } else {
+ break :blk Request.Object{
+ .method = .@"torrent-get",
+ .arguments = .{ .@"torrent-get" = r.? },
+ };
+ }
};
- const body = try client.do(r);
+
+ const body = try client.do(request);
return body;
}
+
+//pub fn session_get_raw(client: *Client, session_get: ?Request.SessionGet) ![]u8 {
+//const default: Request.SessionGet = .{
+//.fields = Request.SessionGet.all_fields,
+//};
+
+//const r = Request.Object{
+//.method = .session_get,
+//.arguments = .{ .session_get = session_get orelse default },
+//};
+//const body = try client.do(r);
+//return body;
+//}
+
+//pub fn torrent_get_(client: *Client, torrent_get: ?Request.TorrentGet) !Response.Object {
+//@setEvalBranchQuota(100000);
+//const body = try torrent_get_raw(client, torrent_get);
+//const decoded: Response.TorrentGet = try std.json.parseFromSlice(
+//Response.TorrentGet,
+//client.allocator,
+//body,
+//std.json.ParseOptions{
+//.ignore_unknown_fields = true,
+//.duplicate_field_behavior = .@"error",
+//},
+//);
+//return Response.Object.init(.{ .torrent_get = decoded.arguments }, decoded.result);
+//}
+
+//pub fn session_set_raw(client: *Client, session_set: Request.SessionSet) ![]u8 {
+//const r = Request.Object{
+//.method = .session_set,
+//.arguments = .{ .session_set = session_set },
+//};
+//const body = try client.do(r);
+//return body;
+//}
diff --git a/src/types.zig b/src/types.zig
index 7de8fa1..7ed50dc 100644
--- a/src/types.zig
+++ b/src/types.zig
@@ -1,3 +1,7 @@
+const std = @import("std");
+
+const Request = @import("request.zig").Request;
+
// types are from:
// https://github.com/transmission/transmission/blob/76166d8fa71f351fe46221d737f8849b23f551f7/libtransmission/transmission.h#L1604
// where:
@@ -225,3 +229,44 @@ pub const Torrent = struct {
// Number of webseeds that are sending data to us.
webseedsSendingToUs: ?u16 = null,
};
+
+// Checks that all fields in Torrent are in sync with TorrentGet.Fields
+test "torrent fields" {
+ @setEvalBranchQuota(10000);
+
+ const torrent_get_request_fields = comptime blk: {
+ const kv = struct { []const u8 };
+ var slice: []const kv = &[_]kv{};
+
+ for (std.meta.fields(Request.TorrentGet.Fields)) |field| {
+ slice = slice ++ &[_]kv{.{field.name}};
+ }
+
+ break :blk std.ComptimeStringMap(void, slice);
+ };
+
+ const torrent_fields = comptime blk: {
+ const kv = struct { []const u8 };
+ var slice: []const kv = &[_]kv{};
+
+ for (std.meta.fields(Torrent)) |field| {
+ slice = slice ++ &[_]kv{.{field.name}};
+ }
+
+ break :blk std.ComptimeStringMap(void, slice);
+ };
+
+ inline for (std.meta.fields(Torrent)) |field| {
+ if (!torrent_get_request_fields.has(field.name)) {
+ std.debug.print("field is present in Torrent but missing in TorrentGet: {s}\n", .{field.name});
+ }
+ //try std.testing.expect(torrent_get_request_fields.has(field.name));
+ }
+
+ inline for (std.meta.fields(Request.TorrentGet.Fields)) |field| {
+ if (!torrent_fields.has(field.name)) {
+ std.debug.print("field is present in TorrentGet but missing in Torrent: {s}\n", .{field.name});
+ }
+ //try std.testing.expect(torrent_fields.has(field.name));
+ }
+}
diff --git a/src/util.zig b/src/util.zig
index c46dbe7..07210b2 100644
--- a/src/util.zig
+++ b/src/util.zig
@@ -1,108 +1,108 @@
const std = @import("std");
-const struct_util = @import("util/struct.zig");
+//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 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,
+//}
+//};
+//}
// 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;
+//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;
+//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;
- }
- }
- }
+//// 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,
- }
-}
+//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,
+//}
+//}
-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;
-}
+//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;
+//}