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.
7.0 kB · 175 lines
Zig
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! The objects that are only ever listed, read, created, changed and//! deleted: tags, correspondents, document types, storage paths, custom//! fields, saved views, share links, users and groups.//!//! Each is the same five calls on a different path with a different model,//! so each is one instance of `Resource`. Documents and tasks do more than//! that and have files of their own.
const std = @import("std");const Allocator = std.mem.Allocator;
const Client = @import("Client.zig");const models = @import("models.zig");const query = @import("query.zig");
const Id = models.Id;const Parsed = std.json.Parsed;
/// The filters every one of these accepts, or nearly: by name, by/// identifier, and the page.pub const NamedQuery = struct { page: ?u32 = null, /// Up to 100000. Every list is paginated, and one very large page is /// the simplest way to have all of something. page_size: ?u32 = null, /// A field name, `-` before it to reverse: `name`, `-document_count`. ordering: ?[]const u8 = null, id__in: ?[]const Id = null, name__icontains: ?[]const u8 = null, name__iexact: ?[]const u8 = null, name__istartswith: ?[]const u8 = null, name__iendswith: ?[]const u8 = null, /// `permissions` in each result instead of `user_can_change`. full_perms: ?bool = null, /// Any other filter, as `name=value`. extra: []const query.Param = &.{},};
pub const TagQuery = struct { page: ?u32 = null, page_size: ?u32 = null, ordering: ?[]const u8 = null, id__in: ?[]const Id = null, name__icontains: ?[]const u8 = null, name__iexact: ?[]const u8 = null, name__istartswith: ?[]const u8 = null, name__iendswith: ?[]const u8 = null, /// Only the tags with no parent. is_root: ?bool = null, full_perms: ?bool = null, extra: []const query.Param = &.{},};
pub const PageQuery = struct { page: ?u32 = null, page_size: ?u32 = null, ordering: ?[]const u8 = null, extra: []const query.Param = &.{},};
pub const Tags = Resource(models.Tag, models.TagFields, TagQuery, "tags/");pub const Correspondents = Resource(models.Correspondent, models.CorrespondentFields, NamedQuery, "correspondents/");pub const DocumentTypes = Resource(models.DocumentType, models.DocumentTypeFields, NamedQuery, "document_types/");pub const StoragePaths = Resource(models.StoragePath, models.StoragePathFields, NamedQuery, "storage_paths/");pub const CustomFields = Resource(models.CustomField, models.CustomFieldFields, NamedQuery, "custom_fields/");pub const SavedViews = Resource(models.SavedView, models.SavedViewFields, PageQuery, "saved_views/");pub const ShareLinks = Resource(models.ShareLink, models.ShareLinkFields, PageQuery, "share_links/");pub const Users = Resource(models.User, models.UserFields, PageQuery, "users/");pub const Groups = Resource(models.Group, models.GroupFields, PageQuery, "groups/");
/// List, get, create, update and delete, for one kind of object.pub fn Resource( comptime Model: type, comptime Fields: type, comptime Query: type, comptime path: []const u8,) type { return struct { client: *Client,
const Self = @This();
pub const Object = Model; pub const ObjectFields = Fields; pub const ListQuery = Query;
/// One page. pub fn list(self: Self, q: Query) Client.Error!Parsed(models.Page(Model)) { return self.client.getJson(models.Page(Model), path, q); }
/// Every page, one at a time. See `Pager`. pub fn pages(self: Self, q: Query) Pager(Model, Query) { return .{ .client = self.client, .path = path, .query = q }; }
pub fn get(self: Self, id: Id) Client.Error!Parsed(Model) { var buffer: [path.len + 24]u8 = undefined; return self.client.getJson(Model, objectPath(&buffer, id), .{}); }
/// The first object whose name is exactly `name`, ignoring case, or /// null. Names are unique per owner, so with several owners there /// may be more than one; this is the one the server lists first. pub fn findByName(self: Self, name: []const u8) Client.Error!?Parsed(Model) { if (!@hasField(Query, "name__iexact")) @compileError(path ++ " has no name filter"); var page = try self.list(.{ .name__iexact = name, .page_size = 1 }); if (page.value.results.len == 0) { page.deinit(); return null; } return .{ .arena = page.arena, .value = page.value.results[0] }; }
pub fn create(self: Self, fields: Fields) Client.Error!Parsed(Model) { return self.client.sendJson(Model, .post, path, .{}, fields); }
/// Changes the fields that are set in `fields` and leaves the rest /// alone (a `PATCH`). pub fn update(self: Self, id: Id, fields: Fields) Client.Error!Parsed(Model) { var buffer: [path.len + 24]u8 = undefined; return self.client.sendJson(Model, .patch, objectPath(&buffer, id), .{}, fields); }
pub fn delete(self: Self, id: Id) Client.Error!void { var buffer: [path.len + 24]u8 = undefined; return self.client.sendJsonIgnore(.delete, objectPath(&buffer, id), .{}, null); }
fn objectPath(buffer: []u8, id: Id) []const u8 { return std.fmt.bufPrint(buffer, path ++ "{d}/", .{id}) catch unreachable; } };}
/// Walks a list a page at a time. Each page is its own `Parsed`, freed by/// the caller, so that walking ten thousand documents holds one page of them/// at once rather than all of them.////// ```zig/// var pager = client.tags().pages(.{});/// while (try pager.next()) |page| {/// defer page.deinit();/// for (page.value.results) |tag| .../// }/// ```////// Pages are asked for by number rather than by following the `next` link/// the server sends, because that link is built from the request as the/// server saw it, and behind a proxy that can be a scheme, host or port the/// client cannot reach. A list that changes during the walk can repeat or/// skip an object at a page boundary, as it would in the web interface.pub fn Pager(comptime Model: type, comptime Query: type) type { return struct { client: *Client, path: []const u8, query: Query, page: u32 = 1, done: bool = false,
pub fn next(self: *@This()) Client.Error!?Parsed(models.Page(Model)) { if (self.done) return null; var q = self.query; q.page = self.page; const result = try self.client.getJson(models.Page(Model), self.path, q); self.page += 1; if (result.value.next == null) self.done = true; return result; } };}