//! `GET /api/openapi.yaml` — the API contract, served verbatim (ruling 23). //! //! The document is hand-written and embedded; nothing renders or validates it //! at runtime (rendering is Phase 10, external validators are dependencies we //! refused). What keeps it honest is W10's contract suite plus the tests //! below: every route the router serves must appear textually in the //! document, so a route added without documentation fails the build's tests //! rather than drifting silently. const std = @import("std"); const http_util = @import("http_util.zig"); const router = @import("router.zig"); const server = @import("server.zig"); pub const yaml: []const u8 = @embedFile("openapi.yaml"); pub const content_type = "application/yaml"; pub fn handle( state: *server.WebState, io: std.Io, request: *http_util.Request, ) http_util.HandlerError!void { _ = state; _ = io; return http_util.respondBytes(request, .ok, yaml, content_type, &.{}); } // --------------------------------------------------------------------------- // tests // --------------------------------------------------------------------------- const testing = std.testing; test "every served route appears textually in the document" { for (router.routes) |route| { var key_buf: [128]u8 = undefined; // Path keys are two-space indented under `paths:`; requiring the // colon keeps `/api/groups` from being satisfied by its `{id}` twin. const key = try std.fmt.bufPrint(&key_buf, "\n {s}:\n", .{route.pattern}); try testing.expect(std.mem.containsAtLeast(u8, yaml, 1, key)); var method_buf: [16]u8 = undefined; const method = try std.fmt.bufPrint(&method_buf, " {s}:\n", .{@tagName(route.method)}); _ = std.ascii.lowerString(&method_buf, method); try testing.expect(std.mem.containsAtLeast(u8, yaml, 1, method_buf[0..method.len])); } } // Drift guard for milestone-20 ruling 7: a route classified `config_write` can // answer 403 under file authority, so its operation must say so — and a route // that cannot must not claim it. Textual, like the coverage test above: the // document has no parser here, and the two facts it compares are one line each. test "every config write documents the file-authority 403, and nothing else does" { for (router.routes) |route| { const operation = try operationBlock(route.pattern, route.method); const documented = std.mem.containsAtLeast(u8, operation, 1, "\n \"403\":\n"); if (documented != (route.policy == .config_write)) { std.debug.print( "{t} {s} is {t} but {s} a 403\n", .{ route.method, route.pattern, route.policy, if (documented) "documents" else "does not document" }, ); return error.TestUnexpectedResult; } } } /// The body of one operation: everything under `pattern`'s `method` key. /// /// The path block is bounded *before* the method is looked for. Searching the /// rest of the document instead would let a later path's `delete:` answer for a /// path that has none, and the guard above would pass on an operation nobody /// documented. fn operationBlock(pattern: []const u8, method: std.http.Method) ![]const u8 { var key_buf: [128]u8 = undefined; const path_key = try std.fmt.bufPrint(&key_buf, "\n {s}:\n", .{pattern}); const path_at = std.mem.indexOf(u8, yaml, path_key) orelse return error.PathNotDocumented; const path_body = blockUnder(yaml[path_at + path_key.len ..], 2); var method_buf: [16]u8 = undefined; const method_key = try std.fmt.bufPrint(&method_buf, " {s}:\n", .{@tagName(method)}); _ = std.ascii.lowerString(&method_buf, method_key); const key = method_buf[0..method_key.len]; // Anchored at a line start: a `get:` nested deeper inside a description // contains the four-space key as a substring. var offset: usize = 0; while (offset < path_body.len) { if (std.mem.startsWith(u8, path_body[offset..], key)) { return blockUnder(path_body[offset + key.len ..], 4); } offset = (std.mem.indexOfScalarPos(u8, path_body, offset, '\n') orelse path_body.len) + 1; } return error.MethodNotDocumented; } /// The run of lines at the start of `body` indented deeper than `indent` — what /// belongs to the key that just ended. `body` starts at a line boundary. Blank /// lines belong to whatever surrounds them and never close a block. fn blockUnder(body: []const u8, indent: usize) []const u8 { var offset: usize = 0; while (offset < body.len) { const line_end = std.mem.indexOfScalarPos(u8, body, offset, '\n') orelse body.len; if (line_end != offset) { const depth = for (body[offset..line_end], 0..) |c, i| { if (c != ' ') break i; } else line_end - offset; if (depth <= indent) return body[0..offset]; } offset = line_end + 1; } return body; } test "an operation block stops at its own path and its own method" { // `/api/groups` has no DELETE. An unbounded search answers with the one // under `/api/groups/{id}`, and the 403 guard then grades the wrong // operation — silently passing for a route nobody documented. try testing.expectError(error.MethodNotDocumented, operationBlock("/api/groups", .DELETE)); // A block it does have never reaches into its neighbour under the same // path either. const list_groups = try operationBlock("/api/groups", .GET); try testing.expect(std.mem.containsAtLeast(u8, list_groups, 1, "List groups")); try testing.expect(!std.mem.containsAtLeast(u8, list_groups, 1, "Create a group")); } test "the document names the contract's fixed points" { for ([_][]const u8{ "openapi: 3.0.3", "nxdns_session", "text/event-stream", "snake_case", "Retry-After", }) |needle| { try testing.expect(std.mem.containsAtLeast(u8, yaml, 1, needle)); } }