No description
  • Zig 72.9%
  • Nix 27.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Jeffrey C. Ollie 5b7a294436
All checks were successful
test / test (push) Successful in 5m19s
Regenerate build.zig.zon.nix with the current zon2nix
It names Zig 0.17.0's reason for forking packages with .path
dependencies, where it named only 0.16.0's.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011iuHq9KGXBSt6R4LduFwfp
2026-10-10 11:07:25 -05:00
.forgejo/workflows Package zfj with Nix, building offline through zon2nix 2026-10-10 10:57:06 -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 Upgrade to Zig 0.17 2026-10-10 10:51:09 -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 Upgrade to Zig 0.17 2026-10-10 10:51:09 -05:00
build.zig.zon.nix Regenerate build.zig.zon.nix with the current zon2nix 2026-10-10 11:07:25 -05:00
flake.lock Regenerate build.zig.zon.nix with the current zon2nix 2026-10-10 11:07:25 -05:00
flake.nix Package zfj with Nix, building offline through zon2nix 2026-10-10 10:57:06 -05:00
package.nix Package zfj with Nix, building offline through zon2nix 2026-10-10 10:57:06 -05:00
README.md Package zfj with Nix, building offline through zon2nix 2026-10-10 10:57:06 -05:00
REUSE.toml Update openapi2zig and package its dependencies for Nix 2026-09-12 11:31:25 -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.17. The last release for Zig 0.16 is v0.1.0, and the zig-0.16 branch carries it.

The bindings are generated, not written

This repository holds the API description — api/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.jcollie.dev/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, init.environ_map.get("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.

Where this lives

The repository's home is git.jcollie.dev, with a mirror on Tangled at https://tangled.org/jcollie.dev/zfj:

$ git clone https://git.jcollie.dev/jeff/zfj.git

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.

Building with Nix

$ nix build .#zfj

builds the bindings and runs the tests without touching the network, leaving the generated api.zig in result/. The Zig dependencies come from build.zig.zon.nix, which zon2nix generates from build.zig.zon; regenerate it whenever a dependency changes:

$ nix develop -c zon2nix --17 --nix=build.zig.zon.nix build.zig.zon

The same dependencies are exposed on their own as .#zig-deps, for handing to zig build --system. openapi2zig declares a .path dependency of its own, which --system cannot build from the store, so package.nix forks it from a copy inside the build root, as the zon2nix README describes.

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.