- Zig 88.9%
- Nix 11.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_014dxifDbFdBuNHaR7Xu9cj2 |
||
| api | ||
| LICENSES | ||
| src | ||
| .gitignore | ||
| build.zig | ||
| build.zig.zon | ||
| flake.lock | ||
| flake.nix | ||
| README.md | ||
| REUSE.toml | ||
zig-informacast
Zig bindings for the Singlewire InformaCast API, generated from the OpenAPI specification Singlewire publishes.
Nothing here is written by hand. api/api.json is Singlewire's own schema, and
the Zig is produced from it at build time, so the bindings track whatever
version of the schema is checked in rather than drifting from it.
What is generated
The checked-in schema is the InformaCast Mobile API Explorer, v1 (OpenAPI 3.1): 426 paths and 615 component schemas, which come out as about 1400 structs, 147 enums and 1974 functions.
For each operation the generator emits three functions. Taking getSupervisor
as the example:
| Function | Returns | Use it when |
|---|---|---|
getSupervisor |
Owned(@"supervisors.response") |
You want the parsed body and are happy for a non-2xx status to be error.ResponseError. |
getSupervisorResult |
ApiResult(@"supervisors.response") |
You need to tell an API error from a parse error, and to read the error body. |
getSupervisorRaw |
RawResponse |
You want the status and bytes and will do your own parsing. |
Query and path parameters arrive as one options struct — required ones are plain fields, optional ones default to null.
Owned(T) holds the response body, the std.json.Parsed(T) that borrows from
it, and the allocator; call deinit() and the pair goes away together.
ApiResult(T) is a tagged union of ok, api_error and parse_error with a
deinit() that frees whichever arm is live.
Many of Singlewire's type names are not Zig identifiers — they are written
supervisors.response, _incident-plans.base and so on — so they appear
escaped, as @"supervisors.response". That is a spelling, not a different
kind of type.
Using it
const std = @import("std");
const informacast = @import("informacast");
pub fn main(init: std.process.Init) !void {
const token = init.environ_map.get("INFORMACAST_TOKEN").?;
var ic: informacast.Client = .init(init.gpa, init.io, token);
defer ic.deinit();
var supervisor = try informacast.getSupervisor(&ic, .{
.supervisorId = "0a466b30-7670-11e5-ba6e-765ae9d26291",
.connectionStatus = .CONNECTED,
.includeLatestState = true,
});
defer supervisor.deinit();
std.debug.print("{s}\n", .{supervisor.value().name orelse "(unnamed)"});
}
Client.init takes an allocator, an std.Io and the token. The base URL
defaults to https://api.icmobile.singlewire.com/api, the server the schema
declares, and withBaseUrl overrides it.
Authentication
The schema declares bearer_auth as an HTTP bearer scheme, so the client adds
the Bearer prefix itself — pass the bare token, not a whole header value.
An empty token suppresses the Authorization header entirely.
The schema also declares an OAuth 2.0 authorization-code flow with PKCE
(icmobile_auth). None of that is generated: getting a token is the caller's
problem, and this client only knows how to present one.
The Client struct also carries organization and project fields that emit
OpenAI-Organization and OpenAI-Project headers. InformaCast has no use for
either — they are the generator's own furniture, and leaving them null is
correct.
Fields with a fixed set of values
Where the schema gives a fixed set of choices, the generated type is a Zig enum, so a wrong value is a compile error rather than a rejected request:
pub const ConnectionStatusEnum = enum { CONNECTED, DISCONNECTED, UNKNOWN };
The tag is the wire value verbatim, escaped where it is not a bare identifier,
so nothing is lost in translation. Two kinds of choice list stay []const u8,
because neither can use its values as tag names: one containing the empty
string, since @"" is not a legal Zig identifier, and one whose values are
not strings.
How the generation works
build.zig builds src/generate.zig into a small executable, feeds
api/api.json to it on standard input, and captures its standard output as
api.zig. That captured file — never written into the source tree — is the
root source of the informacast module.
The generator is a thin driver around
openapi2zig, asking it for
code with parameters_as_struct and generate_enums on.
The dependency points at a fork
rather than at upstream, for changes this schema needs. The one it cannot do
without: 31 of its 615 components are written as a bare $ref naming a
"merged" definition next door, and upstream emits nothing at all for such a
component while every use of the name remains — 214 references to types
nothing declared. The fork emits an alias:
pub const @"_notifications.response" = @"_notifications.merged-response";
The fork also generates enums, maps array query parameters to slices rather
than to a single string, and keeps a one-member allOf wrapping a $ref as a
reference instead of flattening it into a copy.
Upgrading to a newer schema is a matter of replacing api/api.json.
Building
nix develop # Zig 0.17 and reuse
zig build # generates api.zig and compiles all of it
zig build test # the same, as a test
Every declaration in the generated module is referenced and compiled, since Zig only analyzes what something uses and a broken corner of the bindings would otherwise go unnoticed until a dependent reached it.
Nothing is installed: the bindings are the informacast module, for another
project to import. Add it as a dependency with
zig fetch --save git+https://git.jcollie.dev/jeff/zig-informacast.git
and import it in that project's build.zig:
const informacast = b.dependency("informacast", .{
.target = target,
.optimize = optimize,
});
exe.root_module.addImport("informacast", informacast.module("informacast"));
Generation takes the better part of a minute: the schema is 3.5 MB and the Zig that comes out of it is 2.6 MB.
Where this lives
git clone https://git.jcollie.dev/jeff/zig-informacast.git
It is mirrored on Tangled at
https://tangled.org/jcollie.dev/zig-informacast
It is also on Radicle, where its Repository ID is
rad:z2sWLU3ghuSwq2UT5jMJcDUE9bPAL
and that ID is the only way to find it, since Radicle has no central index to search:
rad clone rad:z2sWLU3ghuSwq2UT5jMJcDUE9bPAL
Licensing
This project follows the REUSE standard, and
nix develop -c reuse lint passes.
The code here is MIT. api/api.json is Singlewire's schema, redistributed as
received; it states no license of its own, so it is marked
LicenseRef-Singlewire-API-Spec, whose text in LICENSES/ records that
absence rather than granting anything. Read
Singlewire's terms before redistributing
it.