@redocly/client-generator 0.0.0-snapshot.1784554531
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/README.md +506 -0
- package/lib/config-file.d.ts +8 -0
- package/lib/config-file.d.ts.map +1 -0
- package/lib/config-file.js +32 -0
- package/lib/config-file.js.map +1 -0
- package/lib/config.d.ts +63 -0
- package/lib/config.d.ts.map +1 -0
- package/lib/config.js +2 -0
- package/lib/config.js.map +1 -0
- package/lib/emitters/auth.d.ts +14 -0
- package/lib/emitters/auth.d.ts.map +1 -0
- package/lib/emitters/auth.js +30 -0
- package/lib/emitters/auth.js.map +1 -0
- package/lib/emitters/client-assembly.d.ts +15 -0
- package/lib/emitters/client-assembly.d.ts.map +1 -0
- package/lib/emitters/client-assembly.js +299 -0
- package/lib/emitters/client-assembly.js.map +1 -0
- package/lib/emitters/descriptor.d.ts +22 -0
- package/lib/emitters/descriptor.d.ts.map +1 -0
- package/lib/emitters/descriptor.js +207 -0
- package/lib/emitters/descriptor.js.map +1 -0
- package/lib/emitters/emit-options.d.ts +71 -0
- package/lib/emitters/emit-options.d.ts.map +1 -0
- package/lib/emitters/emit-options.js +28 -0
- package/lib/emitters/emit-options.js.map +1 -0
- package/lib/emitters/faker.d.ts +11 -0
- package/lib/emitters/faker.d.ts.map +1 -0
- package/lib/emitters/faker.js +204 -0
- package/lib/emitters/faker.js.map +1 -0
- package/lib/emitters/identifier.d.ts +28 -0
- package/lib/emitters/identifier.d.ts.map +1 -0
- package/lib/emitters/identifier.js +94 -0
- package/lib/emitters/identifier.js.map +1 -0
- package/lib/emitters/inline-runtime.d.ts +11 -0
- package/lib/emitters/inline-runtime.d.ts.map +1 -0
- package/lib/emitters/inline-runtime.js +97 -0
- package/lib/emitters/inline-runtime.js.map +1 -0
- package/lib/emitters/jsdoc.d.ts +9 -0
- package/lib/emitters/jsdoc.d.ts.map +1 -0
- package/lib/emitters/jsdoc.js +83 -0
- package/lib/emitters/jsdoc.js.map +1 -0
- package/lib/emitters/mock.d.ts +22 -0
- package/lib/emitters/mock.d.ts.map +1 -0
- package/lib/emitters/mock.js +243 -0
- package/lib/emitters/mock.js.map +1 -0
- package/lib/emitters/operation-aliases.d.ts +27 -0
- package/lib/emitters/operation-aliases.d.ts.map +1 -0
- package/lib/emitters/operation-aliases.js +138 -0
- package/lib/emitters/operation-aliases.js.map +1 -0
- package/lib/emitters/operation-signature.d.ts +23 -0
- package/lib/emitters/operation-signature.d.ts.map +1 -0
- package/lib/emitters/operation-signature.js +34 -0
- package/lib/emitters/operation-signature.js.map +1 -0
- package/lib/emitters/operation-types.d.ts +32 -0
- package/lib/emitters/operation-types.d.ts.map +1 -0
- package/lib/emitters/operation-types.js +117 -0
- package/lib/emitters/operation-types.js.map +1 -0
- package/lib/emitters/operations.d.ts +39 -0
- package/lib/emitters/operations.d.ts.map +1 -0
- package/lib/emitters/operations.js +33 -0
- package/lib/emitters/operations.js.map +1 -0
- package/lib/emitters/pagination.d.ts +69 -0
- package/lib/emitters/pagination.d.ts.map +1 -0
- package/lib/emitters/pagination.js +223 -0
- package/lib/emitters/pagination.js.map +1 -0
- package/lib/emitters/runtime-sources.d.ts +16 -0
- package/lib/emitters/runtime-sources.d.ts.map +1 -0
- package/lib/emitters/runtime-sources.js +16 -0
- package/lib/emitters/runtime-sources.js.map +1 -0
- package/lib/emitters/sample.d.ts +27 -0
- package/lib/emitters/sample.d.ts.map +1 -0
- package/lib/emitters/sample.js +161 -0
- package/lib/emitters/sample.js.map +1 -0
- package/lib/emitters/setup-bake.d.ts +15 -0
- package/lib/emitters/setup-bake.d.ts.map +1 -0
- package/lib/emitters/setup-bake.js +65 -0
- package/lib/emitters/setup-bake.js.map +1 -0
- package/lib/emitters/sse.d.ts +10 -0
- package/lib/emitters/sse.d.ts.map +1 -0
- package/lib/emitters/sse.js +46 -0
- package/lib/emitters/sse.js.map +1 -0
- package/lib/emitters/support.d.ts +13 -0
- package/lib/emitters/support.d.ts.map +1 -0
- package/lib/emitters/support.js +21 -0
- package/lib/emitters/support.js.map +1 -0
- package/lib/emitters/swr.d.ts +10 -0
- package/lib/emitters/swr.d.ts.map +1 -0
- package/lib/emitters/swr.js +88 -0
- package/lib/emitters/swr.js.map +1 -0
- package/lib/emitters/tanstack-query.d.ts +11 -0
- package/lib/emitters/tanstack-query.d.ts.map +1 -0
- package/lib/emitters/tanstack-query.js +68 -0
- package/lib/emitters/tanstack-query.js.map +1 -0
- package/lib/emitters/transformers.d.ts +11 -0
- package/lib/emitters/transformers.d.ts.map +1 -0
- package/lib/emitters/transformers.js +334 -0
- package/lib/emitters/transformers.js.map +1 -0
- package/lib/emitters/ts.d.ts +40 -0
- package/lib/emitters/ts.d.ts.map +1 -0
- package/lib/emitters/ts.js +104 -0
- package/lib/emitters/ts.js.map +1 -0
- package/lib/emitters/type-guards.d.ts +21 -0
- package/lib/emitters/type-guards.d.ts.map +1 -0
- package/lib/emitters/type-guards.js +183 -0
- package/lib/emitters/type-guards.js.map +1 -0
- package/lib/emitters/types.d.ts +23 -0
- package/lib/emitters/types.d.ts.map +1 -0
- package/lib/emitters/types.js +127 -0
- package/lib/emitters/types.js.map +1 -0
- package/lib/emitters/wrapper-support.d.ts +40 -0
- package/lib/emitters/wrapper-support.d.ts.map +1 -0
- package/lib/emitters/wrapper-support.js +112 -0
- package/lib/emitters/wrapper-support.js.map +1 -0
- package/lib/emitters/zod.d.ts +13 -0
- package/lib/emitters/zod.d.ts.map +1 -0
- package/lib/emitters/zod.js +344 -0
- package/lib/emitters/zod.js.map +1 -0
- package/lib/errors.d.ts +4 -0
- package/lib/errors.d.ts.map +1 -0
- package/lib/errors.js +7 -0
- package/lib/errors.js.map +1 -0
- package/lib/generators/anchor.d.ts +9 -0
- package/lib/generators/anchor.d.ts.map +1 -0
- package/lib/generators/anchor.js +10 -0
- package/lib/generators/anchor.js.map +1 -0
- package/lib/generators/index.d.ts +15 -0
- package/lib/generators/index.d.ts.map +1 -0
- package/lib/generators/index.js +62 -0
- package/lib/generators/index.js.map +1 -0
- package/lib/generators/mock.d.ts +9 -0
- package/lib/generators/mock.d.ts.map +1 -0
- package/lib/generators/mock.js +23 -0
- package/lib/generators/mock.js.map +1 -0
- package/lib/generators/resolve.d.ts +19 -0
- package/lib/generators/resolve.d.ts.map +1 -0
- package/lib/generators/resolve.js +68 -0
- package/lib/generators/resolve.js.map +1 -0
- package/lib/generators/sdk.d.ts +12 -0
- package/lib/generators/sdk.d.ts.map +1 -0
- package/lib/generators/sdk.js +26 -0
- package/lib/generators/sdk.js.map +1 -0
- package/lib/generators/swr.d.ts +15 -0
- package/lib/generators/swr.d.ts.map +1 -0
- package/lib/generators/swr.js +27 -0
- package/lib/generators/swr.js.map +1 -0
- package/lib/generators/tanstack-query.d.ts +17 -0
- package/lib/generators/tanstack-query.d.ts.map +1 -0
- package/lib/generators/tanstack-query.js +30 -0
- package/lib/generators/tanstack-query.js.map +1 -0
- package/lib/generators/transformers.d.ts +19 -0
- package/lib/generators/transformers.d.ts.map +1 -0
- package/lib/generators/transformers.js +28 -0
- package/lib/generators/transformers.js.map +1 -0
- package/lib/generators/types.d.ts +66 -0
- package/lib/generators/types.d.ts.map +1 -0
- package/lib/generators/types.js +2 -0
- package/lib/generators/types.js.map +1 -0
- package/lib/generators/zod.d.ts +14 -0
- package/lib/generators/zod.d.ts.map +1 -0
- package/lib/generators/zod.js +23 -0
- package/lib/generators/zod.js.map +1 -0
- package/lib/index.d.ts +29 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +119 -0
- package/lib/index.js.map +1 -0
- package/lib/intermediate-representation/build.d.ts +4 -0
- package/lib/intermediate-representation/build.d.ts.map +1 -0
- package/lib/intermediate-representation/build.js +761 -0
- package/lib/intermediate-representation/build.js.map +1 -0
- package/lib/intermediate-representation/model.d.ts +275 -0
- package/lib/intermediate-representation/model.d.ts.map +1 -0
- package/lib/intermediate-representation/model.js +5 -0
- package/lib/intermediate-representation/model.js.map +1 -0
- package/lib/intermediate-representation/normalize-swagger2.d.ts +10 -0
- package/lib/intermediate-representation/normalize-swagger2.d.ts.map +1 -0
- package/lib/intermediate-representation/normalize-swagger2.js +210 -0
- package/lib/intermediate-representation/normalize-swagger2.js.map +1 -0
- package/lib/intermediate-representation/sanitize-identifiers.d.ts +30 -0
- package/lib/intermediate-representation/sanitize-identifiers.d.ts.map +1 -0
- package/lib/intermediate-representation/sanitize-identifiers.js +153 -0
- package/lib/intermediate-representation/sanitize-identifiers.js.map +1 -0
- package/lib/loader.d.ts +4 -0
- package/lib/loader.d.ts.map +1 -0
- package/lib/loader.js +14 -0
- package/lib/loader.js.map +1 -0
- package/lib/plugin.d.ts +19 -0
- package/lib/plugin.d.ts.map +1 -0
- package/lib/plugin.js +41 -0
- package/lib/plugin.js.map +1 -0
- package/lib/runtime/auth.d.ts +15 -0
- package/lib/runtime/auth.d.ts.map +1 -0
- package/lib/runtime/auth.js +67 -0
- package/lib/runtime/auth.js.map +1 -0
- package/lib/runtime/create-client.d.ts +35 -0
- package/lib/runtime/create-client.d.ts.map +1 -0
- package/lib/runtime/create-client.js +215 -0
- package/lib/runtime/create-client.js.map +1 -0
- package/lib/runtime/errors.d.ts +11 -0
- package/lib/runtime/errors.d.ts.map +1 -0
- package/lib/runtime/errors.js +25 -0
- package/lib/runtime/errors.js.map +1 -0
- package/lib/runtime/index.d.ts +14 -0
- package/lib/runtime/index.d.ts.map +1 -0
- package/lib/runtime/index.js +24 -0
- package/lib/runtime/index.js.map +1 -0
- package/lib/runtime/multipart.d.ts +9 -0
- package/lib/runtime/multipart.d.ts.map +1 -0
- package/lib/runtime/multipart.js +31 -0
- package/lib/runtime/multipart.js.map +1 -0
- package/lib/runtime/paginate.d.ts +36 -0
- package/lib/runtime/paginate.d.ts.map +1 -0
- package/lib/runtime/paginate.js +99 -0
- package/lib/runtime/paginate.js.map +1 -0
- package/lib/runtime/parse.d.ts +9 -0
- package/lib/runtime/parse.d.ts.map +1 -0
- package/lib/runtime/parse.js +37 -0
- package/lib/runtime/parse.js.map +1 -0
- package/lib/runtime/retry.d.ts +15 -0
- package/lib/runtime/retry.d.ts.map +1 -0
- package/lib/runtime/retry.js +51 -0
- package/lib/runtime/retry.js.map +1 -0
- package/lib/runtime/send.d.ts +31 -0
- package/lib/runtime/send.d.ts.map +1 -0
- package/lib/runtime/send.js +138 -0
- package/lib/runtime/send.js.map +1 -0
- package/lib/runtime/setup.d.ts +11 -0
- package/lib/runtime/setup.d.ts.map +1 -0
- package/lib/runtime/setup.js +13 -0
- package/lib/runtime/setup.js.map +1 -0
- package/lib/runtime/sse.d.ts +21 -0
- package/lib/runtime/sse.d.ts.map +1 -0
- package/lib/runtime/sse.js +163 -0
- package/lib/runtime/sse.js.map +1 -0
- package/lib/runtime/types.d.ts +232 -0
- package/lib/runtime/types.d.ts.map +1 -0
- package/lib/runtime/types.js +9 -0
- package/lib/runtime/types.js.map +1 -0
- package/lib/runtime/url.d.ts +25 -0
- package/lib/runtime/url.d.ts.map +1 -0
- package/lib/runtime/url.js +97 -0
- package/lib/runtime/url.js.map +1 -0
- package/lib/runtime-contract.d.ts +27 -0
- package/lib/runtime-contract.d.ts.map +1 -0
- package/lib/runtime-contract.js +14 -0
- package/lib/runtime-contract.js.map +1 -0
- package/lib/types.d.ts +118 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +2 -0
- package/lib/types.js.map +1 -0
- package/package.json +59 -0
|
@@ -0,0 +1,761 @@
|
|
|
1
|
+
import { isPlainObject, isRef, logger, } from '@redocly/openapi-core';
|
|
2
|
+
import { NotSupportedError } from '../errors.js';
|
|
3
|
+
import { assertSafeIdentifiers, sanitizeIdentifiers } from './sanitize-identifiers.js';
|
|
4
|
+
const HTTP_METHODS = ['get', 'post', 'put', 'delete', 'patch', 'head', 'options'];
|
|
5
|
+
function resolveRef(doc, ref) {
|
|
6
|
+
if (!ref.startsWith('#/')) {
|
|
7
|
+
throw new NotSupportedError(`External $ref not supported: ${ref}`);
|
|
8
|
+
}
|
|
9
|
+
const segments = ref
|
|
10
|
+
.slice(2)
|
|
11
|
+
.split('/')
|
|
12
|
+
.map((s) => s.replace(/~1/g, '/').replace(/~0/g, '~'));
|
|
13
|
+
let current = doc;
|
|
14
|
+
for (const segment of segments) {
|
|
15
|
+
if (!isPlainObject(current) && !Array.isArray(current)) {
|
|
16
|
+
throw new NotSupportedError(`Cannot resolve $ref: ${ref}`);
|
|
17
|
+
}
|
|
18
|
+
current = current[segment];
|
|
19
|
+
}
|
|
20
|
+
if (current === undefined) {
|
|
21
|
+
throw new NotSupportedError(`Cannot resolve $ref: ${ref}`);
|
|
22
|
+
}
|
|
23
|
+
return current;
|
|
24
|
+
}
|
|
25
|
+
function deref(doc, value) {
|
|
26
|
+
if (isRef(value)) {
|
|
27
|
+
return resolveRef(doc, value.$ref);
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
function refName(ref) {
|
|
32
|
+
const idx = ref.lastIndexOf('/');
|
|
33
|
+
return idx >= 0 ? ref.slice(idx + 1) : ref;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Lift validation / annotation keywords off an OAS Schema Object into our
|
|
37
|
+
* neutral SchemaMetadata bag.
|
|
38
|
+
*
|
|
39
|
+
* Normalizes OAS 3.0 boolean `exclusiveMinimum` / `exclusiveMaximum` into the
|
|
40
|
+
* OAS 3.1 numeric form so downstream consumers (the emitter, in particular)
|
|
41
|
+
* have exactly one shape to handle.
|
|
42
|
+
*
|
|
43
|
+
* Returns `undefined` when nothing of interest is present. This keeps the IR
|
|
44
|
+
* uncluttered: schemas without constraints simply have no `metadata` field at
|
|
45
|
+
* all rather than an empty object, which also keeps strict-equality tests
|
|
46
|
+
* stable.
|
|
47
|
+
*/
|
|
48
|
+
function extractMetadata(schema) {
|
|
49
|
+
// We index into the schema with looser typing than the official @redocly types
|
|
50
|
+
// because OAS 3.1 keywords (numeric `exclusiveMinimum/Maximum`) aren't in the
|
|
51
|
+
// 3.0-focused type, and `format`/`deprecated` live in a few different places.
|
|
52
|
+
const s = schema;
|
|
53
|
+
const out = {};
|
|
54
|
+
if (typeof s.minimum === 'number')
|
|
55
|
+
out.minimum = s.minimum;
|
|
56
|
+
if (typeof s.maximum === 'number')
|
|
57
|
+
out.maximum = s.maximum;
|
|
58
|
+
// OAS 3.0: exclusiveMinimum/Maximum are booleans paired with minimum/maximum.
|
|
59
|
+
// OAS 3.1: they are numbers and stand alone.
|
|
60
|
+
// We always emit the 3.1 numeric form. When the 3.0 boolean form is used,
|
|
61
|
+
// hoist the paired bound up and drop the inclusive form.
|
|
62
|
+
if (typeof s.exclusiveMinimum === 'number') {
|
|
63
|
+
out.exclusiveMinimum = s.exclusiveMinimum;
|
|
64
|
+
}
|
|
65
|
+
else if (s.exclusiveMinimum === true && typeof s.minimum === 'number') {
|
|
66
|
+
out.exclusiveMinimum = s.minimum;
|
|
67
|
+
delete out.minimum;
|
|
68
|
+
}
|
|
69
|
+
if (typeof s.exclusiveMaximum === 'number') {
|
|
70
|
+
out.exclusiveMaximum = s.exclusiveMaximum;
|
|
71
|
+
}
|
|
72
|
+
else if (s.exclusiveMaximum === true && typeof s.maximum === 'number') {
|
|
73
|
+
out.exclusiveMaximum = s.maximum;
|
|
74
|
+
delete out.maximum;
|
|
75
|
+
}
|
|
76
|
+
if (typeof s.minLength === 'number')
|
|
77
|
+
out.minLength = s.minLength;
|
|
78
|
+
if (typeof s.maxLength === 'number')
|
|
79
|
+
out.maxLength = s.maxLength;
|
|
80
|
+
if (typeof s.pattern === 'string')
|
|
81
|
+
out.pattern = s.pattern;
|
|
82
|
+
if (typeof s.minItems === 'number')
|
|
83
|
+
out.minItems = s.minItems;
|
|
84
|
+
if (typeof s.maxItems === 'number')
|
|
85
|
+
out.maxItems = s.maxItems;
|
|
86
|
+
// `uniqueItems` defaults to false; only the affirmative case carries info.
|
|
87
|
+
if (s.uniqueItems === true)
|
|
88
|
+
out.uniqueItems = true;
|
|
89
|
+
if (typeof s.format === 'string')
|
|
90
|
+
out.format = s.format;
|
|
91
|
+
// Same idea as uniqueItems — explicit `false` is the default; don't emit it.
|
|
92
|
+
if (s.deprecated === true)
|
|
93
|
+
out.deprecated = true;
|
|
94
|
+
if (s.example !== undefined) {
|
|
95
|
+
out.example = s.example;
|
|
96
|
+
}
|
|
97
|
+
else if (Array.isArray(s.examples) && s.examples.length > 0) {
|
|
98
|
+
out.example = s.examples[0];
|
|
99
|
+
}
|
|
100
|
+
if (s.default !== undefined)
|
|
101
|
+
out.default = s.default;
|
|
102
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Build discriminator metadata for a `oneOf` / `anyOf` union, when the schema
|
|
106
|
+
* declares an explicit `discriminator`. With an explicit `mapping`, each entry
|
|
107
|
+
* pairs the discriminant value with its target schema name. Without a mapping,
|
|
108
|
+
* the OpenAPI spec says the discriminant value equals the referenced schema's
|
|
109
|
+
* name, so we derive entries from the `$ref` members.
|
|
110
|
+
*
|
|
111
|
+
* Returns `undefined` when there's no usable discriminator (no `discriminator`
|
|
112
|
+
* block, or one that yields no named targets) — the emitter then skips guards.
|
|
113
|
+
*/
|
|
114
|
+
function buildDiscriminator(schema, members) {
|
|
115
|
+
const disc = schema.discriminator;
|
|
116
|
+
if (!disc || typeof disc.propertyName !== 'string')
|
|
117
|
+
return undefined;
|
|
118
|
+
const mapping = [];
|
|
119
|
+
if (disc.mapping) {
|
|
120
|
+
for (const [value, target] of Object.entries(disc.mapping)) {
|
|
121
|
+
mapping.push({ value, schemaName: refName(target) });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
for (const member of members) {
|
|
126
|
+
if (isRef(member)) {
|
|
127
|
+
const name = refName(member.$ref);
|
|
128
|
+
mapping.push({ value: name, schemaName: name });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (mapping.length === 0)
|
|
133
|
+
return undefined;
|
|
134
|
+
return { propertyName: disc.propertyName, mapping };
|
|
135
|
+
}
|
|
136
|
+
/** Attach metadata to a freshly-built SchemaModel (no-op when undefined). */
|
|
137
|
+
function withMetadata(model, metadata) {
|
|
138
|
+
if (!metadata)
|
|
139
|
+
return model;
|
|
140
|
+
return { ...model, metadata };
|
|
141
|
+
}
|
|
142
|
+
/** Merge two metadata bags; right wins on conflicts. Returns undefined when both are empty. */
|
|
143
|
+
function mergeMetadata(a, b) {
|
|
144
|
+
if (!a)
|
|
145
|
+
return b;
|
|
146
|
+
if (!b)
|
|
147
|
+
return a;
|
|
148
|
+
return { ...a, ...b };
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* The first server's URL with `{variable}` templates substituted by their declared
|
|
152
|
+
* defaults (required by the spec). An undeclared variable keeps its placeholder —
|
|
153
|
+
* there is nothing valid to substitute, and the literal template is easier to spot.
|
|
154
|
+
*/
|
|
155
|
+
function resolveServerUrl(server) {
|
|
156
|
+
if (!server)
|
|
157
|
+
return '';
|
|
158
|
+
return server.url.replace(/\{([^{}]+)\}/g, (template, variableName) => server.variables?.[variableName]?.default ?? template);
|
|
159
|
+
}
|
|
160
|
+
export function buildApiModel(doc) {
|
|
161
|
+
const title = doc.info?.title ?? 'Api';
|
|
162
|
+
const version = doc.info?.version ?? '0.0.0';
|
|
163
|
+
const description = doc.info?.description;
|
|
164
|
+
const serverUrl = resolveServerUrl(doc.servers?.[0]);
|
|
165
|
+
const schemas = buildNamedSchemas(doc);
|
|
166
|
+
const securitySchemes = buildSecuritySchemes(doc);
|
|
167
|
+
const services = buildServices(doc, securitySchemes);
|
|
168
|
+
const model = {
|
|
169
|
+
title,
|
|
170
|
+
version,
|
|
171
|
+
description,
|
|
172
|
+
serverUrl,
|
|
173
|
+
services,
|
|
174
|
+
schemas,
|
|
175
|
+
securitySchemes,
|
|
176
|
+
};
|
|
177
|
+
// Sanitize names into safe identifiers (and rewrite refs to match) BEFORE any later
|
|
178
|
+
// pass derives names from them — `stripReadOnly` builds `omit` targets from schema
|
|
179
|
+
// names, so it must see the sanitized ones.
|
|
180
|
+
sanitizeIdentifiers(model);
|
|
181
|
+
stripReadOnlyFromRequestBodies(services, schemas);
|
|
182
|
+
// Hard gate: no unsafe name may reach the printer (see sanitize-identifiers.ts).
|
|
183
|
+
assertSafeIdentifiers(model);
|
|
184
|
+
return model;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Drop `readOnly` (server-managed) properties from every request body, in place.
|
|
188
|
+
* OpenAPI says readOnly properties must not be sent in requests, so a create/update
|
|
189
|
+
* body should not demand `id`/`createdAt`/etc. A body that `$ref`s a named schema
|
|
190
|
+
* becomes `Omit<Name, …readOnly>` (keeping the named type); an inline object has
|
|
191
|
+
* its readOnly properties filtered out. Response types are untouched.
|
|
192
|
+
*/
|
|
193
|
+
function stripReadOnlyFromRequestBodies(services, schemas) {
|
|
194
|
+
const byName = new Map(schemas.map((s) => [s.name, s.schema]));
|
|
195
|
+
for (const service of services) {
|
|
196
|
+
for (const op of service.operations) {
|
|
197
|
+
if (op.requestBody)
|
|
198
|
+
op.requestBody.schema = stripReadOnly(op.requestBody.schema, byName);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Remove `unknown` members from a union's member list. `T | unknown` is just
|
|
204
|
+
* `unknown` in TypeScript, so a typeless/empty `oneOf`/`anyOf` branch would erase
|
|
205
|
+
* the real members. When every member is `unknown`, the union is itself `unknown`.
|
|
206
|
+
*/
|
|
207
|
+
function dropRedundantUnknown(members) {
|
|
208
|
+
const real = members.filter((m) => m.kind !== 'unknown');
|
|
209
|
+
return real.length > 0 ? real : [{ kind: 'unknown' }];
|
|
210
|
+
}
|
|
211
|
+
function stripReadOnly(schema, byName) {
|
|
212
|
+
if (schema.kind === 'ref') {
|
|
213
|
+
const keys = collectReadOnlyKeys(schema, byName, new Set());
|
|
214
|
+
return keys.length > 0 ? { kind: 'omit', base: schema.name, keys } : schema;
|
|
215
|
+
}
|
|
216
|
+
if (schema.kind === 'object') {
|
|
217
|
+
const kept = schema.properties.filter((p) => !p.readOnly);
|
|
218
|
+
return kept.length === schema.properties.length ? schema : { ...schema, properties: kept };
|
|
219
|
+
}
|
|
220
|
+
return schema;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* The readOnly top-level property names of a schema, descending through `$ref`s
|
|
224
|
+
* and `allOf` (intersection) members — the shape entity schemas compose with. A
|
|
225
|
+
* `visited` set guards against recursive refs. Order-preserving and deduped.
|
|
226
|
+
*/
|
|
227
|
+
function collectReadOnlyKeys(schema, byName, visited) {
|
|
228
|
+
const keys = [];
|
|
229
|
+
const visit = (s) => {
|
|
230
|
+
if (s.kind === 'ref') {
|
|
231
|
+
if (visited.has(s.name))
|
|
232
|
+
return;
|
|
233
|
+
visited.add(s.name);
|
|
234
|
+
const target = byName.get(s.name);
|
|
235
|
+
if (target)
|
|
236
|
+
visit(target);
|
|
237
|
+
}
|
|
238
|
+
else if (s.kind === 'object') {
|
|
239
|
+
for (const p of s.properties)
|
|
240
|
+
if (p.readOnly && !keys.includes(p.name))
|
|
241
|
+
keys.push(p.name);
|
|
242
|
+
}
|
|
243
|
+
else if (s.kind === 'intersection') {
|
|
244
|
+
for (const member of s.members)
|
|
245
|
+
visit(member);
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
visit(schema);
|
|
249
|
+
return keys;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Collect the security schemes the client can apply on the wire, keyed by their
|
|
253
|
+
* `components.securitySchemes` name. Bearer (`http`+`bearer`/oauth2/openIdConnect),
|
|
254
|
+
* HTTP Basic, and apiKey in header/query/cookie are all injectable. `mutualTLS`
|
|
255
|
+
* (and an `http` scheme that is neither bearer nor basic) is skipped — operations
|
|
256
|
+
* that reference only those will simply carry no auth.
|
|
257
|
+
*/
|
|
258
|
+
function buildSecuritySchemes(doc) {
|
|
259
|
+
const schemes = doc.components?.securitySchemes;
|
|
260
|
+
if (!schemes)
|
|
261
|
+
return [];
|
|
262
|
+
const result = [];
|
|
263
|
+
for (const [key, raw] of Object.entries(schemes)) {
|
|
264
|
+
const scheme = deref(doc, raw);
|
|
265
|
+
const type = scheme.type;
|
|
266
|
+
if (type === 'oauth2' || type === 'openIdConnect') {
|
|
267
|
+
result.push({ kind: 'bearer', key });
|
|
268
|
+
}
|
|
269
|
+
else if (type === 'http' && (scheme.scheme ?? '').toLowerCase() === 'bearer') {
|
|
270
|
+
result.push({ kind: 'bearer', key });
|
|
271
|
+
}
|
|
272
|
+
else if (type === 'http' && (scheme.scheme ?? '').toLowerCase() === 'basic') {
|
|
273
|
+
result.push({ kind: 'basic', key });
|
|
274
|
+
}
|
|
275
|
+
else if (type === 'apiKey' && scheme.in === 'header' && typeof scheme.name === 'string') {
|
|
276
|
+
result.push({ kind: 'apiKeyHeader', key, headerName: scheme.name });
|
|
277
|
+
}
|
|
278
|
+
else if (type === 'apiKey' && scheme.in === 'query' && typeof scheme.name === 'string') {
|
|
279
|
+
result.push({ kind: 'apiKeyQuery', key, paramName: scheme.name });
|
|
280
|
+
}
|
|
281
|
+
else if (type === 'apiKey' && scheme.in === 'cookie' && typeof scheme.name === 'string') {
|
|
282
|
+
// Injected via the Cookie request header, which browsers refuse to set —
|
|
283
|
+
// the credential silently goes missing there, so tell the user up front.
|
|
284
|
+
logger.warn(`generate-client: security scheme "${key}" sends its credential in the Cookie header, ` +
|
|
285
|
+
`which browsers ignore — cookie auth works only in server-side clients.\n`);
|
|
286
|
+
result.push({ kind: 'apiKeyCookie', key, cookieName: scheme.name });
|
|
287
|
+
}
|
|
288
|
+
// Everything else (http schemes other than bearer/basic, mutualTLS) is not
|
|
289
|
+
// injectable by the generated client — intentionally skipped.
|
|
290
|
+
}
|
|
291
|
+
return result;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Resolve the effective security for one operation into the list of injectable
|
|
295
|
+
* OR-alternatives, each an AND-set of scheme keys. The operation's own `security`
|
|
296
|
+
* overrides the document default; `security: []` opts out entirely.
|
|
297
|
+
*
|
|
298
|
+
* Every fully-injectable alternative is kept — the runtime applies exactly ONE of
|
|
299
|
+
* them (the first whose credentials are all configured), so an operation that
|
|
300
|
+
* accepts "bearer OR apiKey" works with either credential and never sends both.
|
|
301
|
+
* `{}` (the optional-auth marker) and alternatives with non-injectable schemes
|
|
302
|
+
* are skipped; an empty result means no auth is applied.
|
|
303
|
+
*/
|
|
304
|
+
function resolveOperationSecurity(operation, doc, injectable) {
|
|
305
|
+
const requirements = operation.security ??
|
|
306
|
+
doc.security;
|
|
307
|
+
if (!requirements)
|
|
308
|
+
return [];
|
|
309
|
+
const alternatives = [];
|
|
310
|
+
for (const requirement of requirements) {
|
|
311
|
+
const keys = Object.keys(requirement);
|
|
312
|
+
if (keys.length === 0)
|
|
313
|
+
continue;
|
|
314
|
+
if (keys.every((key) => injectable.has(key))) {
|
|
315
|
+
alternatives.push([...new Set(keys)]);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return alternatives;
|
|
319
|
+
}
|
|
320
|
+
function buildNamedSchemas(doc) {
|
|
321
|
+
const namedSchemas = doc.components?.schemas;
|
|
322
|
+
if (!namedSchemas)
|
|
323
|
+
return [];
|
|
324
|
+
return Object.entries(namedSchemas).map(([name, schema]) => {
|
|
325
|
+
if (isRef(schema)) {
|
|
326
|
+
// A top-level entry that is a $ref forwards to another component.
|
|
327
|
+
const target = resolveRef(doc, schema.$ref);
|
|
328
|
+
return {
|
|
329
|
+
name,
|
|
330
|
+
schema: buildSchema(target, `components.schemas.${name}`, doc),
|
|
331
|
+
description: target.description,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
const built = buildSchema(schema, `components.schemas.${name}`, doc);
|
|
335
|
+
return { name, schema: built, description: schema.description };
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
function buildServices(doc, securitySchemes) {
|
|
339
|
+
const injectable = new Set(securitySchemes.map((s) => s.key));
|
|
340
|
+
const entries = [];
|
|
341
|
+
const usedNames = new Set();
|
|
342
|
+
for (const [path, pathItemRaw] of Object.entries(doc.paths ?? {})) {
|
|
343
|
+
if (!pathItemRaw)
|
|
344
|
+
continue;
|
|
345
|
+
const pathItem = deref(doc, pathItemRaw);
|
|
346
|
+
const pathLevelParams = (pathItem.parameters ?? []).map((p) => buildParameter(deref(doc, p), `paths.${path}.parameters`, doc));
|
|
347
|
+
for (const method of HTTP_METHODS) {
|
|
348
|
+
const operation = pathItem[method];
|
|
349
|
+
if (!operation)
|
|
350
|
+
continue;
|
|
351
|
+
entries.push({ method, path, operation, pathLevelParams });
|
|
352
|
+
// Reserve every declared operationId up front so a synthesized fallback name
|
|
353
|
+
// never collides with one — declared ids always win.
|
|
354
|
+
if (operation.operationId)
|
|
355
|
+
usedNames.add(operation.operationId);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
const operations = entries.map((entry) => {
|
|
359
|
+
const name = entry.operation.operationId ??
|
|
360
|
+
takeUniqueName(fallbackOperationName(entry.method, entry.path), usedNames);
|
|
361
|
+
return buildOperation(entry.method, entry.path, entry.operation, name, entry.pathLevelParams, doc, injectable);
|
|
362
|
+
});
|
|
363
|
+
return [{ name: 'Default', operations }];
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Synthesize an operation name from method + path when the spec omits `operationId`:
|
|
367
|
+
* `<method><PascalCasedPathSegments>` (braces stripped; each segment split on
|
|
368
|
+
* non-identifier chars). Path-param segments are kept so a collection (`GET /pets`)
|
|
369
|
+
* and an item (`GET /pets/{id}`) stay distinct. Always a valid identifier — the
|
|
370
|
+
* lowercase `method` prefix guarantees a letter start.
|
|
371
|
+
*/
|
|
372
|
+
function fallbackOperationName(method, path) {
|
|
373
|
+
const segments = path
|
|
374
|
+
.split('/')
|
|
375
|
+
.filter(Boolean)
|
|
376
|
+
.map((segment) => pascalSegment(segment.replace(/[{}]/g, '')));
|
|
377
|
+
return `${method}${segments.join('')}`;
|
|
378
|
+
}
|
|
379
|
+
function pascalSegment(segment) {
|
|
380
|
+
return segment
|
|
381
|
+
.split(/[^A-Za-z0-9]+/)
|
|
382
|
+
.filter(Boolean)
|
|
383
|
+
.map((word) => word[0].toUpperCase() + word.slice(1))
|
|
384
|
+
.join('');
|
|
385
|
+
}
|
|
386
|
+
/** Return `base`, or `base2`/`base3`/… if already taken; records the result in `used`. */
|
|
387
|
+
function takeUniqueName(base, used) {
|
|
388
|
+
let name = base;
|
|
389
|
+
let suffix = 2;
|
|
390
|
+
while (used.has(name))
|
|
391
|
+
name = `${base}${suffix++}`;
|
|
392
|
+
used.add(name);
|
|
393
|
+
return name;
|
|
394
|
+
}
|
|
395
|
+
function buildOperation(method, path, operation, name, pathLevelParams, doc, injectable) {
|
|
396
|
+
const operationParams = (operation.parameters ?? []).map((p) => buildParameter(deref(doc, p), `paths.${path}.${method}.parameters`, doc));
|
|
397
|
+
// Operation-level parameters override path-level ones (by name + in).
|
|
398
|
+
const seen = new Set();
|
|
399
|
+
const allParams = [];
|
|
400
|
+
for (const p of operationParams) {
|
|
401
|
+
seen.add(`${p.in}:${p.name}`);
|
|
402
|
+
allParams.push(p);
|
|
403
|
+
}
|
|
404
|
+
for (const p of pathLevelParams) {
|
|
405
|
+
if (!seen.has(`${p.in}:${p.name}`)) {
|
|
406
|
+
allParams.push(p);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
const pathParams = allParams.filter((p) => p.in === 'path');
|
|
410
|
+
const queryParams = allParams.filter((p) => p.in === 'query');
|
|
411
|
+
const headerParams = allParams.filter((p) => p.in === 'header');
|
|
412
|
+
// Browsers own the Cookie header, so the generated client cannot set cookie
|
|
413
|
+
// params — drop them, but tell the user instead of vanishing them silently.
|
|
414
|
+
const cookieParams = allParams.filter((p) => p.in === 'cookie');
|
|
415
|
+
if (cookieParams.length > 0) {
|
|
416
|
+
logger.warn(`generate-client: skipped cookie parameter(s) ${cookieParams
|
|
417
|
+
.map((p) => `"${p.name}"`)
|
|
418
|
+
.join(', ')} on operation "${name}" — cookie parameters are not supported.\n`);
|
|
419
|
+
}
|
|
420
|
+
const requestBody = operation.requestBody
|
|
421
|
+
? buildRequestBody(deref(doc, operation.requestBody), `${method} ${path}`, doc)
|
|
422
|
+
: undefined;
|
|
423
|
+
const successResponses = buildSuccessResponses(operation, path, doc);
|
|
424
|
+
const errorResponses = buildErrorResponses(operation, path, doc);
|
|
425
|
+
const security = resolveOperationSecurity(operation, doc, injectable);
|
|
426
|
+
// Extensions aren't in the @redocly operation type — read loosely, like `deprecated`.
|
|
427
|
+
const paginationExtension = operation['x-pagination'];
|
|
428
|
+
return {
|
|
429
|
+
name,
|
|
430
|
+
method,
|
|
431
|
+
path,
|
|
432
|
+
summary: operation.summary,
|
|
433
|
+
description: operation.description,
|
|
434
|
+
pathParams,
|
|
435
|
+
queryParams,
|
|
436
|
+
headerParams,
|
|
437
|
+
requestBody,
|
|
438
|
+
successResponses,
|
|
439
|
+
errorResponses,
|
|
440
|
+
security,
|
|
441
|
+
tags: Array.isArray(operation.tags) ? operation.tags.filter((t) => typeof t === 'string') : [],
|
|
442
|
+
...(paginationExtension !== undefined ? { paginationExtension } : {}),
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
function buildParameter(param, location, doc) {
|
|
446
|
+
if (!param.in) {
|
|
447
|
+
throw new NotSupportedError(`Parameter ${param.name} at ${location} is missing "in"`);
|
|
448
|
+
}
|
|
449
|
+
if (!['path', 'query', 'header', 'cookie'].includes(param.in)) {
|
|
450
|
+
throw new NotSupportedError(`Unsupported parameter location "${param.in}" for ${param.name} at ${location}`);
|
|
451
|
+
}
|
|
452
|
+
let schema = schemaFromSlot(param.schema, `${location}.${param.name}`, doc);
|
|
453
|
+
// OpenAPI lets `deprecated: true` live on the Parameter Object itself, not
|
|
454
|
+
// just on its schema. Fold it into the schema's metadata so the emitter has
|
|
455
|
+
// a single source of truth when rendering tags around this param.
|
|
456
|
+
if (param.deprecated === true) {
|
|
457
|
+
schema = {
|
|
458
|
+
...schema,
|
|
459
|
+
metadata: mergeMetadata(schema.metadata, { deprecated: true }),
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
const model = {
|
|
463
|
+
name: param.name,
|
|
464
|
+
in: param.in,
|
|
465
|
+
schema,
|
|
466
|
+
required: Boolean(param.required),
|
|
467
|
+
description: param.description,
|
|
468
|
+
};
|
|
469
|
+
// Query-serialization hints (OpenAPI `style`/`explode`/`allowReserved`) live on the
|
|
470
|
+
// Parameter Object and aren't in the @redocly types — read them loosely, like
|
|
471
|
+
// `deprecated`. Only set them for query params, and only when present (absence ⇒
|
|
472
|
+
// the defaults, so the IR stays clean and downstream takes the default path).
|
|
473
|
+
if (model.in === 'query') {
|
|
474
|
+
const p = param;
|
|
475
|
+
if (p.style === 'form' ||
|
|
476
|
+
p.style === 'spaceDelimited' ||
|
|
477
|
+
p.style === 'pipeDelimited' ||
|
|
478
|
+
p.style === 'deepObject') {
|
|
479
|
+
model.style = p.style;
|
|
480
|
+
}
|
|
481
|
+
if (typeof p.explode === 'boolean')
|
|
482
|
+
model.explode = p.explode;
|
|
483
|
+
if (typeof p.allowReserved === 'boolean')
|
|
484
|
+
model.allowReserved = p.allowReserved;
|
|
485
|
+
}
|
|
486
|
+
return model;
|
|
487
|
+
}
|
|
488
|
+
function buildRequestBody(rb, location, doc) {
|
|
489
|
+
const content = rb.content ?? {};
|
|
490
|
+
// Prefer JSON; fall back to first available.
|
|
491
|
+
const preferred = content['application/json'] ??
|
|
492
|
+
content['application/merge-patch+json'] ??
|
|
493
|
+
content['application/x-www-form-urlencoded'] ??
|
|
494
|
+
content['multipart/form-data'] ??
|
|
495
|
+
Object.values(content)[0];
|
|
496
|
+
if (!preferred)
|
|
497
|
+
return undefined;
|
|
498
|
+
// `preferred` is one of the values we sampled out of `content`, so its key must exist there.
|
|
499
|
+
const contentType = Object.keys(content).find((k) => content[k] === preferred);
|
|
500
|
+
const schema = schemaFromSlot(preferred.schema, `${location}.requestBody`, doc);
|
|
501
|
+
return {
|
|
502
|
+
contentType,
|
|
503
|
+
schema,
|
|
504
|
+
required: Boolean(rb.required),
|
|
505
|
+
description: rb.description,
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
function buildSuccessResponses(operation, path, doc) {
|
|
509
|
+
const responses = operation.responses ?? {};
|
|
510
|
+
const successCodes = Object.keys(responses).filter((code) => /^2\d\d$/.test(code));
|
|
511
|
+
if (successCodes.length === 0) {
|
|
512
|
+
if (responses['default'])
|
|
513
|
+
successCodes.push('default');
|
|
514
|
+
}
|
|
515
|
+
// Pick the first success response.
|
|
516
|
+
const code = successCodes[0];
|
|
517
|
+
if (!code)
|
|
518
|
+
return [];
|
|
519
|
+
const responseRaw = responses[code];
|
|
520
|
+
if (!responseRaw)
|
|
521
|
+
return [];
|
|
522
|
+
const response = deref(doc, responseRaw);
|
|
523
|
+
const content = response.content;
|
|
524
|
+
if (!content)
|
|
525
|
+
return [];
|
|
526
|
+
const status = code === 'default' ? 'default' : Number(code);
|
|
527
|
+
const result = [];
|
|
528
|
+
for (const [contentType, media] of Object.entries(content)) {
|
|
529
|
+
const schema = schemaFromSlot(media.schema, `paths.${path}.response.${code}.${contentType}`, doc);
|
|
530
|
+
const itemSlot = media.itemSchema;
|
|
531
|
+
const item = itemSlot !== undefined
|
|
532
|
+
? schemaFromSlot(itemSlot, `paths.${path}.response.${code}.${contentType}.itemSchema`, doc)
|
|
533
|
+
: undefined;
|
|
534
|
+
result.push(item === undefined
|
|
535
|
+
? { contentType, schema, status }
|
|
536
|
+
: { contentType, schema, status, itemSchema: item });
|
|
537
|
+
}
|
|
538
|
+
return result;
|
|
539
|
+
}
|
|
540
|
+
function buildErrorResponses(operation, path, doc) {
|
|
541
|
+
const responses = operation.responses ?? {};
|
|
542
|
+
const codes = Object.keys(responses).filter((code) => /^[45]\d\d$/.test(code));
|
|
543
|
+
// `default` is an error only when a 2xx success exists; otherwise
|
|
544
|
+
// `buildSuccessResponses` already consumes it as the success response.
|
|
545
|
+
const hasSuccess = Object.keys(responses).some((code) => /^2\d\d$/.test(code));
|
|
546
|
+
if (hasSuccess && responses['default'])
|
|
547
|
+
codes.push('default');
|
|
548
|
+
const result = [];
|
|
549
|
+
for (const code of codes) {
|
|
550
|
+
const responseRaw = responses[code];
|
|
551
|
+
if (!responseRaw)
|
|
552
|
+
continue;
|
|
553
|
+
const response = deref(doc, responseRaw);
|
|
554
|
+
const content = response.content;
|
|
555
|
+
if (!content)
|
|
556
|
+
continue;
|
|
557
|
+
const status = code === 'default' ? 'default' : Number(code);
|
|
558
|
+
for (const [contentType, media] of Object.entries(content)) {
|
|
559
|
+
const schema = schemaFromSlot(media.schema, `paths.${path}.response.${code}.${contentType}`, doc);
|
|
560
|
+
result.push({ contentType, schema, status });
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return result;
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Decode a "schema slot" — the referenced-or-inline-or-absent schema position
|
|
567
|
+
* that recurs all over an OpenAPI document (`parameter.schema`, `media.schema`,
|
|
568
|
+
* property values, array `items`, `additionalProperties`) — into a SchemaModel.
|
|
569
|
+
*
|
|
570
|
+
* - absent (`undefined`) or a boolean JSON-Schema → `unknown` (we don't model
|
|
571
|
+
* boolean schemas; this collapses the "missing schema" fallbacks too).
|
|
572
|
+
* - a `$ref` → a `ref` node carrying the target's local name (the IR keeps
|
|
573
|
+
* refs un-resolved so named schemas map back to exported types).
|
|
574
|
+
* - an inline schema → recurse via `buildSchema`.
|
|
575
|
+
*/
|
|
576
|
+
function schemaFromSlot(slot, location, doc) {
|
|
577
|
+
if (slot === undefined || typeof slot === 'boolean')
|
|
578
|
+
return { kind: 'unknown' };
|
|
579
|
+
if (isRef(slot))
|
|
580
|
+
return { kind: 'ref', name: refName(slot.$ref) };
|
|
581
|
+
return buildSchema(slot, location, doc);
|
|
582
|
+
}
|
|
583
|
+
function buildSchema(schema, location, doc) {
|
|
584
|
+
// Note: every caller checks `isRef(schema)` before invoking this function, so a top-level
|
|
585
|
+
// `{$ref: ...}` never reaches here — see `buildNamedSchemas`, `buildParameter`, etc.
|
|
586
|
+
const metadata = extractMetadata(schema);
|
|
587
|
+
const oneOfish = schema.oneOf ?? schema.anyOf;
|
|
588
|
+
if (oneOfish) {
|
|
589
|
+
const allMembers = oneOfish.map((sub, idx) => isRef(sub)
|
|
590
|
+
? { kind: 'ref', name: refName(sub.$ref) }
|
|
591
|
+
: buildSchema(sub, `${location}.[${idx}]`, doc));
|
|
592
|
+
// Drop `unknown` members (typeless/empty branches): `T | unknown` collapses to
|
|
593
|
+
// `unknown` in TS, erasing the real members. When only one real member remains,
|
|
594
|
+
// the union degenerates to it.
|
|
595
|
+
const members = dropRedundantUnknown(allMembers);
|
|
596
|
+
// Collapse to the lone member only when dropping `unknown` branches reduced the
|
|
597
|
+
// union to one — an originally single-member union is left as-is.
|
|
598
|
+
if (members.length === 1 && members.length < allMembers.length) {
|
|
599
|
+
const inner = members[0];
|
|
600
|
+
return {
|
|
601
|
+
...inner,
|
|
602
|
+
description: inner.description ?? schema.description,
|
|
603
|
+
metadata: mergeMetadata(metadata, inner.metadata),
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
const discriminator = buildDiscriminator(schema, oneOfish);
|
|
607
|
+
return withMetadata({ kind: 'union', members, discriminator, description: schema.description }, metadata);
|
|
608
|
+
}
|
|
609
|
+
if (schema.allOf) {
|
|
610
|
+
const members = schema.allOf.map((sub, idx) => isRef(sub)
|
|
611
|
+
? { kind: 'ref', name: refName(sub.$ref) }
|
|
612
|
+
: buildSchema(sub, `${location}.allOf[${idx}]`, doc));
|
|
613
|
+
// `allOf` does not replace sibling `properties`: a schema may declare its own
|
|
614
|
+
// object shape (often a `const` discriminant) alongside `allOf`. Fold that own
|
|
615
|
+
// object into the intersection so those properties aren't dropped.
|
|
616
|
+
const ownProperties = buildProperties(schema, location, doc);
|
|
617
|
+
if (ownProperties.length > 0) {
|
|
618
|
+
members.unshift({ kind: 'object', properties: ownProperties });
|
|
619
|
+
}
|
|
620
|
+
if (members.length === 1) {
|
|
621
|
+
// Single-member allOf collapses to its target. Preserve the wrapper's
|
|
622
|
+
// description/metadata only when the inner doesn't already have them.
|
|
623
|
+
const inner = members[0];
|
|
624
|
+
return {
|
|
625
|
+
...inner,
|
|
626
|
+
description: inner.description ?? schema.description,
|
|
627
|
+
metadata: mergeMetadata(metadata, inner.metadata),
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
return withMetadata({ kind: 'intersection', members, description: schema.description }, metadata);
|
|
631
|
+
}
|
|
632
|
+
// OpenAPI 3.1 style nullable: `type: ['string', 'null']`.
|
|
633
|
+
const rawType = schema.type;
|
|
634
|
+
if (Array.isArray(rawType)) {
|
|
635
|
+
const nonNull = rawType.filter((t) => t !== 'null');
|
|
636
|
+
const hasNull = rawType.includes('null');
|
|
637
|
+
const baseMembers = nonNull.map((t) => {
|
|
638
|
+
// Strip `null` out of any `enum` before recursing: the array-type null is
|
|
639
|
+
// the single source of nullability here, so the enum branch must not add a
|
|
640
|
+
// second `null` member (and an enum of only `null` would otherwise throw).
|
|
641
|
+
const sub = { ...schema, type: t };
|
|
642
|
+
if (Array.isArray(sub.enum)) {
|
|
643
|
+
const filtered = sub.enum.filter((v) => v !== null);
|
|
644
|
+
sub.enum = filtered.length > 0 ? filtered : undefined;
|
|
645
|
+
}
|
|
646
|
+
return buildSchema(sub, location, doc);
|
|
647
|
+
});
|
|
648
|
+
if (hasNull)
|
|
649
|
+
baseMembers.push({ kind: 'null' });
|
|
650
|
+
if (baseMembers.length === 1) {
|
|
651
|
+
return baseMembers[0];
|
|
652
|
+
}
|
|
653
|
+
return withMetadata({ kind: 'union', members: baseMembers, description: schema.description }, metadata);
|
|
654
|
+
}
|
|
655
|
+
// OpenAPI 3.0 nullable.
|
|
656
|
+
if (schema.nullable) {
|
|
657
|
+
const base = buildSchema({ ...schema, nullable: undefined }, location, doc);
|
|
658
|
+
return {
|
|
659
|
+
kind: 'union',
|
|
660
|
+
members: [base, { kind: 'null' }],
|
|
661
|
+
description: schema.description,
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
if (schema.const !== undefined) {
|
|
665
|
+
const value = schema.const;
|
|
666
|
+
if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') {
|
|
667
|
+
throw new NotSupportedError(`Unsupported const value at ${location}: ${JSON.stringify(value)}`);
|
|
668
|
+
}
|
|
669
|
+
return withMetadata({ kind: 'literal', value, description: schema.description }, metadata);
|
|
670
|
+
}
|
|
671
|
+
if (schema.enum) {
|
|
672
|
+
// OAS 3.1 allows `null` among enum values. Model it as `<enum> | null` and
|
|
673
|
+
// keep the enum itself null-free so scalar inference stays simple.
|
|
674
|
+
const rawValues = schema.enum;
|
|
675
|
+
const nonNullValues = rawValues.filter((v) => v !== null);
|
|
676
|
+
const enumHasNull = nonNullValues.length !== rawValues.length;
|
|
677
|
+
if (nonNullValues.length === 0) {
|
|
678
|
+
return withMetadata({ kind: 'null', description: schema.description }, metadata);
|
|
679
|
+
}
|
|
680
|
+
const scalar = scalarForEnumValues(nonNullValues, location);
|
|
681
|
+
// A boolean `enum` conveys no useful narrowing: `[true, false]` is just `boolean`, and a
|
|
682
|
+
// single-value `[false]`/`[true]` is a spec quirk, not an intended literal type. Widen to
|
|
683
|
+
// `boolean` so a normal boolean field never becomes a literal. (`const: false` stays a
|
|
684
|
+
// literal — that's an explicit, deliberate single-value constraint, handled above.)
|
|
685
|
+
const base = scalar === 'boolean'
|
|
686
|
+
? { kind: 'scalar', scalar: 'boolean', description: schema.description }
|
|
687
|
+
: { kind: 'enum', values: nonNullValues, scalar, description: schema.description };
|
|
688
|
+
if (enumHasNull) {
|
|
689
|
+
return withMetadata({ kind: 'union', members: [base, { kind: 'null' }], description: schema.description }, metadata);
|
|
690
|
+
}
|
|
691
|
+
return withMetadata(base, metadata);
|
|
692
|
+
}
|
|
693
|
+
const type = rawType;
|
|
694
|
+
if (type === 'null') {
|
|
695
|
+
// OAS 3.1 single null type — a value that is always `null` (e.g. a field a
|
|
696
|
+
// variant pins to null). Without this it would fall through to `unknown`.
|
|
697
|
+
return withMetadata({ kind: 'null', description: schema.description }, metadata);
|
|
698
|
+
}
|
|
699
|
+
if (type === 'array') {
|
|
700
|
+
const itemsRaw = schema.items;
|
|
701
|
+
const items = schemaFromSlot(itemsRaw, `${location}.items`, doc);
|
|
702
|
+
return withMetadata({ kind: 'array', items, description: schema.description }, metadata);
|
|
703
|
+
}
|
|
704
|
+
if (type === 'object' || schema.properties || schema.additionalProperties) {
|
|
705
|
+
const properties = buildProperties(schema, location, doc);
|
|
706
|
+
const additional = schema.additionalProperties;
|
|
707
|
+
if (properties.length === 0 && additional !== false) {
|
|
708
|
+
// A property-less object accepts arbitrary keys: OpenAPI defaults absent
|
|
709
|
+
// `additionalProperties` to allowed. Emit a record — of the declared
|
|
710
|
+
// additionalProperties schema, or `unknown` when it's absent/`true` — rather
|
|
711
|
+
// than `{}`, which in TS forbids member access. An explicit
|
|
712
|
+
// `additionalProperties: false` is closed and stays an empty object.
|
|
713
|
+
const value = schemaFromSlot(additional, `${location}.additionalProperties`, doc);
|
|
714
|
+
return withMetadata({ kind: 'record', value, description: schema.description }, metadata);
|
|
715
|
+
}
|
|
716
|
+
return withMetadata({ kind: 'object', properties, description: schema.description }, metadata);
|
|
717
|
+
}
|
|
718
|
+
if (type === 'string' || type === 'number' || type === 'integer' || type === 'boolean') {
|
|
719
|
+
return withMetadata({ kind: 'scalar', scalar: type, description: schema.description }, metadata);
|
|
720
|
+
}
|
|
721
|
+
// No usable type information — fall back to `unknown` instead of erroring,
|
|
722
|
+
// so generation still succeeds for sparsely-typed schemas.
|
|
723
|
+
return withMetadata({ kind: 'unknown', description: schema.description }, metadata);
|
|
724
|
+
}
|
|
725
|
+
function scalarForEnumValues(values, location) {
|
|
726
|
+
let hasStr = false;
|
|
727
|
+
let hasNum = false;
|
|
728
|
+
let hasBool = false;
|
|
729
|
+
for (const v of values) {
|
|
730
|
+
if (typeof v === 'string')
|
|
731
|
+
hasStr = true;
|
|
732
|
+
else if (typeof v === 'number')
|
|
733
|
+
hasNum = true;
|
|
734
|
+
else if (typeof v === 'boolean')
|
|
735
|
+
hasBool = true;
|
|
736
|
+
else
|
|
737
|
+
throw new NotSupportedError(`Unsupported enum value type at ${location}: ${JSON.stringify(v)}`);
|
|
738
|
+
}
|
|
739
|
+
if (hasStr && !hasNum && !hasBool)
|
|
740
|
+
return 'string';
|
|
741
|
+
if (hasNum && !hasStr && !hasBool)
|
|
742
|
+
return 'number';
|
|
743
|
+
if (hasBool && !hasStr && !hasNum)
|
|
744
|
+
return 'boolean';
|
|
745
|
+
return 'string';
|
|
746
|
+
}
|
|
747
|
+
function buildProperties(schema, location, doc) {
|
|
748
|
+
const props = schema.properties ?? {};
|
|
749
|
+
const required = new Set(schema.required ?? []);
|
|
750
|
+
return Object.entries(props).map(([name, sub]) => {
|
|
751
|
+
const readOnly = !isRef(sub) && sub.readOnly === true;
|
|
752
|
+
return {
|
|
753
|
+
name,
|
|
754
|
+
schema: schemaFromSlot(sub, `${location}.${name}`, doc),
|
|
755
|
+
required: required.has(name),
|
|
756
|
+
description: sub.description,
|
|
757
|
+
...(readOnly ? { readOnly: true } : {}),
|
|
758
|
+
};
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
//# sourceMappingURL=build.js.map
|