atproto pds in zig pds.zat.dev
pds atproto
24

Configure Feed

Select the types of activity you want to include in your feed.

Polish generated API reference

zzstoatzz (Jun 1, 2026, 9:33 PM -0500) 11ac7a9b 4733dc36

+358 -302
+3 -302
src/http/docs.zig
··· 1 1 const std = @import("std"); 2 - const config = @import("../core/config.zig"); 2 + const api_reference = @import("../internal/api_reference/root.zig"); 3 3 const http_api = @import("api.zig"); 4 - const router = @import("router.zig"); 5 4 6 5 const http = std.http; 7 6 8 - const Endpoint = router.Endpoint; 9 - const endpoints = router.endpoints; 10 - 11 - const group_order = [_][]const u8{ 12 - "zds", 13 - "status", 14 - "discovery", 15 - "oauth", 16 - "server", 17 - "account", 18 - "preferences", 19 - "repo", 20 - "sync", 21 - "identity", 22 - }; 23 - 24 7 pub fn serve(request: *http_api.Request) !void { 25 8 var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 26 9 defer arena.deinit(); 27 - const body = try renderHtml(arena.allocator()); 10 + const body = try api_reference.renderHtml(arena.allocator()); 28 11 try http_api.respond(request, .ok, body, &html_headers); 29 12 } 30 13 31 14 pub fn serveOpenApi(request: *http_api.Request) !void { 32 15 var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 33 16 defer arena.deinit(); 34 - const body = try renderOpenApi(arena.allocator()); 17 + const body = try api_reference.renderOpenApi(arena.allocator()); 35 18 try http_api.respond(request, .ok, body, &json_headers); 36 19 } 37 20 38 - fn renderHtml(allocator: std.mem.Allocator) ![]const u8 { 39 - var group_buttons: std.Io.Writer.Allocating = .init(allocator); 40 - defer group_buttons.deinit(); 41 - try group_buttons.writer.print("<button class=\"chip active\" type=\"button\" data-group=\"all\">All <span>{d}</span></button>", .{endpoints.len}); 42 - for (group_order) |group| { 43 - const total = groupCount(group); 44 - if (total == 0) continue; 45 - try group_buttons.writer.print( 46 - "<button class=\"chip\" type=\"button\" data-group={f}>{s} <span>{d}</span></button>", 47 - .{ std.json.fmt(group, .{}), try escapeHtml(allocator, group), total }, 48 - ); 49 - } 50 - 51 - var cards: std.Io.Writer.Allocating = .init(allocator); 52 - defer cards.deinit(); 53 - for (endpoints, 0..) |endpoint, i| { 54 - try renderEndpointCard(allocator, &cards.writer, endpoint, i == 0); 55 - } 56 - 57 - const public_url = config.publicUrl(); 58 - return std.fmt.allocPrint(allocator, 59 - \\<!doctype html> 60 - \\<html lang="en"> 61 - \\<head> 62 - \\ <meta charset="utf-8"> 63 - \\ <meta name="viewport" content="width=device-width, initial-scale=1"> 64 - \\ <meta name="color-scheme" content="dark light"> 65 - \\ <title>zds api</title> 66 - \\ <link rel="canonical" href="{s}/api"> 67 - \\ <link rel="icon" href="/favicon.svg" type="image/svg+xml"> 68 - \\ <style>{s}</style> 69 - \\</head> 70 - \\<body> 71 - \\ <main class="shell"> 72 - \\ <header> 73 - \\ <a class="brand" href="/">zds</a> 74 - \\ <nav aria-label="api links"> 75 - \\ <a href="/api/openapi.json">OpenAPI JSON</a> 76 - \\ <a href="/xrpc/com.atproto.server.describeServer">describeServer</a> 77 - \\ </nav> 78 - \\ </header> 79 - \\ <section class="intro"> 80 - \\ <p class="eyebrow">endpoint reference</p> 81 - \\ <h1>{s}</h1> 82 - \\ <p class="lede">This PDS exposes {d} documented HTTP operations, including XRPC methods at <code>/xrpc/{{NSID}}</code>.</p> 83 - \\ <div class="stats" aria-label="endpoint summary"> 84 - \\ <span><strong>{d}</strong> operations</span> 85 - \\ <span><strong>{d}</strong> groups</span> 86 - \\ <span><strong>{d}</strong> XRPC methods</span> 87 - \\ </div> 88 - \\ </section> 89 - \\ <section class="tools" aria-label="filters"> 90 - \\ <label class="search"> 91 - \\ <span>Search</span> 92 - \\ <input id="search" type="search" placeholder="com.atproto.repo.applyWrites, sync, bearer"> 93 - \\ </label> 94 - \\ <div class="chips" id="groups" aria-label="groups">{s}</div> 95 - \\ </section> 96 - \\ <section class="endpoints" id="endpoints" aria-label="endpoints">{s}</section> 97 - \\ </main> 98 - \\ <script>{s}</script> 99 - \\</body> 100 - \\</html> 101 - , .{ 102 - try escapeHtml(allocator, public_url), 103 - css, 104 - try escapeHtml(allocator, public_url), 105 - endpoints.len, 106 - endpoints.len, 107 - groupCountTotal(), 108 - xrpcCount(), 109 - group_buttons.written(), 110 - cards.written(), 111 - script, 112 - }); 113 - } 114 - 115 - fn groupCount(group: []const u8) usize { 116 - var count: usize = 0; 117 - for (endpoints) |endpoint| { 118 - if (std.mem.eql(u8, endpoint.group, group)) count += 1; 119 - } 120 - return count; 121 - } 122 - 123 - fn groupCountTotal() usize { 124 - var count: usize = 0; 125 - for (group_order) |group| { 126 - if (groupCount(group) > 0) count += 1; 127 - } 128 - return count; 129 - } 130 - 131 - fn renderEndpointCard(allocator: std.mem.Allocator, writer: *std.Io.Writer, endpoint: Endpoint, open: bool) !void { 132 - const method_class = try lower(allocator, endpoint.method); 133 - const details_open = if (open) " open" else ""; 134 - const nsid = endpoint.nsid(); 135 - const search_text = try std.fmt.allocPrint(allocator, "{s} {s} {s} {s} {s}", .{ endpoint.method, endpoint.path, endpoint.group, endpoint.auth, endpoint.summary }); 136 - try writer.print( 137 - \\<details class="endpoint" data-group={f} data-method={f} data-search={f}{s}> 138 - \\ <summary><span class="method {s}">{s}</span><code>{s}</code><span class="auth">{s}</span></summary> 139 - \\ <div class="body"> 140 - \\ <p>{s}</p> 141 - , .{ 142 - std.json.fmt(endpoint.group, .{}), 143 - std.json.fmt(endpoint.method, .{}), 144 - std.json.fmt(search_text, .{}), 145 - details_open, 146 - method_class, 147 - try escapeHtml(allocator, endpoint.method), 148 - try escapeHtml(allocator, endpoint.path), 149 - try escapeHtml(allocator, endpoint.auth), 150 - try escapeHtml(allocator, endpoint.summary), 151 - }); 152 - 153 - if (nsid) |id| { 154 - try writer.print("<div class=\"nsid\"><span>NSID</span><code>{s}</code></div>", .{try escapeHtml(allocator, id)}); 155 - } 156 - try writer.writeAll("<div class=\"meta-grid\">"); 157 - try renderList(allocator, writer, "Query", endpoint.params); 158 - try renderList(allocator, writer, "Body", endpoint.body); 159 - try renderList(allocator, writer, "Response", endpoint.response); 160 - try writer.writeAll("</div>"); 161 - if (endpoint.notes.len > 0) { 162 - try writer.print("<p class=\"note\">{s}</p>", .{try escapeHtml(allocator, endpoint.notes)}); 163 - } 164 - try renderCurl(allocator, writer, endpoint); 165 - try writer.writeAll("</div></details>"); 166 - } 167 - 168 - fn renderList(allocator: std.mem.Allocator, writer: *std.Io.Writer, label: []const u8, items: []const []const u8) !void { 169 - try writer.print("<div><h2>{s}</h2>", .{label}); 170 - if (items.len == 0) { 171 - try writer.writeAll("<p class=\"muted\">none documented yet</p></div>"); 172 - return; 173 - } 174 - try writer.writeAll("<ul>"); 175 - for (items) |item| try writer.print("<li><code>{s}</code></li>", .{try escapeHtml(allocator, item)}); 176 - try writer.writeAll("</ul></div>"); 177 - } 178 - 179 - fn renderCurl(allocator: std.mem.Allocator, writer: *std.Io.Writer, endpoint: Endpoint) !void { 180 - const token = if (std.mem.indexOf(u8, endpoint.auth, "bearer") != null or std.mem.indexOf(u8, endpoint.auth, "service") != null) " \\\n -H 'authorization: Bearer $TOKEN'" else ""; 181 - const body = if (std.mem.eql(u8, endpoint.method, "POST")) " \\\n -H 'content-type: application/json' \\\n --data '{}'" else ""; 182 - const public_url = try escapeHtml(allocator, config.publicUrl()); 183 - try writer.print( 184 - "<pre><code>curl -sS -X {s}{s} \"{s}{s}\"{s}</code></pre>", 185 - .{ try escapeHtml(allocator, endpoint.method), token, public_url, try escapeHtml(allocator, endpoint.path), body }, 186 - ); 187 - } 188 - 189 - fn renderOpenApi(allocator: std.mem.Allocator) ![]const u8 { 190 - var out: std.Io.Writer.Allocating = .init(allocator); 191 - defer out.deinit(); 192 - try out.writer.print( 193 - \\{{"openapi":"3.1.0","info":{{"title":"ZDS endpoint reference","version":"0.1.0","description":"Endpoint inventory generated by ZDS."}},"servers":[{{"url":{f}}}],"paths":{{ 194 - , .{std.json.fmt(config.publicUrl(), .{})}); 195 - 196 - var first_path = true; 197 - for (endpoints, 0..) |endpoint, i| { 198 - if (pathSeenBefore(i)) continue; 199 - if (!first_path) try out.writer.writeAll(","); 200 - first_path = false; 201 - try out.writer.print("{f}:{{", .{std.json.fmt(endpoint.path, .{})}); 202 - var first_method = true; 203 - for (endpoints) |method_endpoint| { 204 - if (!std.mem.eql(u8, endpoint.path, method_endpoint.path)) continue; 205 - if (!first_method) try out.writer.writeAll(","); 206 - first_method = false; 207 - try renderOperation(&out.writer, method_endpoint); 208 - } 209 - try out.writer.writeAll("}"); 210 - } 211 - 212 - try out.writer.writeAll("}}\n"); 213 - return out.toOwnedSlice(); 214 - } 215 - 216 - fn renderOperation(writer: *std.Io.Writer, endpoint: Endpoint) !void { 217 - try writer.print( 218 - \\{f}:{{"summary":{f},"tags":[{f}],"description":{f},"responses":{{"200":{{"description":"OK"}}}}}} 219 - , .{ 220 - std.json.fmt(tryLowerStatic(endpoint.method), .{}), 221 - std.json.fmt(endpoint.summary, .{}), 222 - std.json.fmt(endpoint.group, .{}), 223 - std.json.fmt(endpointDescription(endpoint), .{}), 224 - }); 225 - } 226 - 227 - fn endpointDescription(endpoint: Endpoint) []const u8 { 228 - _ = endpoint; 229 - return "Generated from the ZDS endpoint inventory. Request and response schemas are intentionally sparse in this MVP."; 230 - } 231 - 232 - fn pathSeenBefore(index: usize) bool { 233 - for (endpoints[0..index]) |endpoint| { 234 - if (std.mem.eql(u8, endpoint.path, endpoints[index].path)) return true; 235 - } 236 - return false; 237 - } 238 - 239 - fn xrpcCount() usize { 240 - var count: usize = 0; 241 - for (endpoints) |endpoint| { 242 - if (endpoint.nsid() != null) count += 1; 243 - } 244 - return count; 245 - } 246 - 247 - fn lower(allocator: std.mem.Allocator, input: []const u8) ![]const u8 { 248 - const out = try allocator.alloc(u8, input.len); 249 - for (input, 0..) |c, i| out[i] = std.ascii.toLower(c); 250 - return out; 251 - } 252 - 253 - fn tryLowerStatic(input: []const u8) []const u8 { 254 - if (std.mem.eql(u8, input, "GET")) return "get"; 255 - if (std.mem.eql(u8, input, "HEAD")) return "head"; 256 - if (std.mem.eql(u8, input, "POST")) return "post"; 257 - return input; 258 - } 259 - 260 - fn escapeHtml(allocator: std.mem.Allocator, input: []const u8) ![]const u8 { 261 - var out: std.Io.Writer.Allocating = .init(allocator); 262 - defer out.deinit(); 263 - for (input) |c| { 264 - switch (c) { 265 - '&' => try out.writer.writeAll("&amp;"), 266 - '<' => try out.writer.writeAll("&lt;"), 267 - '>' => try out.writer.writeAll("&gt;"), 268 - '"' => try out.writer.writeAll("&quot;"), 269 - '\'' => try out.writer.writeAll("&#39;"), 270 - else => try out.writer.writeByte(c), 271 - } 272 - } 273 - return out.toOwnedSlice(); 274 - } 275 - 276 21 const html_headers = [_]http.Header{ 277 22 .{ .name = "content-type", .value = "text/html; charset=utf-8" }, 278 23 .{ .name = "cache-control", .value = "no-store" }, ··· 288 33 .{ .name = "access-control-allow-private-network", .value = "true" }, 289 34 .{ .name = "connection", .value = "close" }, 290 35 }; 291 - 292 - const css = 293 - \\:root{color-scheme:dark light;--bg:#111411;--panel:#171b18;--text:#eef4ed;--muted:#98a69b;--line:#2d352f;--accent:#7ee787;--cyan:#7dd3fc;--rose:#f0a6ca;--gold:#e6c36a;--shadow:0 14px 38px rgba(0,0,0,.24);font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif} 294 - \\*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text)}a{color:inherit}.shell{width:min(1180px,calc(100% - 32px));margin:0 auto;padding:24px 0 56px}header{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:54px}.brand{font-weight:800;text-decoration:none;font-size:1.25rem}nav{display:flex;gap:10px;flex-wrap:wrap}nav a{border:1px solid var(--line);border-radius:8px;padding:9px 12px;text-decoration:none;color:var(--muted);background:rgba(255,255,255,.02)} 295 - \\.intro{display:grid;gap:14px;margin-bottom:28px}.eyebrow{text-transform:uppercase;letter-spacing:.08em;color:var(--accent);font-size:.78rem;font-weight:800;margin:0}h1{font-size:clamp(2.4rem,8vw,5.8rem);line-height:.9;margin:0;letter-spacing:0}.lede{max-width:760px;color:var(--muted);font-size:1.12rem;line-height:1.6;margin:0}.lede code{color:var(--cyan)}.stats{display:flex;gap:10px;flex-wrap:wrap;margin-top:6px}.stats span{border:1px solid var(--line);border-radius:8px;padding:10px 12px;color:var(--muted);background:rgba(255,255,255,.025)}.stats strong{color:var(--text)} 296 - \\.tools{position:sticky;top:0;z-index:5;background:color-mix(in srgb,var(--bg) 90%,transparent);backdrop-filter:blur(16px);padding:12px 0 16px;margin-bottom:12px;border-bottom:1px solid var(--line)}.search{display:grid;gap:7px;margin-bottom:12px}.search span{font-size:.78rem;text-transform:uppercase;color:var(--muted);font-weight:800}.search input{width:100%;border:1px solid var(--line);border-radius:8px;background:var(--panel);color:var(--text);padding:13px 14px;font:inherit;outline:none}.search input:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(126,231,135,.15)}.chips{display:flex;flex-wrap:wrap;gap:8px}.chip{border:1px solid var(--line);border-radius:999px;background:transparent;color:var(--muted);padding:8px 11px;font:inherit;cursor:pointer}.chip.active{background:var(--text);border-color:var(--text);color:var(--bg)}.chip span{opacity:.72} 297 - \\.endpoints{display:grid;gap:10px}.endpoint{border:1px solid var(--line);border-radius:8px;background:var(--panel);box-shadow:var(--shadow);overflow:hidden}.endpoint[hidden]{display:none}.endpoint summary{list-style:none;display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:12px;align-items:center;padding:15px;cursor:pointer}.endpoint summary::-webkit-details-marker{display:none}.endpoint code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;overflow-wrap:anywhere}.method{font-size:.76rem;font-weight:900;border-radius:6px;padding:5px 7px;color:#071008}.method.get{background:var(--cyan)}.method.post{background:var(--accent)}.method.head{background:var(--gold)}.auth{font-size:.78rem;color:var(--muted);border:1px solid var(--line);border-radius:999px;padding:5px 8px;white-space:nowrap}.body{border-top:1px solid var(--line);padding:16px;display:grid;gap:15px}.body p{margin:0;color:var(--muted);line-height:1.55}.nsid{display:flex;gap:10px;align-items:center;color:var(--muted)}.nsid span{font-size:.75rem;text-transform:uppercase;font-weight:900;color:var(--rose)}.meta-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}.meta-grid>div{border:1px solid var(--line);border-radius:8px;padding:12px;background:rgba(255,255,255,.018)}h2{font-size:.78rem;text-transform:uppercase;color:var(--muted);margin:0 0 8px}ul{margin:0;padding-left:18px}.muted{color:var(--muted)}.note{border-left:3px solid var(--rose);padding-left:12px}pre{margin:0;white-space:pre-wrap;background:#090b09;color:#d6ead8;border-radius:8px;border:1px solid var(--line);padding:12px;overflow:auto} 298 - \\@media (max-width:720px){.shell{width:min(100% - 20px,1180px);padding-top:16px}header{align-items:flex-start;margin-bottom:32px}.endpoint summary{grid-template-columns:auto minmax(0,1fr);}.endpoint summary .auth{grid-column:2}.meta-grid{grid-template-columns:1fr}nav{justify-content:flex-end}} 299 - \\@media (prefers-color-scheme:light){:root{--bg:#f6f7f3;--panel:#ffffff;--text:#111411;--muted:#5b665f;--line:#d9dfd8;--shadow:0 12px 34px rgba(33,45,36,.09)}pre{background:#101510;color:#ecf7ec}} 300 - ; 301 - 302 - const script = 303 - \\const search=document.querySelector("#search"); 304 - \\const chips=[...document.querySelectorAll(".chip")]; 305 - \\const cards=[...document.querySelectorAll(".endpoint")]; 306 - \\let group="all"; 307 - \\function apply(){ 308 - \\ const q=search.value.trim().toLowerCase(); 309 - \\ for(const card of cards){ 310 - \\ const groupMatch=group==="all"||card.dataset.group===group; 311 - \\ const text=(card.dataset.search||"").toLowerCase(); 312 - \\ card.hidden=!(groupMatch&&text.includes(q)); 313 - \\ } 314 - \\} 315 - \\search.addEventListener("input",apply); 316 - \\for(const chip of chips){ 317 - \\ chip.addEventListener("click",()=>{ 318 - \\ group=chip.dataset.group; 319 - \\ for(const other of chips) other.classList.toggle("active",other===chip); 320 - \\ apply(); 321 - \\ }); 322 - \\} 323 - ; 324 - 325 - test "endpoint inventory covers XRPC paths" { 326 - try std.testing.expect(endpoints.len > 50); 327 - try std.testing.expect(xrpcCount() > 30); 328 - for (endpoints) |endpoint| { 329 - if (endpoint.nsid()) |id| { 330 - try std.testing.expect(id.len > 0); 331 - try std.testing.expect(std.mem.indexOfScalar(u8, id, '/') == null); 332 - } 333 - } 334 - }
+204
src/internal/api_reference/html.zig
··· 1 + const std = @import("std"); 2 + const config = @import("../../core/config.zig"); 3 + const model = @import("model.zig"); 4 + 5 + const Endpoint = model.Endpoint; 6 + const endpoints = model.endpoints; 7 + 8 + pub fn render(allocator: std.mem.Allocator) ![]const u8 { 9 + var group_buttons: std.Io.Writer.Allocating = .init(allocator); 10 + defer group_buttons.deinit(); 11 + try group_buttons.writer.print("<button class=\"chip active\" type=\"button\" data-group=\"all\">All <span>{d}</span></button>", .{endpoints.len}); 12 + for (model.group_order) |group| { 13 + const total = model.groupCount(group); 14 + if (total == 0) continue; 15 + try group_buttons.writer.print( 16 + "<button class=\"chip\" type=\"button\" data-group={f}>{s} <span>{d}</span></button>", 17 + .{ std.json.fmt(group, .{}), try model.escapeHtml(allocator, group), total }, 18 + ); 19 + } 20 + 21 + var cards: std.Io.Writer.Allocating = .init(allocator); 22 + defer cards.deinit(); 23 + for (endpoints, 0..) |endpoint, i| { 24 + try renderEndpointCard(allocator, &cards.writer, endpoint, i == 0); 25 + } 26 + 27 + const public_url = config.publicUrl(); 28 + return std.fmt.allocPrint(allocator, 29 + \\<!doctype html> 30 + \\<html lang="en"> 31 + \\<head> 32 + \\ <meta charset="utf-8"> 33 + \\ <meta name="viewport" content="width=device-width, initial-scale=1"> 34 + \\ <meta name="color-scheme" content="dark light"> 35 + \\ <title>zds api</title> 36 + \\ <link rel="canonical" href="{s}/api"> 37 + \\ <link rel="icon" href="/favicon.svg" type="image/svg+xml"> 38 + \\ <style>{s}</style> 39 + \\</head> 40 + \\<body> 41 + \\ <main class="shell"> 42 + \\ <header> 43 + \\ <a class="brand" href="/">zds</a> 44 + \\ <nav aria-label="api links"> 45 + \\ <a href="/api/openapi.json">OpenAPI JSON</a> 46 + \\ <a href="/xrpc/com.atproto.server.describeServer">describeServer</a> 47 + \\ </nav> 48 + \\ </header> 49 + \\ <section class="intro"> 50 + \\ <p class="eyebrow">endpoint reference</p> 51 + \\ <h1>{s}</h1> 52 + \\ <p class="lede">This PDS exposes {d} documented HTTP operations, including XRPC methods at <code>/xrpc/{{NSID}}</code>.</p> 53 + \\ <div class="stats" aria-label="endpoint summary"> 54 + \\ <span><strong>{d}</strong> operations</span> 55 + \\ <span><strong>{d}</strong> groups</span> 56 + \\ <span><strong>{d}</strong> XRPC methods</span> 57 + \\ </div> 58 + \\ </section> 59 + \\ <section class="tools" aria-label="filters"> 60 + \\ <label class="search"> 61 + \\ <span>Search</span> 62 + \\ <input id="search" type="search" placeholder="com.atproto.repo.applyWrites, sync, bearer"> 63 + \\ </label> 64 + \\ <div class="chips" id="groups" aria-label="groups">{s}</div> 65 + \\ </section> 66 + \\ <section class="endpoints" id="endpoints" aria-label="endpoints">{s}</section> 67 + \\ </main> 68 + \\ <script>{s}</script> 69 + \\</body> 70 + \\</html> 71 + , .{ 72 + try model.escapeHtml(allocator, public_url), 73 + css, 74 + try model.escapeHtml(allocator, public_url), 75 + endpoints.len, 76 + endpoints.len, 77 + model.groupCountTotal(), 78 + model.xrpcCount(), 79 + group_buttons.written(), 80 + cards.written(), 81 + script, 82 + }); 83 + } 84 + 85 + fn renderEndpointCard(allocator: std.mem.Allocator, writer: *std.Io.Writer, endpoint: Endpoint, open: bool) !void { 86 + const method_class = try model.lower(allocator, endpoint.method); 87 + const details_open = if (open) " open" else ""; 88 + const nsid = endpoint.nsid(); 89 + const curl = try renderCurl(allocator, endpoint); 90 + const search_text = try std.fmt.allocPrint(allocator, "{s} {s} {s} {s} {s}", .{ endpoint.method, endpoint.path, endpoint.group, endpoint.auth, endpoint.summary }); 91 + try writer.print( 92 + \\<details class="endpoint" data-group={f} data-method={f} data-search={f}{s}> 93 + \\ <summary><span class="method {s}">{s}</span><code>{s}</code><span class="auth">{s}</span></summary> 94 + \\ <div class="body"> 95 + \\ <p>{s}</p> 96 + , .{ 97 + std.json.fmt(endpoint.group, .{}), 98 + std.json.fmt(endpoint.method, .{}), 99 + std.json.fmt(search_text, .{}), 100 + details_open, 101 + method_class, 102 + try model.escapeHtml(allocator, endpoint.method), 103 + try model.escapeHtml(allocator, endpoint.path), 104 + try model.escapeHtml(allocator, endpoint.auth), 105 + try model.escapeHtml(allocator, endpoint.summary), 106 + }); 107 + 108 + if (nsid) |id| { 109 + try writer.print("<div class=\"nsid\"><span>NSID</span><code>{s}</code></div>", .{try model.escapeHtml(allocator, id)}); 110 + } 111 + try writer.writeAll("<div class=\"meta-grid\">"); 112 + try renderList(allocator, writer, "Query", endpoint.params); 113 + try renderList(allocator, writer, "Body", endpoint.body); 114 + try renderList(allocator, writer, "Response", endpoint.response); 115 + try writer.writeAll("</div>"); 116 + if (endpoint.notes.len > 0) { 117 + try writer.print("<p class=\"note\">{s}</p>", .{try model.escapeHtml(allocator, endpoint.notes)}); 118 + } 119 + try writer.print( 120 + \\<div class="snippet"> 121 + \\ <button class="copy" type="button" data-copy="{s}" aria-label="Copy curl command">Copy</button> 122 + \\ <pre><code>{s}</code></pre> 123 + \\</div> 124 + , .{ try model.escapeHtml(allocator, curl), try model.escapeHtml(allocator, curl) }); 125 + try writer.writeAll("</div></details>"); 126 + } 127 + 128 + fn renderList(allocator: std.mem.Allocator, writer: *std.Io.Writer, label: []const u8, items: []const []const u8) !void { 129 + try writer.print("<div><h2>{s}</h2>", .{label}); 130 + if (items.len == 0) { 131 + try writer.writeAll("<p class=\"muted\">none documented yet</p></div>"); 132 + return; 133 + } 134 + try writer.writeAll("<ul>"); 135 + for (items) |item| try writer.print("<li><code>{s}</code></li>", .{try model.escapeHtml(allocator, item)}); 136 + try writer.writeAll("</ul></div>"); 137 + } 138 + 139 + fn renderCurl(allocator: std.mem.Allocator, endpoint: Endpoint) ![]const u8 { 140 + const token = if (std.mem.indexOf(u8, endpoint.auth, "bearer") != null or std.mem.indexOf(u8, endpoint.auth, "service") != null) " \\\n -H 'authorization: Bearer $TOKEN'" else ""; 141 + const body = if (std.mem.eql(u8, endpoint.method, "POST")) " \\\n -H 'content-type: application/json' \\\n --data '{}'" else ""; 142 + return std.fmt.allocPrint( 143 + allocator, 144 + "curl -sS -X {s}{s} \"{s}{s}\"{s}", 145 + .{ endpoint.method, token, config.publicUrl(), endpoint.path, body }, 146 + ); 147 + } 148 + 149 + const css = 150 + \\:root{color-scheme:dark light;--bg:#111411;--panel:#171b18;--text:#eef4ed;--muted:#98a69b;--line:#2d352f;--accent:#7ee787;--cyan:#7dd3fc;--rose:#f0a6ca;--gold:#e6c36a;--shadow:0 14px 38px rgba(0,0,0,.24);font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif} 151 + \\*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text)}a{color:inherit}.shell{width:min(1180px,calc(100% - 32px));margin:0 auto;padding:24px 0 56px}header{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:54px}.brand{font-weight:800;text-decoration:none;font-size:1.25rem}nav{display:flex;gap:10px;flex-wrap:wrap}nav a{border:1px solid var(--line);border-radius:8px;padding:9px 12px;text-decoration:none;color:var(--muted);background:rgba(255,255,255,.02)} 152 + \\.intro{display:grid;gap:14px;margin-bottom:28px}.eyebrow{text-transform:uppercase;letter-spacing:.08em;color:var(--accent);font-size:.78rem;font-weight:800;margin:0}h1{font-size:clamp(2.4rem,8vw,5.8rem);line-height:.9;margin:0;letter-spacing:0}.lede{max-width:760px;color:var(--muted);font-size:1.12rem;line-height:1.6;margin:0}.lede code{color:var(--cyan)}.stats{display:flex;gap:10px;flex-wrap:wrap;margin-top:6px}.stats span{border:1px solid var(--line);border-radius:8px;padding:10px 12px;color:var(--muted);background:rgba(255,255,255,.025)}.stats strong{color:var(--text)} 153 + \\.tools{position:sticky;top:0;z-index:5;background:color-mix(in srgb,var(--bg) 90%,transparent);backdrop-filter:blur(16px);padding:12px 0 16px;margin-bottom:12px;border-bottom:1px solid var(--line)}.search{display:grid;gap:7px;margin-bottom:12px}.search span{font-size:.78rem;text-transform:uppercase;color:var(--muted);font-weight:800}.search input{width:100%;border:1px solid var(--line);border-radius:8px;background:var(--panel);color:var(--text);padding:13px 14px;font:inherit;outline:none}.search input:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(126,231,135,.15)}.chips{display:flex;flex-wrap:wrap;gap:8px}.chip{border:1px solid var(--line);border-radius:999px;background:transparent;color:var(--muted);padding:8px 11px;font:inherit;cursor:pointer}.chip.active{background:var(--text);border-color:var(--text);color:var(--bg)}.chip span{opacity:.72} 154 + \\.endpoints{display:grid;gap:10px}.endpoint{border:1px solid var(--line);border-radius:8px;background:var(--panel);box-shadow:var(--shadow);overflow:hidden}.endpoint[hidden]{display:none}.endpoint summary{list-style:none;display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:12px;align-items:center;padding:15px;cursor:pointer}.endpoint summary::-webkit-details-marker{display:none}.endpoint code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;overflow-wrap:anywhere}.method{font-size:.76rem;font-weight:900;border-radius:6px;padding:5px 7px;color:#071008}.method.get{background:var(--cyan)}.method.post{background:var(--accent)}.method.head{background:var(--gold)}.auth{font-size:.78rem;color:var(--muted);border:1px solid var(--line);border-radius:999px;padding:5px 8px;white-space:nowrap}.body{border-top:1px solid var(--line);padding:16px;display:grid;gap:15px}.body p{margin:0;color:var(--muted);line-height:1.55}.nsid{display:flex;gap:10px;align-items:center;color:var(--muted)}.nsid span{font-size:.75rem;text-transform:uppercase;font-weight:900;color:var(--rose)}.meta-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}.meta-grid>div{border:1px solid var(--line);border-radius:8px;padding:12px;background:rgba(255,255,255,.018)}h2{font-size:.78rem;text-transform:uppercase;color:var(--muted);margin:0 0 8px}ul{margin:0;padding-left:18px}.muted{color:var(--muted)}.note{border-left:3px solid var(--rose);padding-left:12px}.snippet{position:relative}.copy{position:absolute;right:8px;top:8px;border:1px solid var(--line);border-radius:6px;background:var(--panel);color:var(--text);font:inherit;font-size:.75rem;font-weight:800;padding:6px 9px;cursor:pointer}.copy:hover,.copy:focus{border-color:var(--accent);outline:none}.copy.done{background:var(--accent);border-color:var(--accent);color:#071008}pre{margin:0;white-space:pre-wrap;background:#090b09;color:#d6ead8;border-radius:8px;border:1px solid var(--line);padding:42px 12px 12px;overflow:auto} 155 + \\@media (max-width:720px){.shell{width:min(100% - 20px,1180px);padding-top:16px}header{align-items:flex-start;margin-bottom:32px}.endpoint summary{grid-template-columns:auto minmax(0,1fr);}.endpoint summary .auth{grid-column:2}.meta-grid{grid-template-columns:1fr}nav{justify-content:flex-end}} 156 + \\@media (prefers-color-scheme:light){:root{--bg:#f6f7f3;--panel:#ffffff;--text:#111411;--muted:#5b665f;--line:#d9dfd8;--shadow:0 12px 34px rgba(33,45,36,.09)}pre{background:#101510;color:#ecf7ec}} 157 + ; 158 + 159 + const script = 160 + \\const search=document.querySelector("#search"); 161 + \\const chips=[...document.querySelectorAll(".chip")]; 162 + \\const cards=[...document.querySelectorAll(".endpoint")]; 163 + \\let group="all"; 164 + \\function apply(){ 165 + \\ const q=search.value.trim().toLowerCase(); 166 + \\ for(const card of cards){ 167 + \\ const groupMatch=group==="all"||card.dataset.group===group; 168 + \\ const text=(card.dataset.search||"").toLowerCase(); 169 + \\ card.hidden=!(groupMatch&&text.includes(q)); 170 + \\ } 171 + \\} 172 + \\search.addEventListener("input",apply); 173 + \\for(const chip of chips){ 174 + \\ chip.addEventListener("click",()=>{ 175 + \\ group=chip.dataset.group; 176 + \\ for(const other of chips) other.classList.toggle("active",other===chip); 177 + \\ apply(); 178 + \\ }); 179 + \\} 180 + \\for(const button of document.querySelectorAll(".copy")){ 181 + \\ button.addEventListener("click",async()=>{ 182 + \\ const ok=await copyText(button.dataset.copy||""); 183 + \\ button.classList.toggle("done",ok); 184 + \\ button.textContent=ok?"Copied":"Failed"; 185 + \\ setTimeout(()=>{button.classList.remove("done");button.textContent="Copy";},1400); 186 + \\ }); 187 + \\} 188 + \\async function copyText(text){ 189 + \\ if(typeof navigator!=="undefined"&&navigator.clipboard&&navigator.clipboard.writeText){ 190 + \\ try{await navigator.clipboard.writeText(text);return true;}catch(_){} 191 + \\ } 192 + \\ const area=document.createElement("textarea"); 193 + \\ area.value=text; 194 + \\ area.setAttribute("readonly",""); 195 + \\ area.style.position="fixed"; 196 + \\ area.style.opacity="0"; 197 + \\ document.body.appendChild(area); 198 + \\ area.select(); 199 + \\ let ok=false; 200 + \\ try{ok=document.execCommand("copy");}catch(_){} 201 + \\ area.remove(); 202 + \\ return ok; 203 + \\} 204 + ;
+89
src/internal/api_reference/model.zig
··· 1 + const std = @import("std"); 2 + const router = @import("../../http/router.zig"); 3 + 4 + pub const Endpoint = router.Endpoint; 5 + pub const endpoints = router.endpoints; 6 + 7 + pub const group_order = [_][]const u8{ 8 + "zds", 9 + "status", 10 + "discovery", 11 + "oauth", 12 + "server", 13 + "account", 14 + "preferences", 15 + "repo", 16 + "sync", 17 + "identity", 18 + }; 19 + 20 + pub fn groupCount(group: []const u8) usize { 21 + var count: usize = 0; 22 + for (endpoints) |endpoint| { 23 + if (std.mem.eql(u8, endpoint.group, group)) count += 1; 24 + } 25 + return count; 26 + } 27 + 28 + pub fn groupCountTotal() usize { 29 + var count: usize = 0; 30 + for (group_order) |group| { 31 + if (groupCount(group) > 0) count += 1; 32 + } 33 + return count; 34 + } 35 + 36 + pub fn xrpcCount() usize { 37 + var count: usize = 0; 38 + for (endpoints) |endpoint| { 39 + if (endpoint.nsid() != null) count += 1; 40 + } 41 + return count; 42 + } 43 + 44 + pub fn pathSeenBefore(index: usize) bool { 45 + for (endpoints[0..index]) |endpoint| { 46 + if (std.mem.eql(u8, endpoint.path, endpoints[index].path)) return true; 47 + } 48 + return false; 49 + } 50 + 51 + pub fn lower(allocator: std.mem.Allocator, input: []const u8) ![]const u8 { 52 + const out = try allocator.alloc(u8, input.len); 53 + for (input, 0..) |c, i| out[i] = std.ascii.toLower(c); 54 + return out; 55 + } 56 + 57 + pub fn methodLower(input: []const u8) []const u8 { 58 + if (std.mem.eql(u8, input, "GET")) return "get"; 59 + if (std.mem.eql(u8, input, "HEAD")) return "head"; 60 + if (std.mem.eql(u8, input, "POST")) return "post"; 61 + return input; 62 + } 63 + 64 + pub fn escapeHtml(allocator: std.mem.Allocator, input: []const u8) ![]const u8 { 65 + var out: std.Io.Writer.Allocating = .init(allocator); 66 + defer out.deinit(); 67 + for (input) |c| { 68 + switch (c) { 69 + '&' => try out.writer.writeAll("&amp;"), 70 + '<' => try out.writer.writeAll("&lt;"), 71 + '>' => try out.writer.writeAll("&gt;"), 72 + '"' => try out.writer.writeAll("&quot;"), 73 + '\'' => try out.writer.writeAll("&#39;"), 74 + else => try out.writer.writeByte(c), 75 + } 76 + } 77 + return out.toOwnedSlice(); 78 + } 79 + 80 + test "endpoint inventory covers XRPC paths" { 81 + try std.testing.expect(endpoints.len > 50); 82 + try std.testing.expect(xrpcCount() > 30); 83 + for (endpoints) |endpoint| { 84 + if (endpoint.nsid()) |id| { 85 + try std.testing.expect(id.len > 0); 86 + try std.testing.expect(std.mem.indexOfScalar(u8, id, '/') == null); 87 + } 88 + } 89 + }
+49
src/internal/api_reference/openapi.zig
··· 1 + const std = @import("std"); 2 + const config = @import("../../core/config.zig"); 3 + const model = @import("model.zig"); 4 + 5 + const Endpoint = model.Endpoint; 6 + const endpoints = model.endpoints; 7 + 8 + pub fn render(allocator: std.mem.Allocator) ![]const u8 { 9 + var out: std.Io.Writer.Allocating = .init(allocator); 10 + defer out.deinit(); 11 + try out.writer.print( 12 + \\{{"openapi":"3.1.0","info":{{"title":"ZDS endpoint reference","version":"0.1.0","description":"Endpoint inventory generated by ZDS."}},"servers":[{{"url":{f}}}],"paths":{{ 13 + , .{std.json.fmt(config.publicUrl(), .{})}); 14 + 15 + var first_path = true; 16 + for (endpoints, 0..) |endpoint, i| { 17 + if (model.pathSeenBefore(i)) continue; 18 + if (!first_path) try out.writer.writeAll(","); 19 + first_path = false; 20 + try out.writer.print("{f}:{{", .{std.json.fmt(endpoint.path, .{})}); 21 + var first_method = true; 22 + for (endpoints) |method_endpoint| { 23 + if (!std.mem.eql(u8, endpoint.path, method_endpoint.path)) continue; 24 + if (!first_method) try out.writer.writeAll(","); 25 + first_method = false; 26 + try renderOperation(&out.writer, method_endpoint); 27 + } 28 + try out.writer.writeAll("}"); 29 + } 30 + 31 + try out.writer.writeAll("}}\n"); 32 + return out.toOwnedSlice(); 33 + } 34 + 35 + fn renderOperation(writer: *std.Io.Writer, endpoint: Endpoint) !void { 36 + try writer.print( 37 + \\{f}:{{"summary":{f},"tags":[{f}],"description":{f},"responses":{{"200":{{"description":"OK"}}}}}} 38 + , .{ 39 + std.json.fmt(model.methodLower(endpoint.method), .{}), 40 + std.json.fmt(endpoint.summary, .{}), 41 + std.json.fmt(endpoint.group, .{}), 42 + std.json.fmt(endpointDescription(endpoint), .{}), 43 + }); 44 + } 45 + 46 + fn endpointDescription(endpoint: Endpoint) []const u8 { 47 + _ = endpoint; 48 + return "Generated from the ZDS endpoint inventory. Request and response schemas are intentionally sparse in this MVP."; 49 + }
+12
src/internal/api_reference/root.zig
··· 1 + pub const html = @import("html.zig"); 2 + pub const model = @import("model.zig"); 3 + pub const openapi = @import("openapi.zig"); 4 + 5 + pub const renderHtml = html.render; 6 + pub const renderOpenApi = openapi.render; 7 + 8 + test { 9 + _ = html; 10 + _ = model; 11 + _ = openapi; 12 + }
+1
src/root.zig
··· 31 31 }; 32 32 33 33 pub const internal = struct { 34 + pub const api_reference = @import("internal/api_reference/root.zig"); 34 35 pub const cli = @import("internal/cli.zig"); 35 36 pub const passkeys = @import("internal/passkeys.zig"); 36 37 pub const scopes = @import("internal/scopes.zig");