radiant-docs 0.1.58 → 0.1.60
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/template/astro.config.mjs +24 -2
- package/template/package-lock.json +216 -513
- package/template/package.json +13 -3
- package/template/scripts/generate-og-images.mjs +338 -6
- package/template/scripts/generate-og-metadata.mjs +29 -0
- package/template/src/components/Footer.astro +1 -1
- package/template/src/components/Header.astro +6 -13
- package/template/src/components/MdxPage.astro +3 -1
- package/template/src/components/OpenApiPage.astro +26 -832
- package/template/src/components/Sidebar.astro +1 -1
- package/template/src/components/SidebarGroup.astro +1 -1
- package/template/src/components/ThemeSwitcher.astro +5 -15
- package/template/src/components/chat/AssistantEmbedPanel.tsx +1 -1
- package/template/src/components/chat/AssistantEmbedPanelPage.astro +15 -2
- package/template/src/components/endpoint/PlaygroundButton.astro +1 -1
- package/template/src/components/endpoint/PlaygroundField.astro +1 -1
- package/template/src/components/endpoint/PlaygroundForm.astro +9 -5
- package/template/src/components/endpoint/ResponseFields.astro +3 -3
- package/template/src/components/ui/Field.astro +4 -4
- package/template/src/components/user/Callout.astro +2 -2
- package/template/src/components/user/Card.astro +2 -2
- package/template/src/components/user/Step.astro +3 -1
- package/template/src/layouts/Layout.astro +21 -46
- package/template/src/lib/ai-artifacts.ts +792 -0
- package/template/src/lib/font-css.ts +376 -0
- package/template/src/lib/mdx/remark-resolve-internal-links.ts +22 -8
- package/template/src/lib/oas.ts +5 -1
- package/template/src/lib/openapi/operation-doc.ts +1150 -0
- package/template/src/lib/page-description.ts +20 -0
- package/template/src/lib/routes.ts +73 -18
- package/template/src/pages/-/fonts/[...font].ts +50 -0
- package/template/src/pages/404.astro +2 -2
- package/template/src/pages/[...slug]/index.md.ts +35 -0
- package/template/src/pages/[...spec].json.ts +33 -0
- package/template/src/pages/[...spec].yaml.ts +33 -0
- package/template/src/pages/[...spec].yml.ts +33 -0
- package/template/src/pages/index.md.ts +17 -0
- package/template/src/pages/llms-full.txt.ts +11 -0
- package/template/src/pages/llms.txt.ts +11 -0
- package/template/src/styles/global.css +32 -7
- package/template/src/assets/fonts/geist-mono/cyrillic.woff2 +0 -0
- package/template/src/assets/fonts/geist-mono/latin-ext.woff2 +0 -0
- package/template/src/assets/fonts/geist-mono/latin.woff2 +0 -0
- package/template/src/assets/fonts/google-sans-flex/canadian-aboriginal.woff2 +0 -0
- package/template/src/assets/fonts/google-sans-flex/cherokee.woff2 +0 -0
- package/template/src/assets/fonts/google-sans-flex/latin-ext.woff2 +0 -0
- package/template/src/assets/fonts/google-sans-flex/latin.woff2 +0 -0
- package/template/src/assets/fonts/google-sans-flex/math.woff2 +0 -0
- package/template/src/assets/fonts/google-sans-flex/nushu.woff2 +0 -0
- package/template/src/assets/fonts/google-sans-flex/symbols.woff2 +0 -0
- package/template/src/assets/fonts/google-sans-flex/syriac.woff2 +0 -0
- package/template/src/assets/fonts/google-sans-flex/tifinagh.woff2 +0 -0
- package/template/src/assets/fonts/google-sans-flex/vietnamese.woff2 +0 -0
- package/template/src/styles/geist-mono.css +0 -33
- package/template/src/styles/google-sans-flex.css +0 -143
- package/template/src/styles/vaul.css +0 -255
|
@@ -0,0 +1,1150 @@
|
|
|
1
|
+
import type { HttpMethods } from "oas/types";
|
|
2
|
+
import type { OpenApiRoute } from "../routes";
|
|
3
|
+
import { getOasInstance } from "../oas";
|
|
4
|
+
import { loadOpenApiSpec } from "../validation";
|
|
5
|
+
|
|
6
|
+
export const OPENAPI_REQUEST_SECTION_KEYS = [
|
|
7
|
+
"header",
|
|
8
|
+
"cookie",
|
|
9
|
+
"path",
|
|
10
|
+
"query",
|
|
11
|
+
"body",
|
|
12
|
+
] as const;
|
|
13
|
+
|
|
14
|
+
export type OpenApiRequestSectionKey =
|
|
15
|
+
(typeof OPENAPI_REQUEST_SECTION_KEYS)[number];
|
|
16
|
+
|
|
17
|
+
export const OPENAPI_REQUEST_SECTION_LABELS: Record<
|
|
18
|
+
OpenApiRequestSectionKey,
|
|
19
|
+
string
|
|
20
|
+
> = {
|
|
21
|
+
header: "Header",
|
|
22
|
+
cookie: "Cookie",
|
|
23
|
+
path: "Path Parameters",
|
|
24
|
+
query: "Query Parameters",
|
|
25
|
+
body: "Body",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export interface OpenApiFieldVariant {
|
|
29
|
+
label: string;
|
|
30
|
+
fields: OpenApiField[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface OpenApiField {
|
|
34
|
+
name: string;
|
|
35
|
+
required: boolean;
|
|
36
|
+
type: string;
|
|
37
|
+
description: string;
|
|
38
|
+
enum?: (string | number)[];
|
|
39
|
+
minLength?: number;
|
|
40
|
+
maxLength?: number;
|
|
41
|
+
minimum?: number;
|
|
42
|
+
maximum?: number;
|
|
43
|
+
exclusiveMinimum?: number;
|
|
44
|
+
exclusiveMaximum?: number;
|
|
45
|
+
hasDefault?: boolean;
|
|
46
|
+
defaultValue?: unknown;
|
|
47
|
+
isArray?: boolean;
|
|
48
|
+
style?: string;
|
|
49
|
+
explode?: boolean;
|
|
50
|
+
nested?: OpenApiField[];
|
|
51
|
+
variants?: OpenApiFieldVariant[];
|
|
52
|
+
variantType?: "oneOf" | "anyOf";
|
|
53
|
+
isAdditionalProperty?: boolean;
|
|
54
|
+
mapKnownKeys?: string[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type OpenApiRequestFields = Record<
|
|
58
|
+
OpenApiRequestSectionKey,
|
|
59
|
+
OpenApiField[]
|
|
60
|
+
>;
|
|
61
|
+
|
|
62
|
+
export interface OpenApiRequestSectionVariantData {
|
|
63
|
+
variants: OpenApiFieldVariant[];
|
|
64
|
+
variantType: "oneOf" | "anyOf";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type OpenApiBodyDefaultKind = "object" | "array";
|
|
68
|
+
|
|
69
|
+
export interface OpenApiOperationDoc {
|
|
70
|
+
api: any;
|
|
71
|
+
definition: any;
|
|
72
|
+
operation: any;
|
|
73
|
+
operationSpecSlice: any;
|
|
74
|
+
route: OpenApiRoute;
|
|
75
|
+
title: string;
|
|
76
|
+
description?: string;
|
|
77
|
+
method: string;
|
|
78
|
+
path: string;
|
|
79
|
+
responses: any;
|
|
80
|
+
serverUrl?: string;
|
|
81
|
+
requestFields: OpenApiRequestFields;
|
|
82
|
+
requestSectionVariants: Partial<
|
|
83
|
+
Record<OpenApiRequestSectionKey, OpenApiRequestSectionVariantData>
|
|
84
|
+
>;
|
|
85
|
+
bodyDescription: string;
|
|
86
|
+
bodyDefaultKind?: OpenApiBodyDefaultKind;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function createEmptyRequestFields(): OpenApiRequestFields {
|
|
90
|
+
return {
|
|
91
|
+
header: [],
|
|
92
|
+
cookie: [],
|
|
93
|
+
path: [],
|
|
94
|
+
query: [],
|
|
95
|
+
body: [],
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function getSchemaVariants(schema: any): any[] {
|
|
100
|
+
if (!schema) return [];
|
|
101
|
+
if (Array.isArray(schema.oneOf)) return schema.oneOf;
|
|
102
|
+
if (Array.isArray(schema.anyOf)) return schema.anyOf;
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function getSchemaVariantType(schema: any): "oneOf" | "anyOf" | undefined {
|
|
107
|
+
if (Array.isArray(schema?.oneOf) && schema.oneOf.length > 0) return "oneOf";
|
|
108
|
+
if (Array.isArray(schema?.anyOf) && schema.anyOf.length > 0) return "anyOf";
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function getAdditionalPropertiesSchema(schema: any): any {
|
|
113
|
+
if (!schema || typeof schema !== "object") return undefined;
|
|
114
|
+
|
|
115
|
+
let additionalProperties = schema.additionalProperties;
|
|
116
|
+
if (Array.isArray(schema.allOf)) {
|
|
117
|
+
schema.allOf.forEach((part: any) => {
|
|
118
|
+
const partAdditional = getAdditionalPropertiesSchema(part);
|
|
119
|
+
if (partAdditional !== undefined) {
|
|
120
|
+
additionalProperties = partAdditional;
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return additionalProperties;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function getSchemaEnumValues(schema: any): (string | number)[] | undefined {
|
|
129
|
+
if (!schema) return undefined;
|
|
130
|
+
if (Array.isArray(schema.enum) && schema.enum.length > 0) {
|
|
131
|
+
const values = schema.enum.filter(
|
|
132
|
+
(value: unknown): value is string | number =>
|
|
133
|
+
typeof value === "string" || typeof value === "number",
|
|
134
|
+
);
|
|
135
|
+
return values.length > 0 ? values : undefined;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const variants = getSchemaVariants(schema);
|
|
139
|
+
if (variants.length === 0) return undefined;
|
|
140
|
+
|
|
141
|
+
const deduped = new Map<string, string | number>();
|
|
142
|
+
variants.forEach((variant) => {
|
|
143
|
+
const variantEnum = getSchemaEnumValues(variant);
|
|
144
|
+
variantEnum?.forEach((value) => {
|
|
145
|
+
deduped.set(`${typeof value}:${String(value)}`, value);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
return deduped.size > 0 ? Array.from(deduped.values()) : undefined;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function parseFiniteNumber(value: unknown): number | undefined {
|
|
153
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
154
|
+
? value
|
|
155
|
+
: undefined;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function parseNonNegativeInteger(value: unknown): number | undefined {
|
|
159
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
|
|
160
|
+
if (!Number.isInteger(value) || value < 0) return undefined;
|
|
161
|
+
return value;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function getDirectSchemaNumericConstraints(schema: any): {
|
|
165
|
+
minimum?: number;
|
|
166
|
+
maximum?: number;
|
|
167
|
+
exclusiveMinimum?: number;
|
|
168
|
+
exclusiveMaximum?: number;
|
|
169
|
+
} {
|
|
170
|
+
const minimum = parseFiniteNumber(schema?.minimum);
|
|
171
|
+
const maximum = parseFiniteNumber(schema?.maximum);
|
|
172
|
+
const exclusiveMinimumValue = parseFiniteNumber(schema?.exclusiveMinimum);
|
|
173
|
+
const exclusiveMaximumValue = parseFiniteNumber(schema?.exclusiveMaximum);
|
|
174
|
+
|
|
175
|
+
const exclusiveMinimum =
|
|
176
|
+
exclusiveMinimumValue !== undefined
|
|
177
|
+
? exclusiveMinimumValue
|
|
178
|
+
: schema?.exclusiveMinimum === true
|
|
179
|
+
? minimum
|
|
180
|
+
: undefined;
|
|
181
|
+
const exclusiveMaximum =
|
|
182
|
+
exclusiveMaximumValue !== undefined
|
|
183
|
+
? exclusiveMaximumValue
|
|
184
|
+
: schema?.exclusiveMaximum === true
|
|
185
|
+
? maximum
|
|
186
|
+
: undefined;
|
|
187
|
+
|
|
188
|
+
const constraints: {
|
|
189
|
+
minimum?: number;
|
|
190
|
+
maximum?: number;
|
|
191
|
+
exclusiveMinimum?: number;
|
|
192
|
+
exclusiveMaximum?: number;
|
|
193
|
+
} = {};
|
|
194
|
+
|
|
195
|
+
if (exclusiveMinimum !== undefined) {
|
|
196
|
+
constraints.exclusiveMinimum = exclusiveMinimum;
|
|
197
|
+
} else if (minimum !== undefined) {
|
|
198
|
+
constraints.minimum = minimum;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (exclusiveMaximum !== undefined) {
|
|
202
|
+
constraints.exclusiveMaximum = exclusiveMaximum;
|
|
203
|
+
} else if (maximum !== undefined) {
|
|
204
|
+
constraints.maximum = maximum;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return constraints;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function mergeConjunctiveNumericConstraints(
|
|
211
|
+
base: {
|
|
212
|
+
minimum?: number;
|
|
213
|
+
maximum?: number;
|
|
214
|
+
exclusiveMinimum?: number;
|
|
215
|
+
exclusiveMaximum?: number;
|
|
216
|
+
},
|
|
217
|
+
next: {
|
|
218
|
+
minimum?: number;
|
|
219
|
+
maximum?: number;
|
|
220
|
+
exclusiveMinimum?: number;
|
|
221
|
+
exclusiveMaximum?: number;
|
|
222
|
+
},
|
|
223
|
+
): {
|
|
224
|
+
minimum?: number;
|
|
225
|
+
maximum?: number;
|
|
226
|
+
exclusiveMinimum?: number;
|
|
227
|
+
exclusiveMaximum?: number;
|
|
228
|
+
} {
|
|
229
|
+
const toLowerBound = (constraint: typeof base) => {
|
|
230
|
+
if (constraint.exclusiveMinimum !== undefined) {
|
|
231
|
+
return { value: constraint.exclusiveMinimum, exclusive: true };
|
|
232
|
+
}
|
|
233
|
+
if (constraint.minimum !== undefined) {
|
|
234
|
+
return { value: constraint.minimum, exclusive: false };
|
|
235
|
+
}
|
|
236
|
+
return undefined;
|
|
237
|
+
};
|
|
238
|
+
const toUpperBound = (constraint: typeof base) => {
|
|
239
|
+
if (constraint.exclusiveMaximum !== undefined) {
|
|
240
|
+
return { value: constraint.exclusiveMaximum, exclusive: true };
|
|
241
|
+
}
|
|
242
|
+
if (constraint.maximum !== undefined) {
|
|
243
|
+
return { value: constraint.maximum, exclusive: false };
|
|
244
|
+
}
|
|
245
|
+
return undefined;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const pickStricterLower = (
|
|
249
|
+
first?: { value: number; exclusive: boolean },
|
|
250
|
+
second?: { value: number; exclusive: boolean },
|
|
251
|
+
) => {
|
|
252
|
+
if (!first) return second;
|
|
253
|
+
if (!second) return first;
|
|
254
|
+
if (first.value > second.value) return first;
|
|
255
|
+
if (second.value > first.value) return second;
|
|
256
|
+
return {
|
|
257
|
+
value: first.value,
|
|
258
|
+
exclusive: first.exclusive || second.exclusive,
|
|
259
|
+
};
|
|
260
|
+
};
|
|
261
|
+
const pickStricterUpper = (
|
|
262
|
+
first?: { value: number; exclusive: boolean },
|
|
263
|
+
second?: { value: number; exclusive: boolean },
|
|
264
|
+
) => {
|
|
265
|
+
if (!first) return second;
|
|
266
|
+
if (!second) return first;
|
|
267
|
+
if (first.value < second.value) return first;
|
|
268
|
+
if (second.value < first.value) return second;
|
|
269
|
+
return {
|
|
270
|
+
value: first.value,
|
|
271
|
+
exclusive: first.exclusive || second.exclusive,
|
|
272
|
+
};
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const lower = pickStricterLower(toLowerBound(base), toLowerBound(next));
|
|
276
|
+
const upper = pickStricterUpper(toUpperBound(base), toUpperBound(next));
|
|
277
|
+
|
|
278
|
+
const merged: {
|
|
279
|
+
minimum?: number;
|
|
280
|
+
maximum?: number;
|
|
281
|
+
exclusiveMinimum?: number;
|
|
282
|
+
exclusiveMaximum?: number;
|
|
283
|
+
} = {};
|
|
284
|
+
if (lower) {
|
|
285
|
+
if (lower.exclusive) merged.exclusiveMinimum = lower.value;
|
|
286
|
+
else merged.minimum = lower.value;
|
|
287
|
+
}
|
|
288
|
+
if (upper) {
|
|
289
|
+
if (upper.exclusive) merged.exclusiveMaximum = upper.value;
|
|
290
|
+
else merged.maximum = upper.value;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return merged;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function getSchemaNumericConstraints(
|
|
297
|
+
schema: any,
|
|
298
|
+
seen = new Set<any>(),
|
|
299
|
+
): {
|
|
300
|
+
minimum?: number;
|
|
301
|
+
maximum?: number;
|
|
302
|
+
exclusiveMinimum?: number;
|
|
303
|
+
exclusiveMaximum?: number;
|
|
304
|
+
} {
|
|
305
|
+
if (!schema || typeof schema !== "object") return {};
|
|
306
|
+
if (seen.has(schema)) return {};
|
|
307
|
+
|
|
308
|
+
const nextSeen = new Set(seen);
|
|
309
|
+
nextSeen.add(schema);
|
|
310
|
+
|
|
311
|
+
let constraints = getDirectSchemaNumericConstraints(schema);
|
|
312
|
+
|
|
313
|
+
if (Array.isArray(schema.allOf)) {
|
|
314
|
+
schema.allOf.forEach((part: any) => {
|
|
315
|
+
constraints = mergeConjunctiveNumericConstraints(
|
|
316
|
+
constraints,
|
|
317
|
+
getSchemaNumericConstraints(part, nextSeen),
|
|
318
|
+
);
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const variants = getSchemaVariants(schema);
|
|
323
|
+
if (variants.length > 0) {
|
|
324
|
+
const variantConstraints = variants.map((variant) =>
|
|
325
|
+
getSchemaNumericConstraints(variant, nextSeen),
|
|
326
|
+
);
|
|
327
|
+
const shared: {
|
|
328
|
+
minimum?: number;
|
|
329
|
+
maximum?: number;
|
|
330
|
+
exclusiveMinimum?: number;
|
|
331
|
+
exclusiveMaximum?: number;
|
|
332
|
+
} = {};
|
|
333
|
+
const keys = [
|
|
334
|
+
"minimum",
|
|
335
|
+
"maximum",
|
|
336
|
+
"exclusiveMinimum",
|
|
337
|
+
"exclusiveMaximum",
|
|
338
|
+
] as const;
|
|
339
|
+
|
|
340
|
+
keys.forEach((key) => {
|
|
341
|
+
const firstValue = variantConstraints[0]?.[key];
|
|
342
|
+
if (firstValue === undefined) return;
|
|
343
|
+
const allMatch = variantConstraints.every(
|
|
344
|
+
(variantConstraint) => variantConstraint[key] === firstValue,
|
|
345
|
+
);
|
|
346
|
+
if (allMatch) {
|
|
347
|
+
shared[key] = firstValue;
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
constraints = mergeConjunctiveNumericConstraints(constraints, shared);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return constraints;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function getDirectSchemaStringLengthConstraints(schema: any): {
|
|
358
|
+
minLength?: number;
|
|
359
|
+
maxLength?: number;
|
|
360
|
+
} {
|
|
361
|
+
const minLength = parseNonNegativeInteger(schema?.minLength);
|
|
362
|
+
const maxLength = parseNonNegativeInteger(schema?.maxLength);
|
|
363
|
+
|
|
364
|
+
const constraints: {
|
|
365
|
+
minLength?: number;
|
|
366
|
+
maxLength?: number;
|
|
367
|
+
} = {};
|
|
368
|
+
if (minLength !== undefined) constraints.minLength = minLength;
|
|
369
|
+
if (maxLength !== undefined) constraints.maxLength = maxLength;
|
|
370
|
+
|
|
371
|
+
return constraints;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function mergeConjunctiveStringLengthConstraints(
|
|
375
|
+
base: {
|
|
376
|
+
minLength?: number;
|
|
377
|
+
maxLength?: number;
|
|
378
|
+
},
|
|
379
|
+
next: {
|
|
380
|
+
minLength?: number;
|
|
381
|
+
maxLength?: number;
|
|
382
|
+
},
|
|
383
|
+
): {
|
|
384
|
+
minLength?: number;
|
|
385
|
+
maxLength?: number;
|
|
386
|
+
} {
|
|
387
|
+
const merged: {
|
|
388
|
+
minLength?: number;
|
|
389
|
+
maxLength?: number;
|
|
390
|
+
} = {};
|
|
391
|
+
|
|
392
|
+
const minLengthCandidates = [base.minLength, next.minLength].filter(
|
|
393
|
+
(value): value is number => value !== undefined,
|
|
394
|
+
);
|
|
395
|
+
const maxLengthCandidates = [base.maxLength, next.maxLength].filter(
|
|
396
|
+
(value): value is number => value !== undefined,
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
if (minLengthCandidates.length > 0) {
|
|
400
|
+
merged.minLength = Math.max(...minLengthCandidates);
|
|
401
|
+
}
|
|
402
|
+
if (maxLengthCandidates.length > 0) {
|
|
403
|
+
merged.maxLength = Math.min(...maxLengthCandidates);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return merged;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function getSchemaStringLengthConstraints(
|
|
410
|
+
schema: any,
|
|
411
|
+
seen = new Set<any>(),
|
|
412
|
+
): {
|
|
413
|
+
minLength?: number;
|
|
414
|
+
maxLength?: number;
|
|
415
|
+
} {
|
|
416
|
+
if (!schema || typeof schema !== "object") return {};
|
|
417
|
+
if (seen.has(schema)) return {};
|
|
418
|
+
|
|
419
|
+
const nextSeen = new Set(seen);
|
|
420
|
+
nextSeen.add(schema);
|
|
421
|
+
|
|
422
|
+
let constraints = getDirectSchemaStringLengthConstraints(schema);
|
|
423
|
+
|
|
424
|
+
if (Array.isArray(schema.allOf)) {
|
|
425
|
+
schema.allOf.forEach((part: any) => {
|
|
426
|
+
constraints = mergeConjunctiveStringLengthConstraints(
|
|
427
|
+
constraints,
|
|
428
|
+
getSchemaStringLengthConstraints(part, nextSeen),
|
|
429
|
+
);
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const variants = getSchemaVariants(schema);
|
|
434
|
+
if (variants.length > 0) {
|
|
435
|
+
const variantConstraints = variants.map((variant) =>
|
|
436
|
+
getSchemaStringLengthConstraints(variant, nextSeen),
|
|
437
|
+
);
|
|
438
|
+
const shared: {
|
|
439
|
+
minLength?: number;
|
|
440
|
+
maxLength?: number;
|
|
441
|
+
} = {};
|
|
442
|
+
|
|
443
|
+
(["minLength", "maxLength"] as const).forEach((key) => {
|
|
444
|
+
const firstValue = variantConstraints[0]?.[key];
|
|
445
|
+
if (firstValue === undefined) return;
|
|
446
|
+
const allMatch = variantConstraints.every(
|
|
447
|
+
(variantConstraint) => variantConstraint[key] === firstValue,
|
|
448
|
+
);
|
|
449
|
+
if (allMatch) {
|
|
450
|
+
shared[key] = firstValue;
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
constraints = mergeConjunctiveStringLengthConstraints(constraints, shared);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return constraints;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function getSchemaTypeLabel(schema: any): string {
|
|
461
|
+
if (!schema) return "unknown";
|
|
462
|
+
|
|
463
|
+
const variants = getSchemaVariants(schema);
|
|
464
|
+
if (variants.length > 0) {
|
|
465
|
+
const labels = Array.from(
|
|
466
|
+
new Set(
|
|
467
|
+
variants
|
|
468
|
+
.map((variant) => getSchemaTypeLabel(variant))
|
|
469
|
+
.filter((label) => label.length > 0),
|
|
470
|
+
),
|
|
471
|
+
);
|
|
472
|
+
if (labels.length === 0) return "unknown";
|
|
473
|
+
return labels.length === 1 ? labels[0] : labels.join(" | ");
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (schema.type === "array") {
|
|
477
|
+
const itemSchema = schema.items;
|
|
478
|
+
if (!itemSchema) return "unknown[]";
|
|
479
|
+
const itemType = getSchemaTypeLabel(itemSchema);
|
|
480
|
+
if (itemType.includes(" | ")) return `(${itemType})[]`;
|
|
481
|
+
return `${itemType}[]`;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (Array.isArray(schema.enum) && schema.enum.length > 0) {
|
|
485
|
+
return `enum<${schema.type || "string"}>`;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (schema.format && schema.type) {
|
|
489
|
+
return `${schema.type} (${schema.format})`;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (schema.type) return schema.type;
|
|
493
|
+
if (schema.properties || schema.allOf || schema.oneOf || schema.anyOf) {
|
|
494
|
+
return "object";
|
|
495
|
+
}
|
|
496
|
+
return "unknown";
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function getSchemaStructuralType(
|
|
500
|
+
schema: any,
|
|
501
|
+
): OpenApiBodyDefaultKind | undefined {
|
|
502
|
+
if (!schema || typeof schema !== "object") return undefined;
|
|
503
|
+
|
|
504
|
+
if (schema.type === "object") return "object";
|
|
505
|
+
if (schema.type === "array") return "array";
|
|
506
|
+
|
|
507
|
+
const variants = getSchemaVariants(schema);
|
|
508
|
+
if (variants.length > 0) {
|
|
509
|
+
const variantTypes = Array.from(
|
|
510
|
+
new Set(
|
|
511
|
+
variants
|
|
512
|
+
.map((variant) => getSchemaStructuralType(variant))
|
|
513
|
+
.filter(
|
|
514
|
+
(variantType): variantType is OpenApiBodyDefaultKind =>
|
|
515
|
+
variantType === "object" || variantType === "array",
|
|
516
|
+
),
|
|
517
|
+
),
|
|
518
|
+
);
|
|
519
|
+
if (variantTypes.length === 1) return variantTypes[0];
|
|
520
|
+
return undefined;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (
|
|
524
|
+
(schema.properties && typeof schema.properties === "object") ||
|
|
525
|
+
schema.additionalProperties !== undefined
|
|
526
|
+
) {
|
|
527
|
+
return "object";
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (schema.items) return "array";
|
|
531
|
+
|
|
532
|
+
if (Array.isArray(schema.allOf) && schema.allOf.length > 0) {
|
|
533
|
+
const allOfTypes = Array.from(
|
|
534
|
+
new Set(
|
|
535
|
+
schema.allOf
|
|
536
|
+
.map((part: any) => getSchemaStructuralType(part))
|
|
537
|
+
.filter(
|
|
538
|
+
(partType): partType is OpenApiBodyDefaultKind =>
|
|
539
|
+
partType === "object" || partType === "array",
|
|
540
|
+
),
|
|
541
|
+
),
|
|
542
|
+
);
|
|
543
|
+
if (allOfTypes.length === 1) return allOfTypes[0];
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return undefined;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function getTopLevelObjectShape(schema: any): {
|
|
550
|
+
properties: Record<string, any>;
|
|
551
|
+
required: Set<string>;
|
|
552
|
+
hasObjectShape: boolean;
|
|
553
|
+
} {
|
|
554
|
+
if (!schema) {
|
|
555
|
+
return {
|
|
556
|
+
properties: {},
|
|
557
|
+
required: new Set<string>(),
|
|
558
|
+
hasObjectShape: false,
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
let properties: Record<string, any> = {};
|
|
563
|
+
const required = new Set<string>(
|
|
564
|
+
Array.isArray(schema.required) ? schema.required : [],
|
|
565
|
+
);
|
|
566
|
+
let hasObjectShape = false;
|
|
567
|
+
|
|
568
|
+
if (schema.properties && typeof schema.properties === "object") {
|
|
569
|
+
properties = { ...properties, ...schema.properties };
|
|
570
|
+
hasObjectShape = true;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (Array.isArray(schema.allOf)) {
|
|
574
|
+
schema.allOf.forEach((part: any) => {
|
|
575
|
+
const partShape = getTopLevelObjectShape(part);
|
|
576
|
+
if (!partShape.hasObjectShape) return;
|
|
577
|
+
|
|
578
|
+
properties = { ...properties, ...partShape.properties };
|
|
579
|
+
partShape.required.forEach((name) => required.add(name));
|
|
580
|
+
hasObjectShape = true;
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const variants = getSchemaVariants(schema);
|
|
585
|
+
if (variants.length > 0) {
|
|
586
|
+
const variantShapes = variants
|
|
587
|
+
.map((variant) => getTopLevelObjectShape(variant))
|
|
588
|
+
.filter((shape) => shape.hasObjectShape);
|
|
589
|
+
|
|
590
|
+
if (variantShapes.length > 0) {
|
|
591
|
+
variantShapes.forEach((shape) => {
|
|
592
|
+
properties = { ...properties, ...shape.properties };
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
const [firstShape, ...restShapes] = variantShapes;
|
|
596
|
+
const requiredAcrossVariants = new Set<string>(firstShape.required);
|
|
597
|
+
restShapes.forEach((shape) => {
|
|
598
|
+
Array.from(requiredAcrossVariants).forEach((name) => {
|
|
599
|
+
if (!shape.required.has(name)) {
|
|
600
|
+
requiredAcrossVariants.delete(name);
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
});
|
|
604
|
+
requiredAcrossVariants.forEach((name) => required.add(name));
|
|
605
|
+
hasObjectShape = true;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
return { properties, required, hasObjectShape };
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function extractRequestSchemaContent(
|
|
613
|
+
schema: any,
|
|
614
|
+
seen = new Set<any>(),
|
|
615
|
+
): {
|
|
616
|
+
fields: OpenApiField[];
|
|
617
|
+
variants?: OpenApiFieldVariant[];
|
|
618
|
+
variantType?: "oneOf" | "anyOf";
|
|
619
|
+
} {
|
|
620
|
+
if (!schema || typeof schema !== "object") return { fields: [] };
|
|
621
|
+
if (seen.has(schema)) return { fields: [] };
|
|
622
|
+
|
|
623
|
+
const nextSeen = new Set(seen);
|
|
624
|
+
nextSeen.add(schema);
|
|
625
|
+
|
|
626
|
+
if (schema.type === "array" && schema.items) {
|
|
627
|
+
return extractRequestSchemaContent(schema.items, nextSeen);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const baseSchema = { ...schema };
|
|
631
|
+
delete baseSchema.oneOf;
|
|
632
|
+
delete baseSchema.anyOf;
|
|
633
|
+
|
|
634
|
+
const shape = getTopLevelObjectShape(baseSchema);
|
|
635
|
+
|
|
636
|
+
const fields: OpenApiField[] = Object.entries(shape.properties)
|
|
637
|
+
.sort(([nameA], [nameB]) => {
|
|
638
|
+
const aRequired = shape.required.has(nameA);
|
|
639
|
+
const bRequired = shape.required.has(nameB);
|
|
640
|
+
if (aRequired && !bRequired) return -1;
|
|
641
|
+
if (!aRequired && bRequired) return 1;
|
|
642
|
+
return 0;
|
|
643
|
+
})
|
|
644
|
+
.map(([name, propertySchema]: [string, any]) => {
|
|
645
|
+
const nestedContent = extractRequestSchemaContent(
|
|
646
|
+
propertySchema,
|
|
647
|
+
nextSeen,
|
|
648
|
+
);
|
|
649
|
+
return {
|
|
650
|
+
name,
|
|
651
|
+
required: shape.required.has(name),
|
|
652
|
+
type: getSchemaTypeLabel(propertySchema),
|
|
653
|
+
description: propertySchema?.description || "",
|
|
654
|
+
enum: getSchemaEnumValues(propertySchema),
|
|
655
|
+
...getSchemaStringLengthConstraints(propertySchema, nextSeen),
|
|
656
|
+
...getSchemaNumericConstraints(propertySchema, nextSeen),
|
|
657
|
+
hasDefault: Object.prototype.hasOwnProperty.call(
|
|
658
|
+
propertySchema || {},
|
|
659
|
+
"default",
|
|
660
|
+
),
|
|
661
|
+
defaultValue: propertySchema?.default,
|
|
662
|
+
isArray: propertySchema?.type === "array",
|
|
663
|
+
nested:
|
|
664
|
+
nestedContent.fields.length > 0 ? nestedContent.fields : undefined,
|
|
665
|
+
variants: nestedContent.variants,
|
|
666
|
+
variantType: nestedContent.variantType,
|
|
667
|
+
};
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
const additionalProperties = getAdditionalPropertiesSchema(baseSchema);
|
|
671
|
+
const supportsAdditionalProperties =
|
|
672
|
+
additionalProperties === true ||
|
|
673
|
+
(additionalProperties &&
|
|
674
|
+
typeof additionalProperties === "object" &&
|
|
675
|
+
!Array.isArray(additionalProperties));
|
|
676
|
+
|
|
677
|
+
if (supportsAdditionalProperties) {
|
|
678
|
+
const additionalSchema =
|
|
679
|
+
additionalProperties && typeof additionalProperties === "object"
|
|
680
|
+
? additionalProperties
|
|
681
|
+
: null;
|
|
682
|
+
const additionalContent = additionalSchema
|
|
683
|
+
? extractRequestSchemaContent(additionalSchema, nextSeen)
|
|
684
|
+
: { fields: [] as OpenApiField[] };
|
|
685
|
+
|
|
686
|
+
fields.push({
|
|
687
|
+
name: "[key: string]",
|
|
688
|
+
required: false,
|
|
689
|
+
type: additionalSchema ? getSchemaTypeLabel(additionalSchema) : "any",
|
|
690
|
+
description: additionalSchema?.description || "",
|
|
691
|
+
enum: additionalSchema
|
|
692
|
+
? getSchemaEnumValues(additionalSchema)
|
|
693
|
+
: undefined,
|
|
694
|
+
...getSchemaStringLengthConstraints(additionalSchema, nextSeen),
|
|
695
|
+
...getSchemaNumericConstraints(additionalSchema, nextSeen),
|
|
696
|
+
hasDefault: Object.prototype.hasOwnProperty.call(
|
|
697
|
+
additionalSchema || {},
|
|
698
|
+
"default",
|
|
699
|
+
),
|
|
700
|
+
defaultValue: additionalSchema?.default,
|
|
701
|
+
isArray: additionalSchema?.type === "array",
|
|
702
|
+
nested:
|
|
703
|
+
additionalContent.fields.length > 0
|
|
704
|
+
? additionalContent.fields
|
|
705
|
+
: undefined,
|
|
706
|
+
variants: additionalContent.variants,
|
|
707
|
+
variantType: additionalContent.variantType,
|
|
708
|
+
isAdditionalProperty: true,
|
|
709
|
+
mapKnownKeys: Object.keys(shape.properties),
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const variantType = getSchemaVariantType(schema);
|
|
714
|
+
const variantSchemas =
|
|
715
|
+
variantType === "oneOf"
|
|
716
|
+
? schema.oneOf || []
|
|
717
|
+
: variantType === "anyOf"
|
|
718
|
+
? schema.anyOf || []
|
|
719
|
+
: [];
|
|
720
|
+
const variants =
|
|
721
|
+
variantType && variantSchemas.length > 0
|
|
722
|
+
? variantSchemas
|
|
723
|
+
.map((variantSchema: any, index: number) => ({
|
|
724
|
+
label: `Variant ${index + 1}`,
|
|
725
|
+
fields: extractRequestSchemaContent(variantSchema, nextSeen).fields,
|
|
726
|
+
}))
|
|
727
|
+
.filter((variant: OpenApiFieldVariant) => variant.fields.length > 0)
|
|
728
|
+
: [];
|
|
729
|
+
|
|
730
|
+
return {
|
|
731
|
+
fields,
|
|
732
|
+
variants: variants.length > 0 ? variants : undefined,
|
|
733
|
+
variantType: variants.length > 0 ? variantType : undefined,
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function addSecurityFields(args: {
|
|
738
|
+
operation: any;
|
|
739
|
+
definition: any;
|
|
740
|
+
requestFields: OpenApiRequestFields;
|
|
741
|
+
}) {
|
|
742
|
+
const securityRequirements = args.operation.getSecurity();
|
|
743
|
+
const schemes = args.definition.components?.securitySchemes || {};
|
|
744
|
+
|
|
745
|
+
if (!securityRequirements || securityRequirements.length === 0) return;
|
|
746
|
+
|
|
747
|
+
const firstOption = securityRequirements[0];
|
|
748
|
+
|
|
749
|
+
Object.keys(firstOption).forEach((key) => {
|
|
750
|
+
const scheme = schemes[key];
|
|
751
|
+
if (!scheme) return;
|
|
752
|
+
|
|
753
|
+
const fieldData: OpenApiField = {
|
|
754
|
+
name: "",
|
|
755
|
+
required: true,
|
|
756
|
+
type: "string",
|
|
757
|
+
description: scheme.description || "",
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
if (scheme.type === "apiKey") {
|
|
761
|
+
fieldData.name = scheme.name;
|
|
762
|
+
|
|
763
|
+
if (!fieldData.description) {
|
|
764
|
+
fieldData.description = `API Key required in ${scheme.in}.`;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
if (scheme.in === "header") args.requestFields.header.push(fieldData);
|
|
768
|
+
else if (scheme.in === "query") args.requestFields.query.push(fieldData);
|
|
769
|
+
else if (scheme.in === "cookie") {
|
|
770
|
+
args.requestFields.cookie.push(fieldData);
|
|
771
|
+
}
|
|
772
|
+
} else if (scheme.type === "http") {
|
|
773
|
+
fieldData.name = "Authorization";
|
|
774
|
+
|
|
775
|
+
if (scheme.scheme === "bearer") {
|
|
776
|
+
fieldData.description =
|
|
777
|
+
fieldData.description || "Bearer token authentication.";
|
|
778
|
+
} else if (scheme.scheme === "basic") {
|
|
779
|
+
fieldData.description =
|
|
780
|
+
fieldData.description ||
|
|
781
|
+
"Basic authentication (Base64 encoded username:password).";
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
args.requestFields.header.push(fieldData);
|
|
785
|
+
} else if (scheme.type === "oauth2" || scheme.type === "openIdConnect") {
|
|
786
|
+
fieldData.name = "Authorization";
|
|
787
|
+
fieldData.description = fieldData.description || "OAuth2 Bearer Token.";
|
|
788
|
+
|
|
789
|
+
args.requestFields.header.push(fieldData);
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function buildOpenApiRequestModel(args: {
|
|
795
|
+
operation: any;
|
|
796
|
+
definition: any;
|
|
797
|
+
}): {
|
|
798
|
+
requestFields: OpenApiRequestFields;
|
|
799
|
+
requestSectionVariants: Partial<
|
|
800
|
+
Record<OpenApiRequestSectionKey, OpenApiRequestSectionVariantData>
|
|
801
|
+
>;
|
|
802
|
+
bodyDescription: string;
|
|
803
|
+
bodyDefaultKind?: OpenApiBodyDefaultKind;
|
|
804
|
+
} {
|
|
805
|
+
const requestFields = createEmptyRequestFields();
|
|
806
|
+
const requestSectionVariants: Partial<
|
|
807
|
+
Record<OpenApiRequestSectionKey, OpenApiRequestSectionVariantData>
|
|
808
|
+
> = {};
|
|
809
|
+
let bodyDescription = "";
|
|
810
|
+
let bodyDefaultKind: OpenApiBodyDefaultKind | undefined = undefined;
|
|
811
|
+
|
|
812
|
+
addSecurityFields({
|
|
813
|
+
operation: args.operation,
|
|
814
|
+
definition: args.definition,
|
|
815
|
+
requestFields,
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
const parameters = args.operation.getParameters();
|
|
819
|
+
parameters.forEach((param: any) => {
|
|
820
|
+
const schema = param.schema as any;
|
|
821
|
+
const isArray = schema?.type === "array";
|
|
822
|
+
const schemaContent = extractRequestSchemaContent(schema);
|
|
823
|
+
|
|
824
|
+
const field: OpenApiField = {
|
|
825
|
+
name: param.name,
|
|
826
|
+
required: param.required || false,
|
|
827
|
+
type: getSchemaTypeLabel(schema),
|
|
828
|
+
description: param.description || "",
|
|
829
|
+
enum: getSchemaEnumValues(schema),
|
|
830
|
+
...getSchemaStringLengthConstraints(schema),
|
|
831
|
+
...getSchemaNumericConstraints(schema),
|
|
832
|
+
hasDefault: Object.prototype.hasOwnProperty.call(schema || {}, "default"),
|
|
833
|
+
defaultValue: schema?.default,
|
|
834
|
+
isArray,
|
|
835
|
+
style: param.style,
|
|
836
|
+
explode: param.explode,
|
|
837
|
+
nested: schemaContent.fields.length > 0 ? schemaContent.fields : undefined,
|
|
838
|
+
variants: schemaContent.variants,
|
|
839
|
+
variantType: schemaContent.variantType,
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
if (param.in === "path") requestFields.path.push(field);
|
|
843
|
+
if (param.in === "query") requestFields.query.push(field);
|
|
844
|
+
if (param.in === "header") requestFields.header.push(field);
|
|
845
|
+
if (param.in === "cookie") requestFields.cookie.push(field);
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
if (args.operation.hasRequestBody()) {
|
|
849
|
+
const requestBody = args.operation.getRequestBody("application/json");
|
|
850
|
+
const requestBodyObject = args.operation.schema.requestBody;
|
|
851
|
+
const bodySchema = (requestBody as any)?.schema as any;
|
|
852
|
+
bodyDescription =
|
|
853
|
+
requestBodyObject?.description ||
|
|
854
|
+
(requestBody as any)?.description ||
|
|
855
|
+
bodySchema?.description ||
|
|
856
|
+
"";
|
|
857
|
+
bodyDefaultKind = getSchemaStructuralType(bodySchema);
|
|
858
|
+
const bodyContent = extractRequestSchemaContent(bodySchema);
|
|
859
|
+
requestFields.body.push(...bodyContent.fields);
|
|
860
|
+
|
|
861
|
+
if (bodyContent.variants && bodyContent.variantType) {
|
|
862
|
+
requestSectionVariants.body = {
|
|
863
|
+
variants: bodyContent.variants,
|
|
864
|
+
variantType: bodyContent.variantType,
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
return {
|
|
870
|
+
requestFields,
|
|
871
|
+
requestSectionVariants,
|
|
872
|
+
bodyDescription,
|
|
873
|
+
bodyDefaultKind,
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function cloneJson<T>(value: T): T {
|
|
878
|
+
if (value === undefined) return value;
|
|
879
|
+
return JSON.parse(JSON.stringify(value));
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function decodeJsonPointerSegment(segment: string): string {
|
|
883
|
+
return decodeURIComponent(segment).replace(/~1/g, "/").replace(/~0/g, "~");
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function getJsonPointerSegments(ref: string): string[] | null {
|
|
887
|
+
if (!ref.startsWith("#/")) return null;
|
|
888
|
+
|
|
889
|
+
return ref.slice(2).split("/").map(decodeJsonPointerSegment);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function getByJsonPointer(source: any, ref: string): unknown {
|
|
893
|
+
const segments = getJsonPointerSegments(ref);
|
|
894
|
+
if (!segments) return undefined;
|
|
895
|
+
|
|
896
|
+
let current = source;
|
|
897
|
+
for (const segment of segments) {
|
|
898
|
+
if (!current || typeof current !== "object") return undefined;
|
|
899
|
+
current = current[segment];
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
return current;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function setByJsonPointer(target: any, ref: string, value: unknown) {
|
|
906
|
+
const segments = getJsonPointerSegments(ref);
|
|
907
|
+
if (!segments || segments.length === 0) return;
|
|
908
|
+
|
|
909
|
+
let current = target;
|
|
910
|
+
segments.slice(0, -1).forEach((segment) => {
|
|
911
|
+
if (
|
|
912
|
+
!Object.prototype.hasOwnProperty.call(current, segment) ||
|
|
913
|
+
!current[segment] ||
|
|
914
|
+
typeof current[segment] !== "object" ||
|
|
915
|
+
Array.isArray(current[segment])
|
|
916
|
+
) {
|
|
917
|
+
current[segment] = {};
|
|
918
|
+
}
|
|
919
|
+
current = current[segment];
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
current[segments[segments.length - 1]] = cloneJson(value);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function collectLocalRefs(args: {
|
|
926
|
+
value: unknown;
|
|
927
|
+
source: any;
|
|
928
|
+
target: any;
|
|
929
|
+
seenRefs: Set<string>;
|
|
930
|
+
}) {
|
|
931
|
+
if (Array.isArray(args.value)) {
|
|
932
|
+
args.value.forEach((item) =>
|
|
933
|
+
collectLocalRefs({
|
|
934
|
+
value: item,
|
|
935
|
+
source: args.source,
|
|
936
|
+
target: args.target,
|
|
937
|
+
seenRefs: args.seenRefs,
|
|
938
|
+
}),
|
|
939
|
+
);
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
if (!args.value || typeof args.value !== "object") return;
|
|
944
|
+
|
|
945
|
+
const ref = (args.value as { $ref?: unknown }).$ref;
|
|
946
|
+
if (typeof ref === "string" && ref.startsWith("#/")) {
|
|
947
|
+
if (!args.seenRefs.has(ref)) {
|
|
948
|
+
args.seenRefs.add(ref);
|
|
949
|
+
const targetValue = getByJsonPointer(args.source, ref);
|
|
950
|
+
|
|
951
|
+
if (targetValue !== undefined) {
|
|
952
|
+
setByJsonPointer(args.target, ref, targetValue);
|
|
953
|
+
collectLocalRefs({
|
|
954
|
+
value: targetValue,
|
|
955
|
+
source: args.source,
|
|
956
|
+
target: args.target,
|
|
957
|
+
seenRefs: args.seenRefs,
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
Object.values(args.value).forEach((child) =>
|
|
964
|
+
collectLocalRefs({
|
|
965
|
+
value: child,
|
|
966
|
+
source: args.source,
|
|
967
|
+
target: args.target,
|
|
968
|
+
seenRefs: args.seenRefs,
|
|
969
|
+
}),
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function getPathItemForOperation(args: {
|
|
974
|
+
spec: any;
|
|
975
|
+
path: string;
|
|
976
|
+
}): { pathKey: string; pathItem: any } | null {
|
|
977
|
+
const paths = args.spec?.paths;
|
|
978
|
+
if (!paths || typeof paths !== "object") return null;
|
|
979
|
+
|
|
980
|
+
const targetPath = args.path.toLowerCase();
|
|
981
|
+
for (const [pathKey, pathItem] of Object.entries(paths)) {
|
|
982
|
+
if (pathKey.toLowerCase() !== targetPath) continue;
|
|
983
|
+
if (!pathItem || typeof pathItem !== "object") return null;
|
|
984
|
+
|
|
985
|
+
return { pathKey, pathItem };
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
return null;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function getSecurityRequirementForSlice(args: {
|
|
992
|
+
spec: any;
|
|
993
|
+
operation: any;
|
|
994
|
+
}): unknown {
|
|
995
|
+
if (Object.prototype.hasOwnProperty.call(args.operation, "security")) {
|
|
996
|
+
return args.operation.security;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
return args.spec?.security;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
function collectSecuritySchemes(args: {
|
|
1003
|
+
source: any;
|
|
1004
|
+
target: any;
|
|
1005
|
+
securityRequirements: unknown;
|
|
1006
|
+
}) {
|
|
1007
|
+
if (!Array.isArray(args.securityRequirements)) return;
|
|
1008
|
+
|
|
1009
|
+
args.securityRequirements.forEach((requirement) => {
|
|
1010
|
+
if (!requirement || typeof requirement !== "object") return;
|
|
1011
|
+
|
|
1012
|
+
Object.keys(requirement).forEach((schemeName) => {
|
|
1013
|
+
const scheme = args.source?.components?.securitySchemes?.[schemeName];
|
|
1014
|
+
if (!scheme) return;
|
|
1015
|
+
|
|
1016
|
+
args.target.components ??= {};
|
|
1017
|
+
args.target.components.securitySchemes ??= {};
|
|
1018
|
+
args.target.components.securitySchemes[schemeName] = cloneJson(scheme);
|
|
1019
|
+
});
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
function addRelevantTags(args: {
|
|
1024
|
+
source: any;
|
|
1025
|
+
target: any;
|
|
1026
|
+
operation: any;
|
|
1027
|
+
}) {
|
|
1028
|
+
if (
|
|
1029
|
+
!args.operation ||
|
|
1030
|
+
!Array.isArray(args.source?.tags) ||
|
|
1031
|
+
!Array.isArray(args.operation.tags)
|
|
1032
|
+
) {
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
const operationTagNames = new Set(args.operation.tags);
|
|
1037
|
+
const tags = args.source.tags.filter(
|
|
1038
|
+
(tag: any) => tag?.name && operationTagNames.has(tag.name),
|
|
1039
|
+
);
|
|
1040
|
+
|
|
1041
|
+
if (tags.length > 0) {
|
|
1042
|
+
args.target.tags = cloneJson(tags);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
export function buildOpenApiOperationSpecSlice(args: {
|
|
1047
|
+
spec: any;
|
|
1048
|
+
path: string;
|
|
1049
|
+
method: string;
|
|
1050
|
+
}): any {
|
|
1051
|
+
const method = args.method.toLowerCase();
|
|
1052
|
+
const pathMatch = getPathItemForOperation({
|
|
1053
|
+
spec: args.spec,
|
|
1054
|
+
path: args.path,
|
|
1055
|
+
});
|
|
1056
|
+
const pathKey = pathMatch?.pathKey ?? args.path;
|
|
1057
|
+
const pathItem = pathMatch?.pathItem ?? {};
|
|
1058
|
+
const operation = pathItem[method];
|
|
1059
|
+
|
|
1060
|
+
const slicedPathItem: Record<string, unknown> = {};
|
|
1061
|
+
if (pathItem.parameters) {
|
|
1062
|
+
slicedPathItem.parameters = cloneJson(pathItem.parameters);
|
|
1063
|
+
}
|
|
1064
|
+
if (pathItem.servers) {
|
|
1065
|
+
slicedPathItem.servers = cloneJson(pathItem.servers);
|
|
1066
|
+
}
|
|
1067
|
+
if (operation) {
|
|
1068
|
+
slicedPathItem[method] = cloneJson(operation);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
const slice: Record<string, unknown> = {
|
|
1072
|
+
...(args.spec.openapi ? { openapi: args.spec.openapi } : {}),
|
|
1073
|
+
...(args.spec.swagger ? { swagger: args.spec.swagger } : {}),
|
|
1074
|
+
...(args.spec.info ? { info: cloneJson(args.spec.info) } : {}),
|
|
1075
|
+
...(args.spec.servers ? { servers: cloneJson(args.spec.servers) } : {}),
|
|
1076
|
+
paths: {
|
|
1077
|
+
[pathKey]: slicedPathItem,
|
|
1078
|
+
},
|
|
1079
|
+
};
|
|
1080
|
+
|
|
1081
|
+
const securityRequirements = operation
|
|
1082
|
+
? getSecurityRequirementForSlice({
|
|
1083
|
+
spec: args.spec,
|
|
1084
|
+
operation,
|
|
1085
|
+
})
|
|
1086
|
+
: undefined;
|
|
1087
|
+
|
|
1088
|
+
if (
|
|
1089
|
+
securityRequirements !== undefined &&
|
|
1090
|
+
!Object.prototype.hasOwnProperty.call(operation || {}, "security")
|
|
1091
|
+
) {
|
|
1092
|
+
slice.security = cloneJson(securityRequirements);
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
addRelevantTags({
|
|
1096
|
+
source: args.spec,
|
|
1097
|
+
target: slice,
|
|
1098
|
+
operation,
|
|
1099
|
+
});
|
|
1100
|
+
collectSecuritySchemes({
|
|
1101
|
+
source: args.spec,
|
|
1102
|
+
target: slice,
|
|
1103
|
+
securityRequirements,
|
|
1104
|
+
});
|
|
1105
|
+
collectLocalRefs({
|
|
1106
|
+
value: slice.paths,
|
|
1107
|
+
source: args.spec,
|
|
1108
|
+
target: slice,
|
|
1109
|
+
seenRefs: new Set<string>(),
|
|
1110
|
+
});
|
|
1111
|
+
|
|
1112
|
+
return slice;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
export async function getOpenApiOperationDoc(
|
|
1116
|
+
route: OpenApiRoute,
|
|
1117
|
+
): Promise<OpenApiOperationDoc> {
|
|
1118
|
+
const [api, spec] = await Promise.all([
|
|
1119
|
+
getOasInstance(route.filePath),
|
|
1120
|
+
loadOpenApiSpec(route.filePath),
|
|
1121
|
+
]);
|
|
1122
|
+
const definition = api.getDefinition();
|
|
1123
|
+
const operation = api.operation(
|
|
1124
|
+
route.openApiPath,
|
|
1125
|
+
route.openApiMethod as HttpMethods,
|
|
1126
|
+
);
|
|
1127
|
+
const requestModel = buildOpenApiRequestModel({
|
|
1128
|
+
operation,
|
|
1129
|
+
definition,
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
return {
|
|
1133
|
+
api,
|
|
1134
|
+
definition,
|
|
1135
|
+
operation,
|
|
1136
|
+
operationSpecSlice: buildOpenApiOperationSpecSlice({
|
|
1137
|
+
spec,
|
|
1138
|
+
path: route.openApiPath,
|
|
1139
|
+
method: route.openApiMethod,
|
|
1140
|
+
}),
|
|
1141
|
+
route,
|
|
1142
|
+
title: route.title,
|
|
1143
|
+
description: operation.getDescription() || undefined,
|
|
1144
|
+
method: route.openApiMethod,
|
|
1145
|
+
path: route.openApiPath,
|
|
1146
|
+
responses: operation.schema.responses,
|
|
1147
|
+
serverUrl: definition.servers?.[0]?.url,
|
|
1148
|
+
...requestModel,
|
|
1149
|
+
};
|
|
1150
|
+
}
|