No description
  • Zig 95.8%
  • Nix 4.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Jeffrey C. Ollie 48852dc731
All checks were successful
test / test (push) Successful in 2m17s
Add a build step that updates the vendored specification
Moving to a new Forgejo release was a curl piped through a python one-liner
in the README, retyped by hand each time and with nothing checking the
result. It is a build step now:

    zig build update-spec                          # the latest release
    zig build update-spec -Dforgejo-version=v16.0.3

The tag is optional because Forgejo's own API answers which release is the
latest; it is available because Forgejo maintains several release branches
at once, so the newest release is not always a successor to the one being
replaced.

The step reaches the network, so it stands alone and nothing else depends
on it -- an ordinary build never runs it. It is also marked as having side
effects, since what a URL answers is not a function of the command line
and must not be cached on it.

Rendering the template is by substitution of its two Go placeholders, and
both are required to be present: the day Forgejo renames one or adds a
third, this fails rather than vendoring a file with a template expression
in the middle of it. What comes back is then parsed and checked -- that it
is JSON, that it is Swagger 2.0 against /api/v1, that its version is the
release that was asked for, and that it describes any paths at all --
before `addUpdateSourceFiles` is allowed to put it in the source tree.

Checked against v15.0.7 as well as v16.0.3: the specification swaps, the
bindings regenerate, and the tests pass on both, which is also a fair sign
the tests are not quietly pinned to one release's peculiarities.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NghACEDMEDVNfKVf1EGfwH
2026-09-06 12:52:32 -05:00
.forgejo/workflows Generate Forgejo API bindings from the vendored specification 2026-09-06 12:28:33 -05:00
api Generate Forgejo API bindings from the vendored specification 2026-09-06 12:28:33 -05:00
LICENSES Generate Forgejo API bindings from the vendored specification 2026-09-06 12:28:33 -05:00
src Add a build step that updates the vendored specification 2026-09-06 12:52:32 -05:00
.gitignore Generate Forgejo API bindings from the vendored specification 2026-09-06 12:28:33 -05:00
build.zig Add a build step that updates the vendored specification 2026-09-06 12:52:32 -05:00
build.zig.zon Depend on the openapi2zig branch rather than a local checkout 2026-09-06 12:33:53 -05:00
flake.lock init 2026-09-06 11:55:04 -05:00
flake.nix init 2026-09-06 11:55:04 -05:00
README.md Add a build step that updates the vendored specification 2026-09-06 12:52:32 -05:00
REUSE.toml Generate Forgejo API bindings from the vendored specification 2026-09-06 12:28:33 -05:00

zfj

Zig bindings for the Forgejo API. All 506 operations and 246 models of Forgejo v16.0.3, typed, with the response of every call parsed into the model the API documentation names for it.

Requires Zig 0.16.

The bindings are generated, not written

This repository holds the API descriptionapi/swagger.v1.json, taken from the Forgejo v16.0.3 release — and a small program that turns it into Zig. The build runs that program and compiles what it prints; no generated source is checked in, so the bindings cannot drift from the specification they came from.

Updating to a new Forgejo release is therefore a matter of replacing one file. See Updating the specification.

zig build leaves the generated source at zig-out/api.zig, which is the reference for what the library actually exposes. zig build docs renders the same thing as browsable documentation.

Using it

Add the dependency:

zig fetch --save https://git.ocjtech.us/jeff/zfj/archive/main.tar.gz

then wire the module up in build.zig:

const zfj = b.dependency("zfj", .{
    .target = target,
    .optimize = optimize,
});
exe.root_module.addImport("zfj", zfj.module("zfj"));

Operations are grouped the way the Forgejo API documentation groups them, one client per tag — IssueClient, RepositoryClient, UserClient, OrganizationClient, AdminClient, NotificationClient, PackageClient, SettingsClient, MiscellaneousClient, ActivitypubClient:

const std = @import("std");
const zfj = @import("zfj");

pub fn main(init: std.process.Init) !u8 {
    const allocator = init.gpa;

    var client = zfj.Client.init(allocator, init.io, std.posix.getenv("FORGEJO_TOKEN").?);
    defer client.deinit();
    client.withBaseUrl("https://codeberg.org/api/v1");

    var issues = zfj.IssueClient.init(&client);

    var issue = try issues.issueGetIssue(.{
        .owner = "forgejo",
        .repo = "forgejo",
        .index = 42,
    });
    defer issue.deinit();

    std.debug.print("{s}\n", .{issue.value().title.?});
    return 0;
}

Every operation is also a plain function taking the client as its first argument, so the call above can equally be written zfj.issueGetIssue(&client, .{ ... }). The tag clients are a grouping over those functions and nothing more.

Three forms of each operation are generated:

Form Returns For
issueGetIssue Owned(Issue) the usual case: a parsed model, or error.ResponseError
issueGetIssueResult ApiResult(Issue) when the failing status code matters
issueGetIssueRaw RawResponse when the bytes matter

Owned(T) owns both the response body and the parsed model; deinit() frees both, and value() borrows the model out of it. Model fields are optional and default to null, because the API omits rather than nulls what it has no value for.

Operations whose only successful response is empty — the deletes, mostly — return !void.

Authentication

The token passed to Client.init is sent as Authorization: Bearer <token>. Forgejo accepts that spelling as well as the token <token> its documentation describes; both are read by the same code path. Pass an empty string to make requests unauthenticated.

Cloning with Radicle

The repository is also published on the Radicle peer-to-peer network, where a repository is found by its Repository ID rather than by a server name:

rad:z2LTU53rqByAo7pj2BM1vJHeKYJSb

With a local Radicle node running, clone it with:

$ rad clone rad:z2LTU53rqByAo7pj2BM1vJHeKYJSb

That fetches the repository, checks out the default branch, and starts seeding it so other peers can fetch from you. To follow the repository without checking out a working copy, use rad seed instead:

$ rad seed rad:z2LTU53rqByAo7pj2BM1vJHeKYJSb

If you already have a clone from the git remote above, you can attach it to the same Radicle repository rather than cloning again:

$ git remote add rad rad://z2LTU53rqByAo7pj2BM1vJHeKYJSb
$ git fetch rad

Updating the specification

$ zig build update-spec

fetches Forgejo's latest release and rewrites api/swagger.v1.json from it. To take a particular release instead — an older one, or a newer one that is not yet the latest across all of Forgejo's parallel release branches — name its tag:

$ zig build update-spec -Dforgejo-version=v16.0.3

Then run zig build test. Nothing else in the repository names a Forgejo version, so that is the whole of it.

The step is the only thing here that reaches the network, and nothing else depends on it: an ordinary build never runs it. What it fetches is templates/swagger/v1_json.tmpl from the release tag, which is the document an instance renders and serves at /swagger.v1.json. Taking it from the tag rather than from a running instance is what makes the vendored file say which Forgejo it describes — an instance's copy is stamped with whatever build happens to be deployed there.

The template is plain JSON but for two Go placeholders, the instance's version and its sub-URL, and both are required to be present. If a future Forgejo renames one or adds a third, the step fails rather than vendoring a file with a template expression left in the middle of it. What it fetched is also parsed and checked over — that it is JSON, that it is Swagger 2.0, that its version is the release asked for, and that it describes any paths at all — before it is allowed to replace the file in the source tree.

License

MIT, and the repository follows the REUSE specification — reuse lint checks it. api/swagger.v1.json is Forgejo's own, distributed by them under the MIT license for the purpose of interoperability.