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.2 kB · 131 lines
Zig
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Query strings, written from structs.//!//! Paperless filters its lists with django-filter, whose parameters are//! named for a field and a lookup joined by double underscores --//! `title__icontains`, `tags__id__all`. The query structs in this library//! use those names as their field names, so that the Paperless//! documentation, the browsable API and this library all spell a filter the//! same way, and `writeStruct` turns one into a query string without a table//! in between.//!//! A field left `null` is not sent. A list of identifiers is sent as one//! comma-separated value, which is what django-filter's `__in`, `__all` and//! `__none` lookups read. Anything the struct does not name goes in its//! `extra`, as name and value pairs.
const std = @import("std");const Io = std.Io;const testing = std.testing;
const uri = @import("uri");
pub const Param = struct { name: []const u8, value: []const u8,};
/// RFC 3986's unreserved characters and nothing else. Narrower than zig-uri's/// `query_value`, which leaves the sub-delimiters alone: Django reads a query/// string as HTML form data, where `+` is a space, so a `+` that is meant to/// be a plus has to be sent as `%2B`.const safe = uri.percent.charSet(.{ .add = uri.percent.unreserved_punct });
/// Writes one `name=value`, preceded by `?` if it is the first and `&` if it/// is not.pub fn writeParam(w: *Io.Writer, first: *bool, name: []const u8, value: []const u8) Io.Writer.Error!void { try w.writeByte(if (first.*) '?' else '&'); first.* = false; try uri.percent.encode(w, name, &safe); try w.writeByte('='); try uri.percent.encode(w, value, &safe);}
pub fn writeParams(w: *Io.Writer, first: *bool, params: []const Param) Io.Writer.Error!void { for (params) |param| try writeParam(w, first, param.name, param.value);}
/// Writes every non-null field of `value` as a parameter named for it.////// What each type becomes: an integer in decimal; a `bool` as `true` or/// `false`, which django-filter's boolean filter reads; a string as it is;/// an enum as its tag name; a slice of integers as one comma-separated/// value. A field named `extra` of type `[]const Param` is written as the/// parameters it holds.pub fn writeStruct(w: *Io.Writer, first: *bool, value: anytype) Io.Writer.Error!void { const T = @TypeOf(value); const info = @typeInfo(T).@"struct"; inline for (info.field_names, info.field_types) |name, FieldType| { const field = @field(value, name); if (comptime std.mem.eql(u8, name, "extra")) { try writeParams(w, first, field); } else if (@typeInfo(FieldType) == .optional) { if (field) |present| try writeValue(w, first, name, present); } else { try writeValue(w, first, name, field); } }}
fn writeValue(w: *Io.Writer, first: *bool, name: []const u8, value: anytype) Io.Writer.Error!void { const T = @TypeOf(value); switch (@typeInfo(T)) { .int, .comptime_int => { var buffer: [24]u8 = undefined; try writeParam(w, first, name, std.fmt.bufPrint(&buffer, "{d}", .{value}) catch unreachable); }, .bool => try writeParam(w, first, name, if (value) "true" else "false"), .@"enum" => try writeParam(w, first, name, @tagName(value)), .pointer => |pointer| { if (pointer.child == u8) return writeParam(w, first, name, value); // A list of integers, as one comma-separated value. try w.writeByte(if (first.*) '?' else '&'); first.* = false; try uri.percent.encode(w, name, &safe); try w.writeByte('='); for (value, 0..) |item, i| { if (i > 0) try w.writeAll("%2C"); try w.print("{d}", .{item}); } }, else => @compileError("no query string form for " ++ @typeName(T)), }}
test writeStruct { const Query = struct { page: ?u32 = null, title__icontains: ?[]const u8 = null, tags__id__all: ?[]const u64 = null, is_tagged: ?bool = null, ordering: ?[]const u8 = null, extra: []const Param = &.{}, };
var buffer: [256]u8 = undefined; var w: Io.Writer = .fixed(&buffer); var first = true; try writeStruct(&w, &first, Query{ .page = 2, .title__icontains = "a+b & c", .tags__id__all = &.{ 1, 22 }, .is_tagged = false, .extra = &.{.{ .name = "custom_field_query", .value = "[\"x\"]" }}, }); try testing.expectEqualStrings( "?page=2&title__icontains=a%2Bb%20%26%20c&tags__id__all=1%2C22&is_tagged=false" ++ "&custom_field_query=%5B%22x%22%5D", w.buffered(), );}
test "nothing set writes nothing" { var buffer: [16]u8 = undefined; var w: Io.Writer = .fixed(&buffer); var first = true; try writeStruct(&w, &first, struct { page: ?u32 = null }{}); try testing.expectEqualStrings("", w.buffered()); try testing.expect(first);}