const std = @import("std"); const util = @import("util.zig"); 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 requests: Mutator // https://github.com/transmission/transmission/blob/main/docs/rpc-spec.md#32-torrent-mutator-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 requests: Torrent adding // https://github.com/transmission/transmission/blob/main/docs/rpc-spec.md#34-adding-a-torrent torrent_add, // Torrent requests: Torrent removing // https://github.com/transmission/transmission/blob/main/docs/rpc-spec.md#35-removing-a-torrent 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 requests: Torrent moving // https://github.com/transmission/transmission/blob/main/docs/rpc-spec.md#37-renaming-a-torrents-path torrent_rename_path, // Session requests: Get // https://github.com/transmission/transmission/blob/main/docs/rpc-spec.md#4--session-requests session_get, // Session requests: Mutator // https://github.com/transmission/transmission/blob/main/docs/rpc-spec.md#411-mutators session_set, //session_stats, 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.RPCFields(Self); 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 std.json.stringify(self.str(), options, out_stream); } }; pub const SessionSetFields = 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, encryption: ?enum { required, preferred, tolerated, const Self = @This(); pub const Fields = util.RPCFields(Self); 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 std.json.stringify(self.str(), 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, 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 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.RPCFields(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 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 Request = struct { method: Method, arguments: union(Method) { torrent_start: TorrentActionFields, torrent_start_now: TorrentActionFields, torrent_stop: TorrentActionFields, torrent_verify: TorrentActionFields, torrent_reannounce: TorrentActionFields, torrent_set: u8, torrent_get: u8, torrent_add: u8, torrent_remove: u8, torrent_set_location: u8, torrent_rename_path: u8, session_get: struct { fields: []const SessionGetFields, }, session_set: SessionSetFields, const Self = @This(); pub fn jsonStringify(self: Self, _: std.json.StringifyOptions, out_stream: anytype) !void { const options = std.json.StringifyOptions{ .emit_null_optional_fields = false, }; switch (self) { .torrent_start, .torrent_start_now, .torrent_stop, .torrent_verify, .torrent_reannounce, => |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, } } }, }; test "json request encoding" { const test_case = struct { name: []const u8, expected: []const u8, request: Request, fn run(self: @This()) !void { var req = std.ArrayList(u8).init(std.testing.allocator); defer req.deinit(); try std.json.stringify(self.request, .{}, req.writer()); try std.testing.expect(std.mem.eql(u8, req.items, self.expected)); } }; const test_cases = [_]test_case{ .{ .name = "session-get", .request = .{ .method = .session_get, .arguments = .{ .session_get = .{ .fields = &[_]SessionGetFields{ .version, .utp_enabled, }, }, }, }, .expected = \\{"method":"session-get","arguments":{"fields":["version","utp-enabled"]}} , }, .{ .name = "session-set", .request = .{ .method = .session_set, .arguments = .{ .session_set = .{ .lpd_enabled = true, .encryption = .required, }, }, }, .expected = \\{"method":"session-set","arguments":{"encryption":"required","lpd_enabled":true}} , }, .{ .name = "torrent-reannounce single id", .request = .{ .method = .torrent_reannounce, .arguments = .{ .torrent_reannounce = .{ .ids = .{ .single = 1 }, }, }, }, .expected = \\{"method":"torrent-reannounce","arguments":{"ids":1}} , }, .{ .name = "torrent-reannounce multiple id", .request = .{ .method = .torrent_reannounce, .arguments = .{ .torrent_reannounce = .{ .ids = .{ .many = &[_]usize{ 1, 2 } }, }, }, }, .expected = \\{"method":"torrent-reannounce","arguments":{"ids":[1,2]}} , }, .{ .name = "torrent-reannounce recently-active", .request = .{ .method = .torrent_reannounce, .arguments = .{ .torrent_reannounce = .{ .ids = .recently_active, }, }, }, .expected = \\{"method":"torrent-reannounce","arguments":{"ids":"recently-active"}} , }, }; for (test_cases) |tc| try tc.run(); }