two-stroke 8.1.0 → 8.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/open-api.ts +149 -72
package/package.json
CHANGED
package/src/open-api.ts
CHANGED
|
@@ -3,85 +3,162 @@ import { type Route } from "./types";
|
|
|
3
3
|
|
|
4
4
|
import type { ZodType } from "zod/v4";
|
|
5
5
|
|
|
6
|
+
const REF_PREFIX = "#/components/schemas/";
|
|
7
|
+
const DEFS_PREFIX = "#/$defs/";
|
|
8
|
+
|
|
9
|
+
const rewriteRefs = (node: unknown, defs: Record<string, string>, self?: string): unknown => {
|
|
10
|
+
if (Array.isArray(node)) return node.map((n) => rewriteRefs(n, defs, self));
|
|
11
|
+
if (typeof node !== "object" || node === null) return node;
|
|
12
|
+
return Object.fromEntries(
|
|
13
|
+
Object.entries(node).map(([k, v]) => {
|
|
14
|
+
if (k === "$ref" && typeof v === "string") {
|
|
15
|
+
if (v === "#" && self !== undefined) return [k, REF_PREFIX + self];
|
|
16
|
+
const def = v.startsWith(DEFS_PREFIX) ? defs[v.slice(DEFS_PREFIX.length)] : undefined;
|
|
17
|
+
if (def !== undefined) return [k, REF_PREFIX + def];
|
|
18
|
+
}
|
|
19
|
+
return [k, rewriteRefs(v, defs, self)];
|
|
20
|
+
}),
|
|
21
|
+
);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const uniqueName = (schemas: Record<string, unknown>, base: string) => {
|
|
25
|
+
if (!(base in schemas)) return base;
|
|
26
|
+
for (let i = 2; ; i++) if (!(`${base}_${i}` in schemas)) return `${base}_${i}`;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const jsonSchema = (
|
|
30
|
+
schemas: Record<string, unknown>,
|
|
31
|
+
name: string,
|
|
32
|
+
schema: ZodType,
|
|
33
|
+
io: "input" | "output",
|
|
34
|
+
) => {
|
|
35
|
+
const {
|
|
36
|
+
$schema: _$schema,
|
|
37
|
+
$defs,
|
|
38
|
+
...json
|
|
39
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
40
|
+
} = z.toJSONSchema(schema, { io }) as Record<string, unknown> & {
|
|
41
|
+
$defs?: Record<string, unknown>;
|
|
42
|
+
};
|
|
43
|
+
// Self references (`#`) only resolve if the schema itself lives in `components.schemas`.
|
|
44
|
+
if ($defs === undefined && !JSON.stringify(json).includes(`"$ref":"#"`)) return json;
|
|
45
|
+
|
|
46
|
+
// Reserve every name before rewriting so sibling `$defs` can reference each other.
|
|
47
|
+
const root = uniqueName(schemas, name);
|
|
48
|
+
schemas[root] = null;
|
|
49
|
+
const defs: Record<string, string> = {};
|
|
50
|
+
for (const key of Object.keys($defs ?? {})) {
|
|
51
|
+
const def = uniqueName(schemas, `${root}_${key}`);
|
|
52
|
+
defs[key] = def;
|
|
53
|
+
schemas[def] = null;
|
|
54
|
+
}
|
|
55
|
+
schemas[root] = rewriteRefs(json, defs, root);
|
|
56
|
+
for (const [key, def] of Object.entries($defs ?? {})) {
|
|
57
|
+
schemas[defs[key] ?? key] = rewriteRefs(def, defs, root);
|
|
58
|
+
}
|
|
59
|
+
return { $ref: REF_PREFIX + root };
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const schemaName = (method: string, path: string, suffix: string) =>
|
|
63
|
+
`${method.toLocaleLowerCase()}${path.replace(/[^a-zA-Z0-9]+/g, "_").replace(/_$/, "")}_${suffix}`;
|
|
64
|
+
|
|
6
65
|
export const openAPI =
|
|
7
66
|
<T, A>(title: string, release: string, noAuth: () => A, routes: Route<T, A>[]) =>
|
|
8
|
-
async () =>
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
schema: z.toJSONSchema(r.input, {
|
|
58
|
-
io: "input",
|
|
59
|
-
}),
|
|
60
|
-
}
|
|
61
|
-
: undefined,
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
}
|
|
65
|
-
: {}),
|
|
66
|
-
responses: {
|
|
67
|
-
"200": {
|
|
68
|
-
description: "OK",
|
|
69
|
-
content: {
|
|
70
|
-
"application/json": {
|
|
71
|
-
schema: z.toJSONSchema(r.output),
|
|
67
|
+
async () => {
|
|
68
|
+
const schemas: Record<string, unknown> = {};
|
|
69
|
+
const paths = Object.fromEntries(
|
|
70
|
+
Object.entries(Object.groupBy(routes, ({ path }) => path)).map(([path, rs]) => [
|
|
71
|
+
path,
|
|
72
|
+
Object.fromEntries(
|
|
73
|
+
(rs ?? []).map((r) => [
|
|
74
|
+
r.method.toLocaleLowerCase(),
|
|
75
|
+
{
|
|
76
|
+
parameters: [
|
|
77
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
78
|
+
...Object.entries((r.params?.shape ?? {}) as Record<string, ZodType>).map(
|
|
79
|
+
([k, v]) => ({
|
|
80
|
+
name: k,
|
|
81
|
+
in: "query",
|
|
82
|
+
required: !v.safeParse(undefined).success,
|
|
83
|
+
schema: jsonSchema(
|
|
84
|
+
schemas,
|
|
85
|
+
schemaName(r.method, r.path, `param_${k}`),
|
|
86
|
+
v,
|
|
87
|
+
"input",
|
|
88
|
+
),
|
|
89
|
+
}),
|
|
90
|
+
),
|
|
91
|
+
...Array.from(r.path.matchAll(/\/{(?<name>[^}]*)}/g), (match) => ({
|
|
92
|
+
name: match.groups!.name,
|
|
93
|
+
in: "path",
|
|
94
|
+
required: true,
|
|
95
|
+
schema: {
|
|
96
|
+
type: "string",
|
|
97
|
+
},
|
|
98
|
+
})),
|
|
99
|
+
],
|
|
100
|
+
...(r.auth === noAuth ? {} : { security: [{ auth: [] }] }),
|
|
101
|
+
...(r.method === "POST" || r.method === "PUT"
|
|
102
|
+
? {
|
|
103
|
+
requestBody: {
|
|
104
|
+
required: true,
|
|
105
|
+
content: {
|
|
106
|
+
"application/json": r.input
|
|
107
|
+
? {
|
|
108
|
+
schema: jsonSchema(
|
|
109
|
+
schemas,
|
|
110
|
+
schemaName(r.method, r.path, "request"),
|
|
111
|
+
r.input,
|
|
112
|
+
"input",
|
|
113
|
+
),
|
|
114
|
+
}
|
|
115
|
+
: undefined,
|
|
72
116
|
},
|
|
73
117
|
},
|
|
118
|
+
}
|
|
119
|
+
: {}),
|
|
120
|
+
responses: {
|
|
121
|
+
"200": {
|
|
122
|
+
description: "OK",
|
|
123
|
+
content: {
|
|
124
|
+
"application/json": {
|
|
125
|
+
schema: jsonSchema(
|
|
126
|
+
schemas,
|
|
127
|
+
schemaName(r.method, r.path, "response"),
|
|
128
|
+
r.output,
|
|
129
|
+
"output",
|
|
130
|
+
),
|
|
131
|
+
},
|
|
74
132
|
},
|
|
75
|
-
"400": status400,
|
|
76
|
-
"500": status500,
|
|
77
133
|
},
|
|
134
|
+
"400": status400,
|
|
135
|
+
"500": status500,
|
|
78
136
|
},
|
|
79
|
-
|
|
80
|
-
),
|
|
81
|
-
|
|
82
|
-
),
|
|
83
|
-
|
|
84
|
-
|
|
137
|
+
},
|
|
138
|
+
]),
|
|
139
|
+
),
|
|
140
|
+
]),
|
|
141
|
+
);
|
|
142
|
+
return {
|
|
143
|
+
body: {
|
|
144
|
+
openapi: "3.1.0",
|
|
145
|
+
info: {
|
|
146
|
+
title,
|
|
147
|
+
version: release,
|
|
148
|
+
},
|
|
149
|
+
components: {
|
|
150
|
+
securitySchemes: {
|
|
151
|
+
auth: {
|
|
152
|
+
type: "http",
|
|
153
|
+
scheme: "bearer",
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
...(Object.keys(schemas).length > 0 ? { schemas } : {}),
|
|
157
|
+
},
|
|
158
|
+
paths,
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
};
|
|
85
162
|
|
|
86
163
|
const status500 = {
|
|
87
164
|
description: "Invalid Request",
|