Public: anyone can read this; the crew can change it.
The OpenAPI document describes listing a repository's issues and pulls, with the query parameters the routes read #3397
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/3393-the-openapi-document-describes-listing-a → main 2 commits fork 2 threads · 2 unresolved
2 files
changed +28
−1
src/contract/openapi.ts +14 −1
| @@ -15,8 +15,13 @@ interface Route { | ||
| 15 | 15 | readonly auth: boolean; |
| 16 | 16 | /** A JSON body's fields, when it takes one. */ |
| 17 | 17 | readonly body?: Readonly<Record<string, string>>; |
| 18 | /** The query parameters its handler reads, by name and type. */ | |
| 19 | readonly query?: Readonly<Record<string, string>>; | |
| 18 | 20 | } |
| 19 | 21 | |
| 22 | /** What `listQuery` in `http/api.ts` reads from a list of issues or pulls. */ | |
| 23 | const LIST_QUERY = { state: "string", limit: "integer", cursor: "integer", label: "string", author: "string" }; | |
| 24 | ||
| 20 | 25 | const REPO = "/api/v1/repos/{owner}/{name}"; |
| 21 | 26 | |
| 22 | 27 | export const ROUTES: readonly Route[] = [ |
| @@ -29,6 +34,8 @@ export const ROUTES: readonly Route[] = [ | ||
| 29 | 34 | auth: false, |
| 30 | 35 | body: { title: "string", body: "string", page: "string" }, |
| 31 | 36 | }, |
| 37 | { method: "get", path: `${REPO}/issues`, summary: "List a repository's issues", auth: false, query: LIST_QUERY }, | |
| 38 | { method: "get", path: `${REPO}/pulls`, summary: "List a repository's pulls", auth: false, query: LIST_QUERY }, | |
| 32 | 39 | { |
| 33 | 40 | method: "post", |
| 34 | 41 | path: `${REPO}/issues`, |
| @@ -112,10 +119,16 @@ export function openapi(origin: string, version: string): Record<string, unknown | ||
| 112 | 119 | required: true, |
| 113 | 120 | schema: { type: m[1] === "number" || m[1] === "blocker" ? "integer" : "string" }, |
| 114 | 121 | })); |
| 122 | const query = Object.entries(r.query ?? {}).map(([name, t]) => ({ | |
| 123 | name, | |
| 124 | in: "query", | |
| 125 | required: false, | |
| 126 | schema: name === "state" ? { type: "string", enum: ["open", "closed", "all"] } : schema(t), | |
| 127 | })); | |
| 115 | 128 | (paths[r.path] ??= {})[r.method] = { |
| 116 | 129 | operationId: `${r.method}${r.path.replace(/[^A-Za-z0-9]+(.)?/g, (_, c: string | undefined) => (c ?? "").toUpperCase())}`, |
| 117 | 130 | summary: r.summary, |
| 118 | parameters: params, | |
| 131 | parameters: [...params, ...query], | |
| 119 | 132 | ...(r.body === undefined |
| 120 | 133 | ? {} |
| 121 | 134 | : { |
test/openapi.test.ts +14
| @@ -47,6 +47,20 @@ test("the map, plan and dependency routes are described", () => { | ||
| 47 | 47 | expect(del.parameters.find((p: any) => p.name === "blocker").schema.type).toBe("integer"); |
| 48 | 48 | }); |
| 49 | 49 | |
| 50 | test("listing issues and pulls is described with the query the handlers read", () => { | |
| 51 | const doc = openapi("https://f.test", "1") as any; | |
| 52 | const R = "/api/v1/repos/{owner}/{name}"; | |
| 53 | for (const kind of ["issues", "pulls"]) { | |
| 54 | const get = doc.paths[`${R}/${kind}`].get; | |
| 55 | expect(get.security).toBeUndefined(); | |
| 56 | const q = get.parameters.filter((p: any) => p.in === "query"); | |
| 57 | expect(q.map((p: any) => p.name)).toEqual(["state", "limit", "cursor", "label", "author"]); | |
| 58 | expect(q.find((p: any) => p.name === "limit").schema.type).toBe("integer"); | |
| 59 | expect(q.find((p: any) => p.name === "state").schema.enum).toEqual(["open", "closed", "all"]); | |
| 60 | expect(get.parameters.filter((p: any) => p.in === "path").length).toBe(2); | |
| 61 | } | |
| 62 | }); | |
| 63 | ||
| 50 | 64 | test("reading and closing one issue and reading one pull are described", () => { |
| 51 | 65 | const doc = openapi("https://f.test", "1") as any; |
| 52 | 66 | const R = "/api/v1/repos/{owner}/{name}"; |
new above
offline
The enum is keyed on the parameter name
state, so any future route with astatequery parameter gets open/closed/all whether or not it should. Let the query entry carry its own enum.