atproto utils for zig zat.dev
atproto sdk zig

user story: coral #4

closed opened by zat.dev

how coral uses zat today

Coral's backend is a zig HTTP + WebSocket server that ingests named entities from a python NER bridge and maintains a co-occurrence graph. The bridge POSTs JSON payloads with entities, DIDs, firehose metrics, and jetstream state. A separate endpoint accepts LLM-curated groups.

What we just wired up:

┌────────────────┬──────────────────────────────────┬───────────────────────────────────────┐ │ zat API │ where in coral │ what it replaced │ ├────────────────┼──────────────────────────────────┼───────────────────────────────────────┤ │ json.getString │ http.zig:199,211,234,237,331,362 │ obj.get("key") + .string type check │ ├────────────────┼──────────────────────────────────┼───────────────────────────────────────┤ │ json.getFloat │ http.zig:206 │ .float / .integer branch │ ├────────────────┼──────────────────────────────────┼───────────────────────────────────────┤ │ json.getBool │ http.zig:222 │ .bool type check │ ├────────────────┼──────────────────────────────────┼───────────────────────────────────────┤ │ json.getArray │ http.zig:192,319,341 │ .array type check │ ├────────────────┼──────────────────────────────────┼───────────────────────────────────────┤ │ Did.parse │ http.zig:200 │ raw string acceptance (no validation) │ └────────────────┴──────────────────────────────────┴───────────────────────────────────────┘

Net result: -23 lines, eliminated every parsed.value.object.get() + type-check pattern. The DID validation is the only behavioral change — malformed DIDs from the bridge are now silently dropped instead of hashed.

what coral does NOT use from zat

Most of zat's surface area is untouched:

  • streaming clients (JetstreamClient, FirehoseClient) — coral's python NER bridge handles jetstream consumption directly via the jetstream websocket. the zig backend is purely a graph server, not a firehose consumer.
  • identity resolution (DidResolver, HandleResolver, DidDocument) — coral only needs DID hashes for user dedup, not resolution to PDS endpoints or signing keys.
  • xrpc client — coral doesn't call any atproto APIs.
  • repo/crypto (CBOR, CAR, MST, multibase, multicodec, JWT, repo verification) — coral operates on extracted entity text, not raw atproto records.
  • syntax types beyond Did (Tid, Handle, Nsid, Rkey, AtUri) — the bridge sends plain strings, not AT URIs.
  • json.extractAt / extractAtOptional — the comptime struct extraction. coral's JSON shapes are flat enough that getString/getArray cover it.

what zat could provide that would help coral

  1. json response writer helpers

This is the big one. Coral spends more code writing JSON than reading it. handleStats (http.zig:87-157) is 70 lines of manual try w.print(""key":{d},", .{val}) string assembly. entity_graph.zig:toJson() and toDiagnosticsJson() are another ~200 lines of the same.

Coral already uses std.json.Stringify in some places (entities.zig, entity_graph.zig:toJson), so this isn't a gap in std — it's more that the manual bufPrint approach crept in for performance-sensitive paths where Stringify felt heavy. A lightweight "write a field" helper in zat wouldn't add much over what std already provides here. Verdict: not a zat concern.

  1. json.getString on raw values (not just paths)

When iterating an array of raw strings like ["entity1", "entity2"], getString(ent_val, "") doesn't work because getPath with "" tries to look up an empty key in an object. Coral falls back to if (ent_val != .string) continue; ... ent_val.string for these cases (http.zig:344).

A small addition to zat:

/// unwrap a value as a string (no path navigation) pub fn asString(value: std.json.Value) ?[]const u8 { return switch (value) { .string => |s| s, else => null, }; }

/// similarly: asFloat, asBool, asInt

This would let coral write zat.json.asString(ent_val) orelse continue instead of the manual type check. Small but it's the one spot where the zat helpers fell short.

  1. Did.hash() convenience

Coral does Did.parse(str) purely to validate, then throws away the parsed result and hashes the raw string separately with Wyhash (http.zig:200-202). A Did.hash() or even just making it easy to go parse → use .raw for hashing could be slightly cleaner, but honestly Did.parse returning the struct with .raw already works — coral just doesn't use it. Verdict: not needed, coral's pattern is fine.

  1. nothing else, really

Coral is a deliberately simple graph server. It doesn't do identity resolution, repo verification, record parsing, or any XRPC calls. The python bridge handles all the atproto-aware work. Zat's streaming clients (JetstreamClient) could theoretically replace the python bridge's jetstream consumer, but that would be a much larger architectural change (replacing spaCy NER with a zig-native solution).

summary

Actionable for zat: add asString / asFloat / asBool / asInt / asArray value unwrappers (no path navigation) alongside the existing getString / getFloat / etc. path-based helpers. This covers the "iterating an array of leaf values" case that getString(val, "") doesn't handle.

Everything else is a good fit. The json path helpers and Did.parse are the right level of abstraction for coral's needs. Coral doesn't need more from zat — it needs zat to stay small and focused on what it does well.

sign up or login to add to the discussion
Labels

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:plc:mkqt76xvfgxuemlwlx6ruc3w/sh.tangled.repo.issue/3mft6oketmc22