atproto pds in zig
Something went wrong. Try again.
21 kB · 263 lines
Zig
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264const std = @import("std");const config = @import("../../core/config.zig");const model = @import("model.zig");
const Endpoint = model.Endpoint;const endpoints = model.endpoints;
pub fn render(allocator: std.mem.Allocator) ![]const u8 { var group_buttons: std.Io.Writer.Allocating = .init(allocator); defer group_buttons.deinit(); try group_buttons.writer.print("<button class=\"chip active\" type=\"button\" data-group=\"all\">All <span>{d}</span></button>", .{endpoints.len}); for (model.group_order) |group| { const total = model.groupCount(group); if (total == 0) continue; try group_buttons.writer.print( "<button class=\"chip\" type=\"button\" data-group={f}>{s} <span>{d}</span></button>", .{ std.json.fmt(group, .{}), try model.escapeHtml(allocator, group), total }, ); }
const sorted = try allocator.alloc(Endpoint, endpoints.len); defer allocator.free(sorted); @memcpy(sorted, endpoints[0..]); std.mem.sort(Endpoint, sorted, {}, compareEndpoints);
var cards: std.Io.Writer.Allocating = .init(allocator); defer cards.deinit(); for (sorted, 0..) |endpoint, i| { try renderEndpointCard(allocator, &cards.writer, endpoint, i == 0); }
const public_url = config.publicUrl(); const public_host = displayHost(public_url); return std.fmt.allocPrint(allocator, \\<!doctype html> \\<html lang="en"> \\<head> \\ <meta charset="utf-8"> \\ <meta name="viewport" content="width=device-width, initial-scale=1"> \\ <meta name="color-scheme" content="dark light"> \\ <title>zds api</title> \\ <meta name="description" content="endpoint reference for this zds pds"> \\ <link rel="canonical" href="{s}/api"> \\ <link rel="icon" href="/favicon.svg" type="image/svg+xml"> \\ <style>{s}</style> \\</head> \\<body> \\ <main class="shell"> \\ <header> \\ <a class="brand" href="/" aria-label="zds home">zds</a> \\ <nav aria-label="project and api links"> \\ <a class="icon-link logo-link" href="https://tangled.org/zat.dev/zds" aria-label="source on Tangled" title="source"><img alt="" src="https://assets.tangled.network/tangled_dolly_face_only_white_on_trans.svg"></a> \\ <a class="icon-link logo-link" href="https://zat.dev" aria-label="zat.dev" title="zat.dev"><img alt="" src="https://zat.dev/favicon.svg"></a> \\ <a class="icon-link logo-link" href="https://atproto.com" aria-label="AT Protocol" title="atproto"><img alt="" src="https://atproto.com/favicon.ico"></a> \\ <a class="nav-link" href="/api/openapi.json">openapi</a> \\ </nav> \\ </header> \\ <section class="intro"> \\ <p class="eyebrow">{s}</p> \\ <h1>api reference</h1> \\ <p class="lede">this pds exposes {d} documented http operations, including <span class="xrpc-expression">xrpc methods at <span class="route-expression"><code>/xrpc/</code><code>{{NSID}}</code></span></span>.</p> \\ <div class="stats" aria-label="endpoint summary"> \\ <span><strong>{d}</strong> operations</span> \\ <span><strong>{d}</strong> groups</span> \\ <span><strong>{d}</strong> xrpc methods</span> \\ <span><strong>{d}</strong> experimental</span> \\ <span><strong>{s}</strong> permissioned data</span> \\ </div> \\ </section> \\ <section class="tools" aria-label="filters"> \\ <label class="search"> \\ <span>Search</span> \\ <input id="search" type="search" placeholder="com.atproto.repo.applyWrites, sync, bearer"> \\ </label> \\ <div class="chips" id="groups" aria-label="groups">{s}</div> \\ </section> \\ <section class="endpoints" id="endpoints" aria-label="endpoints">{s}</section> \\ </main> \\ <script>{s}</script> \\</body> \\</html> , .{ try model.escapeHtml(allocator, public_url), css, try model.escapeHtml(allocator, public_host), endpoints.len, endpoints.len, model.groupCountTotal(), model.xrpcCount(), model.experimentalCount(), if (config.permissionedData()) "enabled" else "disabled", group_buttons.written(), cards.written(), script, });}
fn displayHost(url: []const u8) []const u8 { if (std.mem.startsWith(u8, url, "https://")) return url["https://".len..]; if (std.mem.startsWith(u8, url, "http://")) return url["http://".len..]; return url;}
fn compareEndpoints(_: void, a: Endpoint, b: Endpoint) bool { const group_a = groupRank(a.group); const group_b = groupRank(b.group); if (group_a != group_b) return group_a < group_b; const path_order = std.mem.order(u8, a.path, b.path); if (path_order != .eq) return path_order == .lt; return methodRank(a.method) < methodRank(b.method);}
fn groupRank(group: []const u8) usize { for (model.group_order, 0..) |ordered, i| { if (std.mem.eql(u8, group, ordered)) return i; } return model.group_order.len;}
fn methodRank(method: []const u8) usize { if (std.mem.eql(u8, method, "GET")) return 0; if (std.mem.eql(u8, method, "HEAD")) return 1; if (std.mem.eql(u8, method, "POST")) return 2; return 3;}
fn renderEndpointCard(allocator: std.mem.Allocator, writer: *std.Io.Writer, endpoint: Endpoint, open: bool) !void { const method_class = try model.lower(allocator, endpoint.method); const details_open = if (open) " open" else ""; const nsid = endpoint.nsid(); const curl = try renderCurl(allocator, endpoint); const experimental = model.isExperimental(endpoint); const experimental_badge = if (experimental) "<span class=\"badge experimental\">experimental</span>" else ""; const search_text = try std.fmt.allocPrint( allocator, "{s} {s} {s} {s} {s} {s}", .{ endpoint.method, endpoint.path, endpoint.group, endpoint.auth, endpoint.summary, if (experimental) "experimental" else "" }, ); try writer.print( \\<details class="endpoint" data-group={f} data-method={f} data-search={f}{s}> \\ <summary><span class="method {s}">{s}</span><span class="summary-main"><code>{s}</code><span class="labels">{s}<span class="auth">{s}</span></span></span></summary> \\ <div class="body"> \\ <p>{s}</p> , .{ std.json.fmt(endpoint.group, .{}), std.json.fmt(endpoint.method, .{}), std.json.fmt(search_text, .{}), details_open, method_class, try model.escapeHtml(allocator, endpoint.method), try model.escapeHtml(allocator, endpoint.path), experimental_badge, try model.escapeHtml(allocator, endpoint.auth), try model.escapeHtml(allocator, endpoint.summary), });
if (nsid) |id| { try writer.print("<div class=\"nsid\"><span>NSID</span><code>{s}</code></div>", .{try model.escapeHtml(allocator, id)}); } try renderMetadata(allocator, writer, endpoint); if (experimental) { try writer.writeAll("<p class=\"experimental-copy\">Experimental. Availability is controlled by <code>ZDS_PERMISSIONED_DATA</code>; semantics may change while the protocol is still being shaped.</p>"); } if (endpoint.notes.len > 0) { try writer.print("<p class=\"note\">{s}</p>", .{try model.escapeHtml(allocator, endpoint.notes)}); } try writer.print( \\<div class="snippet"> \\ <button class="copy" type="button" data-copy="{s}" aria-label="Copy curl command">Copy</button> \\ <pre><code>{s}</code></pre> \\</div> , .{ try model.escapeHtml(allocator, curl), try model.escapeHtml(allocator, curl) }); try writer.writeAll("</div></details>");}
fn renderList(allocator: std.mem.Allocator, writer: *std.Io.Writer, label: []const u8, items: []const []const u8) !void { if (items.len == 0) return; try writer.print("<div><h2>{s}</h2>", .{label}); try writer.writeAll("<ul>"); for (items) |item| try writer.print("<li><code>{s}</code></li>", .{try model.escapeHtml(allocator, item)}); try writer.writeAll("</ul></div>");}
fn renderMetadata(allocator: std.mem.Allocator, writer: *std.Io.Writer, endpoint: Endpoint) !void { if (endpoint.params.len == 0 and endpoint.body.len == 0 and endpoint.response.len == 0) { try writer.writeAll("<p class=\"muted compact\">No field metadata yet.</p>"); return; } try writer.writeAll("<div class=\"meta-grid\">"); try renderList(allocator, writer, "Query", endpoint.params); try renderList(allocator, writer, "Body", endpoint.body); try renderList(allocator, writer, "Response", endpoint.response); try writer.writeAll("</div>");}
fn renderCurl(allocator: std.mem.Allocator, endpoint: Endpoint) ![]const u8 { 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 ""; const body = if (std.mem.eql(u8, endpoint.method, "POST")) " \\\n -H 'content-type: application/json' \\\n --data '{}'" else ""; return std.fmt.allocPrint( allocator, "curl -sS -X {s}{s} \"{s}{s}\"{s}", .{ endpoint.method, token, config.publicUrl(), endpoint.path, body }, );}
const css = \\:root{--bg:#070807;--panel:#101210;--panel-2:#151915;--line:#273026;--text:#f0f1ec;--muted:#aaa69b;--dim:#69645c;--accent:#91adff;--green:#39c178;--pink:#f05f8e;--focus:#c9d7ff;--glass:rgba(255,255,255,.085);--accent-bg:color-mix(in srgb,var(--green) 13%,var(--bg));--accent-panel:color-mix(in srgb,var(--green) 9%,var(--panel));--accent-panel-2:color-mix(in srgb,var(--accent) 8%,var(--panel-2));color-scheme:dark} \\*{box-sizing:border-box}html{min-height:100%;background:var(--accent-bg);overflow-x:hidden}body{position:relative;margin:0;min-height:100vh;overflow-x:hidden;background:radial-gradient(circle at 20% 0%,color-mix(in srgb,var(--green) 15%,transparent),transparent 35%),radial-gradient(circle at 94% 18%,color-mix(in srgb,var(--accent) 13%,transparent),transparent 32%),linear-gradient(135deg,var(--accent-bg),var(--bg) 58%);color:var(--text);font:15px/1.55 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono",monospace}body::before,body::after{content:"";position:fixed;inset:-18vmax;z-index:0;pointer-events:none}body::before{background:linear-gradient(108deg,transparent 0 22%,rgba(255,255,255,.10) 24%,transparent 27% 52%,rgba(255,255,255,.055) 54%,transparent 58%),linear-gradient(72deg,transparent 0 38%,color-mix(in srgb,var(--accent) 11%,transparent) 44%,transparent 52%);filter:blur(20px) saturate(1.15);opacity:.58;transform:rotate(-4deg)}body::after{background:radial-gradient(ellipse at 22% 32%,rgba(255,255,255,.09),transparent 28%),radial-gradient(ellipse at 72% 18%,color-mix(in srgb,var(--green) 14%,transparent),transparent 31%),radial-gradient(ellipse at 66% 78%,color-mix(in srgb,var(--pink) 12%,transparent),transparent 30%);filter:blur(34px);opacity:.44}a{color:inherit}.shell{position:relative;z-index:1;width:min(100%,780px);margin:0 auto;padding:18px 16px 46px}header{position:relative;z-index:10;display:flex;justify-content:space-between;align-items:center;gap:12px;min-height:50px;margin:0 0 26px;padding:8px 0;color:var(--muted);font-size:12px}.brand{flex:0 0 auto;display:inline-flex;align-items:center;color:var(--text);text-decoration:none;font-weight:700;font-size:14px;letter-spacing:0}.brand:hover{opacity:.82}nav{min-width:0;display:flex;align-items:center;justify-content:flex-end;gap:7px;padding:4px;border:1px solid color-mix(in srgb,var(--line) 58%,white 14%);border-radius:18px;background:linear-gradient(110deg,rgba(255,255,255,.16),transparent 22% 76%,rgba(255,255,255,.08)),radial-gradient(circle at 20% 0%,color-mix(in srgb,var(--green) 12%,transparent),transparent 46%),color-mix(in srgb,var(--accent-panel) 72%,transparent);box-shadow:inset 0 1px 0 rgba(255,255,255,.22),inset 0 -18px 42px rgba(255,255,255,.035),0 8px 28px rgba(0,0,0,.12);backdrop-filter:blur(26px) saturate(1.26);-webkit-backdrop-filter:blur(26px) saturate(1.26)}nav a{color:var(--muted);text-decoration:none}.icon-link,.nav-link{position:relative;min-width:34px;height:34px;display:inline-grid;place-items:center;border:1px solid color-mix(in srgb,var(--line) 74%,white 10%);border-radius:12px;background:radial-gradient(circle at 30% 18%,rgba(255,255,255,.20),transparent 32%),linear-gradient(145deg,rgba(255,255,255,.12),rgba(255,255,255,.03)),color-mix(in srgb,var(--accent-panel) 74%,transparent);color:var(--muted);text-decoration:none;box-shadow:inset 0 1px 0 rgba(255,255,255,.18),0 10px 28px rgba(0,0,0,.14)}.nav-link{padding:0 11px}.logo-link::after{content:"";position:absolute;inset:5px;border-radius:9px;background:color-mix(in srgb,var(--glass) 62%,var(--accent-panel));box-shadow:inset 0 1px 0 rgba(255,255,255,.18)}.icon-link img{position:relative;z-index:1;width:22px;height:22px;object-fit:contain;display:block;filter:drop-shadow(0 1px 2px rgba(0,0,0,.45))}.icon-link:hover,.nav-link:hover{color:var(--text);border-color:color-mix(in srgb,var(--green) 55%,var(--line));background:color-mix(in srgb,var(--accent-panel-2) 86%,transparent)} \\.intro{display:grid;gap:13px;margin:52px 0 26px}.eyebrow{margin:0;color:var(--green);font-size:12px;font-weight:700;text-transform:lowercase}.eyebrow::before{content:"/";color:var(--dim);margin-right:2px}h1{margin:0;color:var(--text);font-size:clamp(42px,10vw,82px);line-height:.95;letter-spacing:0;text-transform:lowercase}.lede{max-width:none;margin:0;color:var(--muted);font-size:15px}.lede code{color:#7dd3fc}.xrpc-expression,.route-expression{display:inline-flex;white-space:nowrap}.xrpc-expression{gap:.35ch}.stats{display:flex;gap:9px;flex-wrap:wrap;margin-top:5px}.stats span{border:1px solid color-mix(in srgb,var(--line) 70%,white 8%);border-radius:12px;padding:9px 11px;color:var(--muted);background:color-mix(in srgb,var(--accent-panel) 64%,transparent);box-shadow:inset 0 1px 0 rgba(255,255,255,.10)}.stats strong{color:var(--text)} \\.tools{position:relative;z-index:5;padding:12px 0 15px;margin-bottom:12px;border-bottom:1px solid color-mix(in srgb,var(--line) 58%,transparent)}.search{display:grid;gap:7px;margin-bottom:11px}.search span{font-size:11px;text-transform:lowercase;color:var(--dim)}.search input{width:100%;border:1px solid color-mix(in srgb,var(--line) 76%,white 6%);border-radius:12px;background:color-mix(in srgb,var(--panel) 86%,transparent);color:var(--text);padding:12px 13px;font:inherit;outline:none;box-shadow:inset 0 1px 0 rgba(255,255,255,.08)}.search input:focus{border-color:var(--focus)}.chips{display:flex;flex-wrap:wrap;gap:7px}.chip{border:1px solid color-mix(in srgb,var(--line) 78%,white 6%);border-radius:999px;background:color-mix(in srgb,var(--accent-panel) 50%,transparent);color:var(--muted);padding:7px 10px;font:inherit;font-size:12px;cursor:pointer;text-transform:lowercase}.chip.active{background:var(--text);border-color:var(--text);color:var(--bg)}.chip span{opacity:.68} \\.endpoints{display:grid;gap:9px}.endpoint{border:1px solid color-mix(in srgb,var(--line) 76%,white 6%);border-radius:12px;background:color-mix(in srgb,var(--panel) 88%,transparent);box-shadow:inset 0 1px 0 rgba(255,255,255,.08),0 14px 38px rgba(0,0,0,.16);overflow:hidden}.endpoint[hidden]{display:none}.endpoint summary{list-style:none;display:grid;grid-template-columns:auto minmax(0,1fr);gap:11px;align-items:start;padding:13px 14px;cursor:pointer}.endpoint summary::-webkit-details-marker{display:none}.endpoint code{font-family:inherit;overflow-wrap:anywhere}.summary-main{min-width:0;display:grid;gap:7px}.summary-main>code{min-width:0;line-height:1.35;word-break:normal}.method{font-size:11px;font-weight:700;border-radius:8px;padding:4px 6px;color:#071008}.method.get{background:#7dd3fc}.method.post{background:var(--green)}.method.head{background:#f5d85d}.labels{display:flex;gap:6px;align-items:center;justify-content:flex-start;flex-wrap:wrap}.badge,.auth{font-size:11px;border:1px solid color-mix(in srgb,var(--line) 76%,white 5%);border-radius:999px;padding:4px 7px;white-space:nowrap;text-transform:lowercase}.auth{color:var(--muted)}.badge.experimental{color:#071008;background:color-mix(in srgb,var(--pink) 82%,white 16%);border-color:color-mix(in srgb,var(--pink) 84%,white 6%)}.body{border-top:1px solid color-mix(in srgb,var(--line) 78%,transparent);padding:14px;display:grid;gap:14px}.body p{margin:0;color:var(--muted);line-height:1.55}.experimental-copy{border:1px solid color-mix(in srgb,var(--pink) 42%,var(--line));border-radius:10px;padding:10px;background:color-mix(in srgb,var(--pink) 8%,transparent)}.experimental-copy code{color:var(--text)}.nsid{display:flex;gap:8px;align-items:center;color:var(--muted);min-width:0}.nsid span{font-size:10px;text-transform:lowercase;color:var(--pink)}.nsid code{color:var(--text);min-width:0}.meta-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:9px}.meta-grid>div{border:1px solid color-mix(in srgb,var(--line) 78%,white 4%);border-radius:10px;padding:10px;background:rgba(255,255,255,.018)}h2{font-size:11px;text-transform:lowercase;color:var(--dim);font-weight:400;margin:0 0 7px}ul{margin:0;padding-left:18px}.muted{color:var(--dim)}.compact{font-size:12px}.note{border-left:2px solid var(--pink);padding-left:10px}.snippet{position:relative}.copy{position:absolute;right:8px;top:8px;border:1px solid color-mix(in srgb,var(--line) 78%,white 6%);border-radius:10px;background:color-mix(in srgb,var(--panel) 88%,transparent);color:var(--muted);font:inherit;font-size:11px;padding:5px 8px;cursor:pointer;text-transform:lowercase}.copy:hover,.copy:focus{color:var(--text);border-color:color-mix(in srgb,var(--green) 55%,var(--line));outline:none}.copy.done{background:var(--green);border-color:var(--green);color:#071008}pre{margin:0;white-space:pre-wrap;background:#090b09;color:#d6ead8;border-radius:10px;border:1px solid color-mix(in srgb,var(--line) 78%,white 4%);padding:13px 78px 13px 11px;overflow:auto;font:inherit;font-size:13px} \\@media (min-width:680px){.shell{padding-top:24px}nav{gap:8px}} \\@media (max-width:720px){.intro{margin-top:34px}.meta-grid{grid-template-columns:1fr}nav{justify-content:flex-end}.lede{font-size:14px}} \\@media (max-width:430px){.shell{padding-left:14px;padding-right:14px}header{gap:8px}nav{gap:4px;padding:3px;border-radius:16px}.icon-link,.nav-link{min-width:32px;height:32px;border-radius:10px}.logo-link::after{inset:5px;border-radius:8px}.icon-link img{width:20px;height:20px}.nav-link{padding:0 9px}} \\@media (prefers-color-scheme:light){:root{--bg:#f6f4ed;--panel:#fffdf7;--panel-2:#ece8dd;--line:#d8d0c0;--text:#161412;--muted:#625c53;--dim:#8f8679;--accent:#315dcb;--green:#1b7340;--pink:#b72e5c;--focus:#163f9f;--glass:rgba(255,255,255,.72);--accent-bg:color-mix(in srgb,var(--green) 11%,var(--bg));--accent-panel:color-mix(in srgb,var(--green) 5%,var(--panel));--accent-panel-2:color-mix(in srgb,var(--accent) 5%,var(--panel-2));color-scheme:light}pre{background:#101510;color:#ecf7ec}};
const script = \\const search=document.querySelector("#search"); \\const chips=[...document.querySelectorAll(".chip")]; \\const cards=[...document.querySelectorAll(".endpoint")]; \\let group="all"; \\function apply(){ \\ const q=search.value.trim().toLowerCase(); \\ for(const card of cards){ \\ const groupMatch=group==="all"||card.dataset.group===group; \\ const text=(card.dataset.search||"").toLowerCase(); \\ card.hidden=!(groupMatch&&text.includes(q)); \\ } \\} \\search.addEventListener("input",apply); \\for(const chip of chips){ \\ chip.addEventListener("click",()=>{ \\ group=chip.dataset.group; \\ for(const other of chips) other.classList.toggle("active",other===chip); \\ apply(); \\ }); \\} \\for(const button of document.querySelectorAll(".copy")){ \\ button.addEventListener("click",async()=>{ \\ const ok=await copyText(button.dataset.copy||""); \\ button.classList.toggle("done",ok); \\ button.textContent=ok?"Copied":"Failed"; \\ setTimeout(()=>{button.classList.remove("done");button.textContent="Copy";},1400); \\ }); \\} \\async function copyText(text){ \\ if(typeof navigator!=="undefined"&&navigator.clipboard&&navigator.clipboard.writeText){ \\ try{await navigator.clipboard.writeText(text);return true;}catch(_){} \\ } \\ const area=document.createElement("textarea"); \\ area.value=text; \\ area.setAttribute("readonly",""); \\ area.style.position="fixed"; \\ area.style.opacity="0"; \\ document.body.appendChild(area); \\ area.select(); \\ let ok=false; \\ try{ok=document.execCommand("copy");}catch(_){} \\ area.remove(); \\ return ok; \\};