Something went wrong. Try again.
A Zig client for the Paperless-ngx REST API, with streaming uploads, task waiting and a command-line tool.
Something went wrong. Try again.
5.5 kB · 144 lines
Zig
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! The few places where `std.json`'s defaults and Paperless's JSON disagree.//!//! Three of them, each small://!//! * **Integer enums.** Paperless sends `matching_algorithm` and a saved//! view's `rule_type` as numbers. `std.json` reads a number into an enum//! happily, but writes an enum back as its tag name, which Paperless//! refuses. `writeIntEnum` writes the number.//! * **Enums that grow.** A task's `task_type`, a custom field's//! `data_type`: each release adds a value or two, and a client that fails//! to parse a whole page of tasks because one of them is of a kind it has//! not heard of is worse than one that says `unknown`. `parseOpenEnum` reads//! a name it does not know as `.unknown`.//! * **Setting a field to null.** A `PATCH` sends the fields it changes and//! no others, so "leave this alone" and "clear this" are two different//! requests, and an optional can only say one of them. `Clearable` is the//! other.
const std = @import("std");const Allocator = std.mem.Allocator;const testing = std.testing;
/// The options every request body is written with: a field left `null` is/// a field not sent, which is what makes one struct serve as both a `PUT`/// body and a `PATCH` body.pub const stringify_options: std.json.Stringify.Options = .{ .emit_null_optional_fields = false,};
/// The options every response is read with. Unknown fields are ignored/// because every Paperless release adds some, and a client should keep/// working against a server newer than itself; `alloc_always` because the/// response buffer is freed as soon as the parse is done.pub const parse_options: std.json.ParseOptions = .{ .ignore_unknown_fields = true, .allocate = .alloc_always,};
/// A field in an update that can be left alone, set, or cleared.////// Used as `?Clearable(T)`: `null` leaves the field out of the request,/// `.{ .set = x }` sends `x`, and `.clear` sends JSON `null` -- which is how/// a document loses its correspondent.pub fn Clearable(comptime T: type) type { return union(enum) { set: T, clear,
pub fn jsonStringify(self: @This(), jw: anytype) !void { switch (self) { .set => |v| try jw.write(v), .clear => try jw.write(null), } } };}
/// `jsonStringify` for an enum whose wire form is its integer value. An enum/// declares it as////// ```zig/// pub fn jsonStringify(self: @This(), jw: *std.json.Stringify) !void {/// return json.writeIntEnum(self, jw);/// }/// ```pub fn writeIntEnum(value: anytype, jw: *std.json.Stringify) std.json.Stringify.Error!void { try jw.write(@backingInt(value));}
/// `jsonParse` for a string enum with an `unknown` tag, which is what a name/// the enum does not have is read as.pub fn parseOpenEnum( comptime E: type, allocator: Allocator, source: anytype, options: std.json.ParseOptions,) std.json.ParseError(@TypeOf(source.*))!E { const token = try source.nextAllocMax(allocator, .alloc_if_needed, options.max_value_len.?); const slice = switch (token) { inline .string, .allocated_string => |slice| slice, else => return error.UnexpectedToken, }; defer switch (token) { .allocated_string => |owned| allocator.free(owned), else => {}, }; return std.meta.stringToEnum(E, slice) orelse .unknown;}
test Clearable { const Update = struct { title: ?[]const u8 = null, correspondent: ?Clearable(u64) = null, }; var out: std.Io.Writer.Allocating = .init(testing.allocator); defer out.deinit();
try std.json.Stringify.value(Update{ .correspondent = .clear }, stringify_options, &out.writer); try testing.expectEqualStrings("{\"correspondent\":null}", out.written());
out.clearRetainingCapacity(); try std.json.Stringify.value(Update{ .title = "t", .correspondent = .{ .set = 4 } }, stringify_options, &out.writer); try testing.expectEqualStrings("{\"title\":\"t\",\"correspondent\":4}", out.written());
out.clearRetainingCapacity(); try std.json.Stringify.value(Update{}, stringify_options, &out.writer); try testing.expectEqualStrings("{}", out.written());}
test parseOpenEnum { const Kind = enum { a, b, unknown, pub fn jsonParse(allocator: Allocator, source: anytype, options: std.json.ParseOptions) !@This() { return parseOpenEnum(@This(), allocator, source, options); } }; const parsed = try std.json.parseFromSlice([]Kind, testing.allocator, "[\"a\",\"b\",\"zebra\"]", parse_options); defer parsed.deinit(); try testing.expectEqualSlices(Kind, &.{ .a, .b, .unknown }, parsed.value);}
test writeIntEnum { const Algorithm = enum(u8) { none = 0, any = 1, _, pub fn jsonStringify(self: @This(), jw: *std.json.Stringify) !void { return writeIntEnum(self, jw); } }; var out: std.Io.Writer.Allocating = .init(testing.allocator); defer out.deinit(); try std.json.Stringify.value([_]Algorithm{ .any, @fromBackingInt(9) }, .{}, &out.writer); try testing.expectEqualStrings("[1,9]", out.written());
const parsed = try std.json.parseFromSlice(Algorithm, testing.allocator, "1", parse_options); defer parsed.deinit(); try testing.expectEqual(Algorithm.any, parsed.value);}