// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // SPDX-License-Identifier: MIT //! What the server sends back, and what is sent to it. //! //! Each object comes in two shapes. The one named for the object -- `Tag`, //! `Document` -- is what a response holds, and nearly every field in it has a //! default. That is not because the fields are optional in Paperless but //! because what arrives depends on how it was asked for: `?fields=id,title` //! trims a document to two fields, `?full_perms=true` swaps //! `user_can_change` for `permissions`, and an older server leaves out //! whatever a newer one added. Only the identifier is required; anything //! else that is missing reads as its default rather than failing the parse. //! //! The other shape is the request body: `TagFields`, `DocumentFields`. Every //! field in one of those is optional, and a field left `null` is not sent -- //! so the same struct is a complete object to `create` and a partial one to //! `update`, which is a `PATCH`. A field the server allows to be null is a //! `?Clearable(T)`, so that "leave it" and "clear it" can be told apart. //! //! Dates and times are kept as the strings Paperless sent: ISO 8601, a //! date-time in UTC with a `Z` or a bare date. Interpreting them is the //! caller's business, and a calendar library is a heavier dependency than a //! client for one HTTP API should impose on everybody who uses it. const std = @import("std"); const Allocator = std.mem.Allocator; const json = @import("json.zig"); const Clearable = json.Clearable; pub const Id = u64; /// One page of a list. Paperless paginates every list it returns, with a /// default page size of 25 that `page_size` raises -- to as much as 100000, /// so asking for one very large page is a legitimate way to fetch /// everything. pub fn Page(comptime T: type) type { return struct { /// How many objects match, across every page. count: u64, /// The URL of the next page, built by the server from the request it /// received. Behind a proxy that rewrites the scheme or the host it /// can name somewhere the client cannot reach, which is why /// `Pager` counts pages instead of following it; whether it is /// null is all that is used. next: ?[]const u8 = null, previous: ?[]const u8 = null, results: []T, /// The identifiers of every matching object, on every page. Sent with /// document lists, which is what lets a client select "all" without /// fetching them. all: []const Id = &.{}, /// With a full-text `query` that the index thinks was misspelled, /// what it would have searched for instead. corrected_query: ?[]const u8 = null, }; } /// How an object decides, on its own, which documents it belongs on. pub const MatchingAlgorithm = enum(u8) { none = 0, /// Any of the words in `match`. any = 1, /// All of the words in `match`. all = 2, /// `match` as an exact string. literal = 3, /// `match` as a regular expression. regex = 4, /// `match`, approximately. fuzzy = 5, /// Learned from the documents it has been assigned to by hand. auto = 6, _, pub fn jsonStringify(self: @This(), jw: *std.json.Stringify) !void { return json.writeIntEnum(self, jw); } }; /// Who besides the owner may see an object, and who may change it. pub const Permissions = struct { view: Set = .{}, change: Set = .{}, pub const Set = struct { users: []const Id = &.{}, groups: []const Id = &.{}, }; }; /// The `set_permissions` a create or an update sends. The same shape as /// `Permissions`; a separate name because it is a request and not a reading /// of one. pub const SetPermissions = Permissions; pub const Tag = struct { id: Id, slug: []const u8 = "", name: []const u8 = "", /// `#rrggbb`. color: []const u8 = "", /// Chosen by the server to be legible on `color`. text_color: []const u8 = "", match: []const u8 = "", matching_algorithm: MatchingAlgorithm = .none, is_insensitive: bool = false, /// Whether a new document is given this tag on arrival. is_inbox_tag: bool = false, document_count: u64 = 0, owner: ?Id = null, /// With `full_perms`; null without. permissions: ?Permissions = null, /// Without `full_perms`; null with. user_can_change: ?bool = null, /// Tags form a tree. parent: ?Id = null, /// The tags whose parent this is, each whole and with its own children. /// The schema Paperless publishes says these are identifiers; what it /// sends is objects. children: []const Tag = &.{}, }; pub const TagFields = struct { name: ?[]const u8 = null, color: ?[]const u8 = null, match: ?[]const u8 = null, matching_algorithm: ?MatchingAlgorithm = null, is_insensitive: ?bool = null, is_inbox_tag: ?bool = null, owner: ?Clearable(Id) = null, parent: ?Clearable(Id) = null, set_permissions: ?SetPermissions = null, }; pub const Correspondent = struct { id: Id, slug: []const u8 = "", name: []const u8 = "", match: []const u8 = "", matching_algorithm: MatchingAlgorithm = .none, is_insensitive: bool = false, document_count: u64 = 0, /// The `created` date of the newest document from this correspondent. last_correspondence: ?[]const u8 = null, owner: ?Id = null, permissions: ?Permissions = null, user_can_change: ?bool = null, }; pub const CorrespondentFields = struct { name: ?[]const u8 = null, match: ?[]const u8 = null, matching_algorithm: ?MatchingAlgorithm = null, is_insensitive: ?bool = null, owner: ?Clearable(Id) = null, set_permissions: ?SetPermissions = null, }; pub const DocumentType = struct { id: Id, slug: []const u8 = "", name: []const u8 = "", match: []const u8 = "", matching_algorithm: MatchingAlgorithm = .none, is_insensitive: bool = false, document_count: u64 = 0, owner: ?Id = null, permissions: ?Permissions = null, user_can_change: ?bool = null, }; pub const DocumentTypeFields = CorrespondentFields; pub const StoragePath = struct { id: Id, slug: []const u8 = "", name: []const u8 = "", /// A filename template, as in `PAPERLESS_FILENAME_FORMAT`. path: []const u8 = "", match: []const u8 = "", matching_algorithm: MatchingAlgorithm = .none, is_insensitive: bool = false, document_count: u64 = 0, owner: ?Id = null, permissions: ?Permissions = null, user_can_change: ?bool = null, }; pub const StoragePathFields = struct { name: ?[]const u8 = null, path: ?[]const u8 = null, match: ?[]const u8 = null, matching_algorithm: ?MatchingAlgorithm = null, is_insensitive: ?bool = null, owner: ?Clearable(Id) = null, set_permissions: ?SetPermissions = null, }; /// What a custom field holds. Paperless adds a type every few releases, so a /// type this does not name reads as `unknown` rather than failing. pub const CustomFieldType = enum { string, url, date, boolean, integer, float, /// A currency code followed by an amount, `EUR12.50`, or a bare amount. monetary, /// A list of document identifiers. documentlink, /// One of `extra_data.select_options`, by its `id`. select, longtext, unknown, pub fn jsonParse(allocator: Allocator, source: anytype, options: std.json.ParseOptions) !@This() { return json.parseOpenEnum(@This(), allocator, source, options); } }; pub const CustomField = struct { id: Id, name: []const u8 = "", data_type: CustomFieldType = .unknown, /// Type-specific settings -- `select_options` for a select, /// `default_currency` for a monetary field. Left as JSON because what it /// holds depends on `data_type`. extra_data: std.json.Value = .null, document_count: u64 = 0, }; pub const CustomFieldFields = struct { name: ?[]const u8 = null, data_type: ?CustomFieldType = null, extra_data: ?std.json.Value = null, }; /// A custom field's value on one document. pub const CustomFieldInstance = struct { field: Id, /// Whatever the field's type makes it: a string, a number, a boolean, a /// list of document identifiers, or null. value: std.json.Value = .null, }; pub const BasicUser = struct { id: Id, username: []const u8 = "", first_name: []const u8 = "", last_name: []const u8 = "", }; pub const Note = struct { id: Id, note: []const u8 = "", created: []const u8 = "", /// Null for a note whose author has since been deleted. user: ?BasicUser = null, }; /// One version of a document's file. Paperless 3 keeps the history of a /// document's file as a chain of versions, the first of which is the root. pub const DocumentVersion = struct { id: Id, added: []const u8 = "", version_label: ?[]const u8 = null, checksum: ?[]const u8 = null, is_root: bool = false, }; pub const DuplicateDocument = struct { id: Id, title: []const u8 = "", deleted_at: ?[]const u8 = null, }; pub const Document = struct { id: Id, correspondent: ?Id = null, document_type: ?Id = null, storage_path: ?Id = null, title: []const u8 = "", /// The extracted text, which is what full-text search reads. Cut short /// when the list was asked for with `truncate_content`. content: []const u8 = "", tags: []const Id = &.{}, /// The date the document itself bears, `YYYY-MM-DD`. created: ?[]const u8 = null, created_date: ?[]const u8 = null, modified: ?[]const u8 = null, /// When it arrived in Paperless. added: ?[]const u8 = null, /// Set when the document is in the trash. deleted_at: ?[]const u8 = null, archive_serial_number: ?u64 = null, original_file_name: ?[]const u8 = null, archived_file_name: ?[]const u8 = null, duplicate_documents: []const DuplicateDocument = &.{}, owner: ?Id = null, permissions: ?Permissions = null, user_can_change: ?bool = null, is_shared_by_requester: bool = false, notes: []const Note = &.{}, custom_fields: []const CustomFieldInstance = &.{}, page_count: ?u64 = null, mime_type: []const u8 = "", /// The document this one is a version of, when it is one. root_document: ?Id = null, versions: []const DocumentVersion = &.{}, /// With `?search=` or `?more_like_id=`: how well it matched. __search_hit__: ?SearchHit = null, pub const SearchHit = struct { score: ?f64 = null, /// The matching passage, with the terms marked in HTML. highlights: ?[]const u8 = null, /// The same for the document's notes, when it was a note that /// matched. note_highlights: ?[]const u8 = null, rank: ?u64 = null, }; }; /// The editable fields of a document. The file itself is not one of them: /// a new file is a new version, uploaded with `Documents.updateVersion`. pub const DocumentFields = struct { title: ?[]const u8 = null, content: ?[]const u8 = null, correspondent: ?Clearable(Id) = null, document_type: ?Clearable(Id) = null, storage_path: ?Clearable(Id) = null, /// Replaces the document's tags; it does not add to them. tags: ?[]const Id = null, /// `YYYY-MM-DD`. created: ?[]const u8 = null, archive_serial_number: ?Clearable(u64) = null, owner: ?Clearable(Id) = null, /// Replaces the document's custom fields: one left out of the list is /// taken off the document. custom_fields: ?[]const CustomFieldInstance = null, /// Takes every inbox tag off the document, alongside whatever `tags` /// says -- what the web interface's "remove from inbox" sends. remove_inbox_tags: ?bool = null, set_permissions: ?SetPermissions = null, }; pub const MetadataEntry = struct { namespace: []const u8 = "", prefix: []const u8 = "", key: []const u8 = "", value: []const u8 = "", }; /// What Paperless knows about a document's files, as opposed to about the /// document. pub const Metadata = struct { original_checksum: []const u8 = "", original_size: u64 = 0, original_mime_type: []const u8 = "", media_filename: []const u8 = "", has_archive_version: bool = false, original_metadata: []const MetadataEntry = &.{}, archive_checksum: ?[]const u8 = null, archive_media_filename: ?[]const u8 = null, original_filename: []const u8 = "", archive_size: ?u64 = null, archive_metadata: ?[]const MetadataEntry = null, /// The language the classifier guessed, as a two-letter code. lang: []const u8 = "", }; /// What the classifier would assign to a document. pub const Suggestions = struct { correspondents: []const Id = &.{}, tags: []const Id = &.{}, document_types: []const Id = &.{}, storage_paths: []const Id = &.{}, /// Dates found in the content, `YYYY-MM-DD`. dates: []const []const u8 = &.{}, }; pub const TaskStatus = enum { pending, started, success, failure, revoked, unknown, pub fn jsonParse(allocator: Allocator, source: anytype, options: std.json.ParseOptions) !@This() { return json.parseOpenEnum(@This(), allocator, source, options); } /// Whether the task will change no further. pub fn isDone(status: TaskStatus) bool { return switch (status) { .success, .failure, .revoked => true, .pending, .started, .unknown => false, }; } }; pub const TaskType = enum { consume_file, train_classifier, sanity_check, index_optimize, mail_fetch, llm_index, empty_trash, check_workflows, bulk_update, reprocess_document, build_share_link, bulk_delete, apply_ai_suggestions, unknown, pub fn jsonParse(allocator: Allocator, source: anytype, options: std.json.ParseOptions) !@This() { return json.parseOpenEnum(@This(), allocator, source, options); } }; pub const TaskTrigger = enum { scheduled, web_ui, api_upload, folder_consume, email_consume, system, manual, unknown, pub fn jsonParse(allocator: Allocator, source: anytype, options: std.json.ParseOptions) !@This() { return json.parseOpenEnum(@This(), allocator, source, options); } }; /// Something the server is doing, or did, in the background. An upload /// becomes one of these and the document appears only when it has finished. pub const Task = struct { id: Id, /// The Celery identifier, a UUID. What `post_document` hands back. task_id: []const u8 = "", task_type: TaskType = .unknown, task_type_display: []const u8 = "", trigger_source: TaskTrigger = .unknown, trigger_source_display: []const u8 = "", status: TaskStatus = .unknown, status_display: []const u8 = "", date_created: ?[]const u8 = null, date_started: ?[]const u8 = null, date_done: ?[]const u8 = null, duration_seconds: ?f64 = null, wait_time_seconds: ?f64 = null, /// What the task was started with -- for an upload, the filename and the /// overrides that came with it. input_data: std.json.Value = .null, /// What it produced: `{"document_id": N}` for an upload that succeeded, /// `{"error_type": ..., "error_message": ...}` for one that did not. result_data: std.json.Value = .null, related_document_ids: []const Id = &.{}, acknowledged: bool = false, owner: ?Id = null, /// How a finished upload -- a `consume_file` task -- came out. /// /// `status` alone does not say: a refused duplicate is a `failure` with /// no error message, and an upload a plugin stopped on purpose (a barcode /// that split the file, the first half of a double-sided scan) is a /// `success` that made no document. Paperless says which in /// `result_data`, and this reads it. pub const Outcome = union(enum) { /// Not finished yet. pending, /// A new document. created: Id, /// Refused because Paperless already has this file -- with /// `PAPERLESS_CONSUMER_DELETE_DUPLICATES` set. (Without it, a /// duplicate is consumed again and is `created`, with the original /// in the new document's `duplicate_documents`.) duplicate: struct { /// The document that already has it. of: Id, /// Whether that document is in the trash. in_trash: bool, }, /// A plugin ended the task on purpose, and no document was made here. stopped: []const u8, /// It went wrong. failed: struct { /// The Python exception's name, `ConsumerError`. error_type: ?[]const u8, message: ?[]const u8, }, /// Finished, with a result this does not recognise -- a newer /// server, or a task that is not an upload. other, /// One line a person can read: what the command-line tool prints. pub fn format(o: Outcome, w: *std.Io.Writer) std.Io.Writer.Error!void { switch (o) { .pending => try w.writeAll("not finished"), .created => |id| try w.print("document {d}", .{id}), .duplicate => |d| try w.print("already in Paperless as document {d}{s}", .{ d.of, if (d.in_trash) ", which is in the trash" else "", }), .stopped => |reason| try w.print("stopped: {s}", .{reason}), .failed => |f| try w.print("failed: {s}", .{f.message orelse f.error_type orelse "no reason given"}), .other => try w.writeAll("finished"), } } }; pub fn outcome(task: Task) Outcome { if (!task.status.isDone()) return .pending; const data = switch (task.result_data) { .object => |o| o, else => return if (task.status == .success) .other else .{ .failed = .{ .error_type = null, .message = null } }, }; if (integer(data.get("duplicate_of"))) |of| { const in_trash = if (data.get("duplicate_in_trash")) |v| v == .bool and v.bool else false; return .{ .duplicate = .{ .of = of, .in_trash = in_trash } }; } if (integer(data.get("document_id"))) |id| return .{ .created = id }; if (string(data.get("reason"))) |reason| return .{ .stopped = reason }; if (task.status != .success) return .{ .failed = .{ .error_type = string(data.get("error_type")), .message = string(data.get("error_message")), } }; return .other; } /// The document a finished upload made, or null if it made none -- /// including a refused duplicate, whose `related_document_ids` names the /// document that was already there rather than a new one. pub fn documentId(task: Task) ?Id { return switch (task.outcome()) { .created => |id| id, else => null, }; } /// Why a failed task failed, as the server put it. Null for a refused /// duplicate, which carries no message; `outcome` says what happened in /// every case. pub fn errorMessage(task: Task) ?[]const u8 { return switch (task.outcome()) { .failed => |f| f.message, else => null, }; } fn integer(value: ?std.json.Value) ?Id { return switch (value orelse return null) { .integer => |i| if (i >= 0) @intCast(i) else null, else => null, }; } fn string(value: ?std.json.Value) ?[]const u8 { return switch (value orelse return null) { .string => |s| s, else => null, }; } }; /// One condition in a saved view's filter. `rule_type` is one of the fifty /// or so filters the web interface knows -- 0 is "title contains", 6 is /// "has tag" -- and is left as a number, since naming them all here would /// be a table to keep in step with the web interface's source. pub const FilterRule = struct { rule_type: u32, value: ?[]const u8 = null, }; pub const SavedView = struct { id: Id, name: []const u8 = "", icon: ?[]const u8 = null, sort_field: ?[]const u8 = null, sort_reverse: bool = false, filter_rules: []const FilterRule = &.{}, page_size: ?u64 = null, display_mode: ?[]const u8 = null, display_fields: ?[]const []const u8 = null, owner: ?Id = null, permissions: ?Permissions = null, user_can_change: ?bool = null, }; pub const SavedViewFields = struct { name: ?[]const u8 = null, icon: ?[]const u8 = null, sort_field: ?Clearable([]const u8) = null, sort_reverse: ?bool = null, filter_rules: ?[]const FilterRule = null, page_size: ?Clearable(u64) = null, display_mode: ?Clearable([]const u8) = null, display_fields: ?Clearable([]const []const u8) = null, owner: ?Clearable(Id) = null, set_permissions: ?SetPermissions = null, }; pub const FileVersion = enum { archive, original }; /// A link that hands a document to anybody who has it, with no login. pub const ShareLink = struct { id: Id, created: ?[]const u8 = null, expiration: ?[]const u8 = null, /// The last part of `/share/`. slug: []const u8 = "", document: ?Id = null, document_title: []const u8 = "", file_version: ?FileVersion = null, }; pub const ShareLinkFields = struct { document: ?Id = null, /// When the link stops working, as an ISO 8601 date-time. Null never. expiration: ?Clearable([]const u8) = null, file_version: ?FileVersion = null, }; pub const User = struct { id: Id, username: []const u8 = "", email: []const u8 = "", first_name: []const u8 = "", last_name: []const u8 = "", date_joined: ?[]const u8 = null, is_staff: bool = false, is_active: bool = false, is_superuser: bool = false, groups: []const Id = &.{}, user_permissions: []const []const u8 = &.{}, inherited_permissions: []const []const u8 = &.{}, is_mfa_enabled: bool = false, }; pub const UserFields = struct { username: ?[]const u8 = null, email: ?[]const u8 = null, /// Write only; the server never sends a password back. password: ?[]const u8 = null, first_name: ?[]const u8 = null, last_name: ?[]const u8 = null, is_staff: ?bool = null, is_active: ?bool = null, is_superuser: ?bool = null, groups: ?[]const Id = null, user_permissions: ?[]const []const u8 = null, }; pub const Group = struct { id: Id, name: []const u8 = "", /// Django permission codenames, `documents.view_document`. permissions: []const []const u8 = &.{}, }; pub const GroupFields = struct { name: ?[]const u8 = null, permissions: ?[]const []const u8 = null, }; pub const Statistics = struct { documents_total: u64 = 0, /// Documents carrying an inbox tag; null when there is no inbox tag. documents_inbox: ?u64 = null, inbox_tag: ?Id = null, inbox_tags: ?[]const Id = null, document_file_type_counts: []const FileTypeCount = &.{}, character_count: u64 = 0, tag_count: u64 = 0, correspondent_count: u64 = 0, document_type_count: u64 = 0, storage_path_count: u64 = 0, /// The highest archive serial number in use. current_asn: u64 = 0, pub const FileTypeCount = struct { mime_type: []const u8 = "", mime_type_count: u64 = 0, }; }; pub const RemoteVersion = struct { /// The newest release, `v3.2.1`. version: []const u8 = "", update_available: bool = false, }; /// The server's own account of its health: what the system status dialog /// in the web interface shows. pub const SystemStatus = struct { pngx_version: []const u8 = "", server_os: []const u8 = "", install_type: []const u8 = "", storage: ?Storage = null, database: ?Database = null, tasks: ?Tasks = null, pub const Storage = struct { total: u64 = 0, available: u64 = 0, }; pub const Database = struct { type: []const u8 = "", url: []const u8 = "", status: []const u8 = "", @"error": ?[]const u8 = null, }; pub const Tasks = struct { redis_status: []const u8 = "", redis_error: ?[]const u8 = null, celery_status: []const u8 = "", celery_error: ?[]const u8 = null, index_status: []const u8 = "", index_last_modified: ?[]const u8 = null, index_error: ?[]const u8 = null, classifier_status: []const u8 = "", classifier_last_trained: ?[]const u8 = null, classifier_error: ?[]const u8 = null, sanity_check_status: []const u8 = "", sanity_check_last_run: ?[]const u8 = null, sanity_check_error: ?[]const u8 = null, }; }; /// What the search box in the web interface searches: everything at once, /// a few of each, by name. pub const GlobalSearch = struct { total: u64 = 0, documents: []const Document = &.{}, saved_views: []const SavedView = &.{}, tags: []const Tag = &.{}, correspondents: []const Correspondent = &.{}, document_types: []const DocumentType = &.{}, storage_paths: []const StoragePath = &.{}, users: []const User = &.{}, groups: []const Group = &.{}, custom_fields: []const CustomField = &.{}, }; test "a document trimmed by ?fields= still parses" { const parsed = try std.json.parseFromSlice( Document, std.testing.allocator, \\{"id":7,"title":"Hello","permissions":{"view":{"users":[2],"groups":[]},"change":{"users":[],"groups":[]}}} , json.parse_options, ); defer parsed.deinit(); try std.testing.expectEqual(7, parsed.value.id); try std.testing.expectEqualStrings("Hello", parsed.value.title); try std.testing.expectEqualSlices(Id, &.{2}, parsed.value.permissions.?.view.users); try std.testing.expectEqual(null, parsed.value.user_can_change); } test "a task names the document an upload made" { const parsed = try std.json.parseFromSlice( Task, std.testing.allocator, \\{"id":2,"task_id":"737bdbcd-bd4b-42a2-9951-f0fcef1774c9","task_type":"consume_file", \\ "trigger_source":"api_upload","status":"success","result_data":{"document_id":1}, \\ "related_document_ids":[],"acknowledged":false,"owner":1,"brand_new_field":[1,2,3]} , json.parse_options, ); defer parsed.deinit(); try std.testing.expectEqual(TaskStatus.success, parsed.value.status); try std.testing.expect(parsed.value.status.isDone()); try std.testing.expectEqual(1, parsed.value.documentId().?); try std.testing.expectEqual(null, parsed.value.errorMessage()); } fn expectOutcome(comptime task_json: []const u8, expected: []const u8) !void { const parsed = try std.json.parseFromSlice(Task, std.testing.allocator, task_json, json.parse_options); defer parsed.deinit(); var buffer: [128]u8 = undefined; try std.testing.expectEqualStrings(expected, try std.fmt.bufPrint(&buffer, "{f}", .{parsed.value.outcome()})); } test "every way an upload can end, as Paperless 3.2 records it" { // A refused duplicate: a failure with no message, whose related // document is the one that was already there. try expectOutcome( \\{"id":13046,"task_type":"consume_file","status":"failure", \\ "result_data":{"duplicate_of":3011,"duplicate_in_trash":false},"related_document_ids":[3011]} , "already in Paperless as document 3011"); try expectOutcome( \\{"id":1,"status":"failure","result_data":{"duplicate_of":5,"duplicate_in_trash":true},"related_document_ids":[5]} , "already in Paperless as document 5, which is in the trash"); // Stopped by a plugin: a success with no document. try expectOutcome( \\{"id":2,"status":"success","result_data":{"reason":"Barcode splitting complete!"}} , "stopped: Barcode splitting complete!"); try expectOutcome( \\{"id":3,"status":"failure","result_data":{"error_type":"ConsumerError","error_message":"x.pdf: cannot open resource"}} , "failed: x.pdf: cannot open resource"); try expectOutcome( \\{"id":4,"status":"success","result_data":{"document_id":9},"related_document_ids":[9]} , "document 9"); try expectOutcome( \\{"id":5,"status":"started","result_data":null} , "not finished"); const duplicate = try std.json.parseFromSlice(Task, std.testing.allocator, \\{"id":13046,"status":"failure","result_data":{"duplicate_of":3011,"duplicate_in_trash":false},"related_document_ids":[3011]} , json.parse_options); defer duplicate.deinit(); try std.testing.expectEqual(null, duplicate.value.documentId()); try std.testing.expectEqual(null, duplicate.value.errorMessage()); try std.testing.expectEqual(3011, duplicate.value.outcome().duplicate.of); } test "a task type from a newer server is unknown rather than an error" { const parsed = try std.json.parseFromSlice(Task, std.testing.allocator, \\{"id":3,"task_type":"teleport_document","status":"failure","result_data":{"error_message":"no"}} , json.parse_options); defer parsed.deinit(); try std.testing.expectEqual(TaskType.unknown, parsed.value.task_type); try std.testing.expectEqualStrings("no", parsed.value.errorMessage().?); } test "a matching algorithm goes out as a number" { var out: std.Io.Writer.Allocating = .init(std.testing.allocator); defer out.deinit(); try std.json.Stringify.value(TagFields{ .name = "x", .matching_algorithm = .regex, .parent = .clear, }, json.stringify_options, &out.writer); try std.testing.expectEqualStrings( \\{"name":"x","matching_algorithm":4,"parent":null} , out.written()); }