@webpieces/openapi-generator 0.0.1
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 +118 -0
- package/package.json +32 -0
- package/src/OpenApiGenerationError.d.ts +41 -0
- package/src/OpenApiGenerationError.js +45 -0
- package/src/OpenApiGenerationError.js.map +1 -0
- package/src/cli/OpenApiCli.d.ts +33 -0
- package/src/cli/OpenApiCli.js +100 -0
- package/src/cli/OpenApiCli.js.map +1 -0
- package/src/cli/WpOpenApiMain.d.ts +25 -0
- package/src/cli/WpOpenApiMain.js +62 -0
- package/src/cli/WpOpenApiMain.js.map +1 -0
- package/src/cli/wp-openapi.d.ts +2 -0
- package/src/cli/wp-openapi.js +18 -0
- package/src/cli/wp-openapi.js.map +1 -0
- package/src/emit/ArtifactWriter.d.ts +44 -0
- package/src/emit/ArtifactWriter.js +75 -0
- package/src/emit/ArtifactWriter.js.map +1 -0
- package/src/generate/DocumentSelection.d.ts +96 -0
- package/src/generate/DocumentSelection.js +153 -0
- package/src/generate/DocumentSelection.js.map +1 -0
- package/src/generate/GenerationInputs.d.ts +67 -0
- package/src/generate/GenerationInputs.js +88 -0
- package/src/generate/GenerationInputs.js.map +1 -0
- package/src/generate/OpenApiGenerator.d.ts +113 -0
- package/src/generate/OpenApiGenerator.js +306 -0
- package/src/generate/OpenApiGenerator.js.map +1 -0
- package/src/generate/OperationRenderer.d.ts +129 -0
- package/src/generate/OperationRenderer.js +256 -0
- package/src/generate/OperationRenderer.js.map +1 -0
- package/src/generate/SchemaRenderer.d.ts +89 -0
- package/src/generate/SchemaRenderer.js +236 -0
- package/src/generate/SchemaRenderer.js.map +1 -0
- package/src/generate/SecurityDeriver.d.ts +39 -0
- package/src/generate/SecurityDeriver.js +81 -0
- package/src/generate/SecurityDeriver.js.map +1 -0
- package/src/index.d.ts +32 -0
- package/src/index.js +70 -0
- package/src/index.js.map +1 -0
- package/src/json/JsonObject.d.ts +35 -0
- package/src/json/JsonObject.js +32 -0
- package/src/json/JsonObject.js.map +1 -0
- package/src/json/JsonWriter.d.ts +18 -0
- package/src/json/JsonWriter.js +48 -0
- package/src/json/JsonWriter.js.map +1 -0
- package/src/json/YamlReader.d.ts +35 -0
- package/src/json/YamlReader.js +111 -0
- package/src/json/YamlReader.js.map +1 -0
- package/src/json/YamlWriter.d.ts +35 -0
- package/src/json/YamlWriter.js +88 -0
- package/src/json/YamlWriter.js.map +1 -0
- package/src/load/ExportedConstantFolder.d.ts +21 -0
- package/src/load/ExportedConstantFolder.js +57 -0
- package/src/load/ExportedConstantFolder.js.map +1 -0
- package/src/load/ForeignFailure.d.ts +24 -0
- package/src/load/ForeignFailure.js +41 -0
- package/src/load/ForeignFailure.js.map +1 -0
- package/src/load/InputsLoader.d.ts +43 -0
- package/src/load/InputsLoader.js +148 -0
- package/src/load/InputsLoader.js.map +1 -0
- package/src/manifest/JsonReader.d.ts +37 -0
- package/src/manifest/JsonReader.js +109 -0
- package/src/manifest/JsonReader.js.map +1 -0
- package/src/manifest/ManifestLoader.d.ts +20 -0
- package/src/manifest/ManifestLoader.js +69 -0
- package/src/manifest/ManifestLoader.js.map +1 -0
- package/src/manifest/OpenApiManifest.d.ts +116 -0
- package/src/manifest/OpenApiManifest.js +142 -0
- package/src/manifest/OpenApiManifest.js.map +1 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OpenApiGenerator = void 0;
|
|
4
|
+
const api_doc_model_1 = require("@webpieces/api-doc-model");
|
|
5
|
+
const JsonObject_1 = require("../json/JsonObject");
|
|
6
|
+
const OpenApiGenerationError_1 = require("../OpenApiGenerationError");
|
|
7
|
+
const GenerationInputs_1 = require("./GenerationInputs");
|
|
8
|
+
const DocumentSelection_1 = require("./DocumentSelection");
|
|
9
|
+
const OperationRenderer_1 = require("./OperationRenderer");
|
|
10
|
+
const SchemaRenderer_1 = require("./SchemaRenderer");
|
|
11
|
+
const SecurityDeriver_1 = require("./SecurityDeriver");
|
|
12
|
+
/** The OpenAPI version this generator writes. See the class doc for why 3.1 and not 3.0. */
|
|
13
|
+
const OPENAPI_VERSION = '3.1.0';
|
|
14
|
+
/**
|
|
15
|
+
* {@link GenerationInputs} -> one OpenAPI 3.1.0 document per `@ApiType` some contract declares.
|
|
16
|
+
*
|
|
17
|
+
* ## 3.1, because it is what the model can state HONESTLY
|
|
18
|
+
*
|
|
19
|
+
* `type: [T, "null"]` instead of a `nullable` keyword; a `description` legal beside a `$ref`; a
|
|
20
|
+
* top-level `webhooks:` block. Each of those is a fact the model holds that 3.0 would have forced
|
|
21
|
+
* this renderer to drop or to lie about. 3.1's schema dialect is also JSON Schema 2020-12, which is
|
|
22
|
+
* what MCP `tools/list` speaks.
|
|
23
|
+
*
|
|
24
|
+
* ## ONE render function, called once per document
|
|
25
|
+
*
|
|
26
|
+
* ```
|
|
27
|
+
* ApiDocModel --> render(selection) --+--> full-private-openapi.json SVC_TO_SVC contracts
|
|
28
|
+
* +--> public-openapi.json EXTERNAL_CUSTOMER contracts, minus hidden methods
|
|
29
|
+
* +--> mcp-openapi.json MCP contracts
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* `components.schemas` is built by walking OUTWARD from the operations the selection accepted, so an
|
|
33
|
+
* unselected operation's DTOs are never constructed. See {@link DocumentSelection} for why that
|
|
34
|
+
* asymmetry — a bug emits nothing, rather than shipping an unreleased feature's schemas with only
|
|
35
|
+
* its URL removed — decides the whole design.
|
|
36
|
+
*
|
|
37
|
+
* ## It REFUSES to write an unmapped field
|
|
38
|
+
*
|
|
39
|
+
* An unmapped type renders as an empty schema, which in JSON Schema means "anything". Publishing one
|
|
40
|
+
* is a green build handing a partner a field with no shape, so the guard names the JSON pointer of
|
|
41
|
+
* every one and exits non-zero. There is deliberately NO flag to switch it off: the cure is at the
|
|
42
|
+
* contract, by naming the type.
|
|
43
|
+
*/
|
|
44
|
+
class OpenApiGenerator {
|
|
45
|
+
security = new SecurityDeriver_1.SecurityDeriver();
|
|
46
|
+
/**
|
|
47
|
+
* ONE document per `@ApiType` some contract declares, and no others.
|
|
48
|
+
*
|
|
49
|
+
* A document nobody asked for is not written empty — it is not written. That is the ONE
|
|
50
|
+
* conditional in the whole pipeline, and it lives here rather than in three places: no contract
|
|
51
|
+
* declaring `MCP` means no `mcp-openapi.json`, with no second "is it empty?" rule to keep in step
|
|
52
|
+
* with the first.
|
|
53
|
+
*/
|
|
54
|
+
generate(inputs) {
|
|
55
|
+
const documents = [];
|
|
56
|
+
for (const selection of DocumentSelection_1.DocumentSelection.all()) {
|
|
57
|
+
const contracts = inputs.contracts.filter((each) => selection.acceptsContract(each.model));
|
|
58
|
+
if (contracts.length === 0) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
documents.push(new GenerationInputs_1.GeneratedDocument(selection.fileName, this.render(inputs, contracts, selection)));
|
|
62
|
+
}
|
|
63
|
+
return new GenerationInputs_1.GeneratedDocuments(documents);
|
|
64
|
+
}
|
|
65
|
+
/** ONE document, from the operations this selection accepts and nothing else. */
|
|
66
|
+
render(inputs, contracts, selection) {
|
|
67
|
+
const types = this.mergedTypes(inputs);
|
|
68
|
+
const schemas = new SchemaRenderer_1.SchemaRenderer(types);
|
|
69
|
+
const operations = new OperationRenderer_1.OperationRenderer(schemas, selection.includeMcpExtensions, selection.internalNotice);
|
|
70
|
+
const apiKey = this.singleApiKey(inputs, contracts, selection);
|
|
71
|
+
const schemeNames = inputs.manifest.securitySchemeNames;
|
|
72
|
+
const hoisted = apiKey !== undefined && this.everyOperationIsCovered(contracts, selection)
|
|
73
|
+
? this.security.requirement(schemeNames)
|
|
74
|
+
: undefined;
|
|
75
|
+
const contract = new OperationRenderer_1.ResponseContract(this.errorResponses(inputs), this.errorSchemaRef(inputs, schemas), this.headerRefs(inputs));
|
|
76
|
+
const paths = new JsonObject_1.JsonObject();
|
|
77
|
+
const webhooks = new JsonObject_1.JsonObject();
|
|
78
|
+
const tags = new JsonObject_1.JsonObject();
|
|
79
|
+
for (const contractModel of contracts) {
|
|
80
|
+
this.renderContract(contractModel, selection, operations, contract, this.webhookContract(), hoisted === undefined ? this.security.requirement(schemeNames) : undefined, paths, webhooks, tags);
|
|
81
|
+
}
|
|
82
|
+
// The schemas are built HERE, after the operations, because building them is what walks a
|
|
83
|
+
// DTO's fields — and an unmapped field can only be found by that walk. A guard that ran
|
|
84
|
+
// before it would see only the top-level request and response refs and miss every one.
|
|
85
|
+
const renderedSchemas = schemas.components().orUndefined();
|
|
86
|
+
this.refuseUnmappedFields(schemas.unmapped(), inputs, selection);
|
|
87
|
+
const components = new JsonObject_1.JsonObject()
|
|
88
|
+
.set('schemas', renderedSchemas)
|
|
89
|
+
.set('securitySchemes', apiKey === undefined
|
|
90
|
+
? undefined
|
|
91
|
+
: this.security.schemes(apiKey, schemeNames, inputs.manifestPath))
|
|
92
|
+
.set('headers', this.headers(inputs));
|
|
93
|
+
return new JsonObject_1.JsonObject()
|
|
94
|
+
.set('openapi', OPENAPI_VERSION)
|
|
95
|
+
.set('info', this.info(inputs, selection))
|
|
96
|
+
.set('servers', this.servers(inputs))
|
|
97
|
+
.set('tags', this.tagList(tags))
|
|
98
|
+
.set('security', hoisted === undefined ? undefined : hoisted.slice())
|
|
99
|
+
.set('paths', paths)
|
|
100
|
+
.set('webhooks', webhooks.orUndefined())
|
|
101
|
+
.set('components', components.orUndefined());
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* A webhook is served by the PARTNER, so neither our failure envelope nor the header we stamp on
|
|
105
|
+
* our own responses is true of it. Publishing either would document their server, not ours.
|
|
106
|
+
*/
|
|
107
|
+
webhookContract() {
|
|
108
|
+
return new OperationRenderer_1.ResponseContract(new Map(), undefined, undefined);
|
|
109
|
+
}
|
|
110
|
+
/** One contract's ACCEPTED endpoints, into `paths` or into `webhooks`. */
|
|
111
|
+
renderContract(contractModel, selection, operations, contract, webhookContract, perOperationSecurity, paths, webhooks, tags) {
|
|
112
|
+
const model = contractModel.model;
|
|
113
|
+
const tag = contractModel.entry.tag;
|
|
114
|
+
for (const endpoint of model.endpoints) {
|
|
115
|
+
if (!selection.acceptsEndpoint(endpoint)) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
// A tag is added only by an operation that survived, so a document never advertises a
|
|
119
|
+
// section of its sidebar that turns out to be empty.
|
|
120
|
+
tags.set(tag, this.tagProse(model) ?? '');
|
|
121
|
+
if (contractModel.entry.isWebhook()) {
|
|
122
|
+
webhooks.set(this.eventName(endpoint), new JsonObject_1.JsonObject().set('post', operations.webhook(endpoint, model.contractName, tag, webhookContract)));
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const url = `${model.basePath}${endpoint.path}`;
|
|
126
|
+
const existing = paths.get(url);
|
|
127
|
+
const item = existing instanceof JsonObject_1.JsonObject ? existing : new JsonObject_1.JsonObject();
|
|
128
|
+
item.set(endpoint.httpMethod.toLowerCase(), operations.operation(endpoint, model.contractName, tag, contract, endpoint.auth?.apiKey === undefined ? undefined : perOperationSecurity));
|
|
129
|
+
paths.set(url, item);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* A webhook is keyed by the EVENT NAME, with the `@ApiPath` base deliberately NOT prepended.
|
|
134
|
+
*
|
|
135
|
+
* There is no url of ours here — the partner hosts the endpoint, at whatever path they choose —
|
|
136
|
+
* so publishing `/our-base/delivered` would document a route that exists nowhere. What we are
|
|
137
|
+
* naming is the event we will send.
|
|
138
|
+
*/
|
|
139
|
+
eventName(endpoint) {
|
|
140
|
+
return endpoint.path.replace(/^\/+/, '');
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Every named type from every contract, merged.
|
|
144
|
+
*
|
|
145
|
+
* Two contracts declaring DIFFERENT types under one name is a hard failure, not a first-wins
|
|
146
|
+
* merge: `components.schemas` is keyed by name, so one of the two would be published as the
|
|
147
|
+
* other's shape, and the operation referring to it would be quietly wrong.
|
|
148
|
+
*/
|
|
149
|
+
mergedTypes(inputs) {
|
|
150
|
+
const merged = new Map();
|
|
151
|
+
const sources = inputs.contracts.map((each) => each.model);
|
|
152
|
+
const all = inputs.errorType === undefined ? sources : sources.concat([inputs.errorType]);
|
|
153
|
+
for (const model of all) {
|
|
154
|
+
for (const name of Array.from(model.types.keys())) {
|
|
155
|
+
const incoming = model.types.get(name);
|
|
156
|
+
const existing = merged.get(name);
|
|
157
|
+
if (existing !== undefined &&
|
|
158
|
+
this.signature(existing) !== this.signature(incoming)) {
|
|
159
|
+
throw new OpenApiGenerationError_1.OpenApiGenerationError(`two different types are both named '${name}'`, `${model.contractName} (${inputs.manifestPath})`, 'Rename one of them. `components.schemas` is keyed by name, so one shape ' +
|
|
160
|
+
'would be published as the other.');
|
|
161
|
+
}
|
|
162
|
+
merged.set(name, incoming);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return merged;
|
|
166
|
+
}
|
|
167
|
+
/** Enough of a type's shape to tell two same-named types apart without comparing prose. */
|
|
168
|
+
signature(type) {
|
|
169
|
+
const fields = type.fields.map((field) => field.name).join(',');
|
|
170
|
+
return `${fields}|${type.enumValues.join(',')}|${type.unionRefNames.join(',')}`;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* The ONE api-key regime this document publishes.
|
|
174
|
+
*
|
|
175
|
+
* Two regimes in one document is a hard failure: `securitySchemeNames` is a single ordered list,
|
|
176
|
+
* so there is no honest way to say which regime a given name belongs to, and a document that
|
|
177
|
+
* guessed would publish one regime's header names under the other's scheme keys.
|
|
178
|
+
*/
|
|
179
|
+
singleApiKey(inputs, contracts, selection) {
|
|
180
|
+
let found;
|
|
181
|
+
for (const endpoint of this.selectedEndpoints(contracts, selection)) {
|
|
182
|
+
const apiKey = endpoint.auth?.apiKey;
|
|
183
|
+
if (apiKey === undefined) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (found !== undefined && found.regime !== apiKey.regime) {
|
|
187
|
+
throw new OpenApiGenerationError_1.OpenApiGenerationError(`the ${selection.fileName} document mixes the api-key regimes ` +
|
|
188
|
+
`'${found.regime}' ` +
|
|
189
|
+
`and '${apiKey.regime}'`, inputs.manifestPath, 'Publish one regime per document — split the manifest, or move the ' +
|
|
190
|
+
"other regime's contract out of `apis`.");
|
|
191
|
+
}
|
|
192
|
+
found = apiKey;
|
|
193
|
+
}
|
|
194
|
+
return found;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* True when EVERY selected operation demands the credential, which is what licenses hoisting the
|
|
198
|
+
* requirement to the document. A document that mixes credentialled and uncredentialled routes
|
|
199
|
+
* stamps it per-operation instead — hoisting there would tell a partner that a public endpoint
|
|
200
|
+
* needs a key.
|
|
201
|
+
*
|
|
202
|
+
* Judged per DOCUMENT, over the operations that document actually contains, because that is the
|
|
203
|
+
* only question the document's own `security` block answers.
|
|
204
|
+
*/
|
|
205
|
+
everyOperationIsCovered(contracts, selection) {
|
|
206
|
+
const served = this.selectedEndpoints(contracts, selection);
|
|
207
|
+
return (served.length > 0 &&
|
|
208
|
+
served.every((e) => e.auth?.apiKey !== undefined));
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* The endpoints this selection accepts that are routes on OUR server. A webhook is somebody
|
|
212
|
+
* else's route, so it never counts toward our security requirement or our regime.
|
|
213
|
+
*/
|
|
214
|
+
selectedEndpoints(contracts, selection) {
|
|
215
|
+
const served = [];
|
|
216
|
+
for (const contract of contracts) {
|
|
217
|
+
if (contract.entry.isWebhook()) {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
for (const endpoint of contract.model.endpoints) {
|
|
221
|
+
if (selection.acceptsEndpoint(endpoint)) {
|
|
222
|
+
served.push(endpoint);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return served;
|
|
227
|
+
}
|
|
228
|
+
/** `info`, whose `description` is the ONE piece of prose that differs between the documents. */
|
|
229
|
+
info(inputs, selection) {
|
|
230
|
+
return new JsonObject_1.JsonObject()
|
|
231
|
+
.set('title', inputs.manifest.title)
|
|
232
|
+
.set('version', inputs.manifest.version)
|
|
233
|
+
.set('description', selection.description(inputs.description));
|
|
234
|
+
}
|
|
235
|
+
servers(inputs) {
|
|
236
|
+
if (inputs.manifest.servers.length === 0) {
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
return inputs.manifest.servers.map((server) => new JsonObject_1.JsonObject().set('url', server.url).set('description', server.description));
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* `tags[]` in MANIFEST ORDER, because that order IS the published sidebar — one of the two
|
|
243
|
+
* orders in this document that carries meaning and is therefore NOT sorted.
|
|
244
|
+
*/
|
|
245
|
+
tagList(tags) {
|
|
246
|
+
const list = [];
|
|
247
|
+
for (const name of tags.keys()) {
|
|
248
|
+
const prose = tags.get(name);
|
|
249
|
+
list.push(new JsonObject_1.JsonObject()
|
|
250
|
+
.set('name', name)
|
|
251
|
+
.set('description', prose === '' ? undefined : prose));
|
|
252
|
+
}
|
|
253
|
+
return list.length === 0 ? undefined : list;
|
|
254
|
+
}
|
|
255
|
+
tagProse(model) {
|
|
256
|
+
return model.description.trim() === '' ? undefined : model.description;
|
|
257
|
+
}
|
|
258
|
+
errorResponses(inputs) {
|
|
259
|
+
const responses = new Map();
|
|
260
|
+
for (const response of inputs.manifest.errors?.responses ?? []) {
|
|
261
|
+
responses.set(response.status, response.description);
|
|
262
|
+
}
|
|
263
|
+
return responses;
|
|
264
|
+
}
|
|
265
|
+
errorSchemaRef(inputs, schemas) {
|
|
266
|
+
const errors = inputs.manifest.errors;
|
|
267
|
+
if (errors === undefined || inputs.errorType === undefined) {
|
|
268
|
+
return undefined;
|
|
269
|
+
}
|
|
270
|
+
// Rendering the reference is what puts the error body into `components.schemas`, so the
|
|
271
|
+
// published shape is the compiler's answer about that TS type and not a hand-copied one.
|
|
272
|
+
return schemas.type(api_doc_model_1.TypeRef.ref(errors.type), '#/components/schemas/error');
|
|
273
|
+
}
|
|
274
|
+
/** `components.headers`, one entry per declared response header. */
|
|
275
|
+
headers(inputs) {
|
|
276
|
+
const headers = new JsonObject_1.JsonObject();
|
|
277
|
+
for (const header of inputs.responseHeaders) {
|
|
278
|
+
headers.set(header.headerName, new JsonObject_1.JsonObject()
|
|
279
|
+
.set('description', header.description)
|
|
280
|
+
.set('schema', new JsonObject_1.JsonObject().set('type', 'string')));
|
|
281
|
+
}
|
|
282
|
+
return headers.orUndefined();
|
|
283
|
+
}
|
|
284
|
+
/** The `$ref`s every SUCCESS response carries at those headers. */
|
|
285
|
+
headerRefs(inputs) {
|
|
286
|
+
const refs = new JsonObject_1.JsonObject();
|
|
287
|
+
for (const header of inputs.responseHeaders) {
|
|
288
|
+
refs.set(header.headerName, new JsonObject_1.JsonObject().set('$ref', `#/components/headers/${header.headerName}`));
|
|
289
|
+
}
|
|
290
|
+
return refs.orUndefined();
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* The guard. An unmapped field is a published partner-facing field with no shape, and there is
|
|
294
|
+
* deliberately no flag to switch this off — the cure is at the contract, by naming the type.
|
|
295
|
+
*/
|
|
296
|
+
refuseUnmappedFields(unmapped, inputs, selection) {
|
|
297
|
+
if (unmapped.length === 0) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const pointers = unmapped.map((field) => `${field.pointer} (${field.typeText})`);
|
|
301
|
+
throw new OpenApiGenerationError_1.OpenApiGenerationError(`${unmapped.length} field(s) in ${selection.fileName} have no schema it can state`, inputs.manifestPath, 'Give each one a type a document can carry — a named DTO, an array of one, a ' +
|
|
302
|
+
'string-literal union, or Record<string, X>. An untyped field publishes as "anything".', pointers);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
exports.OpenApiGenerator = OpenApiGenerator;
|
|
306
|
+
//# sourceMappingURL=OpenApiGenerator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OpenApiGenerator.js","sourceRoot":"","sources":["../../../../../../packages/docs/openapi-generator/src/generate/OpenApiGenerator.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n ApiDocModel,\n DocumentedApiKey,\n DocumentedEndpoint,\n DocumentedField,\n DocumentedType,\n TypeRef,\n} from '@webpieces/api-doc-model';\nimport { JsonObject, JsonValue } from '../json/JsonObject';\nimport { OpenApiGenerationError } from '../OpenApiGenerationError';\nimport { ServerEntry } from '../manifest/OpenApiManifest';\nimport {\n ContractModel,\n GeneratedDocument,\n GeneratedDocuments,\n GenerationInputs,\n} from './GenerationInputs';\nimport { DocumentSelection } from './DocumentSelection';\nimport { OperationRenderer, ResponseContract } from './OperationRenderer';\nimport { SchemaRenderer, UnmappedField } from './SchemaRenderer';\nimport { SecurityDeriver } from './SecurityDeriver';\n\n/** The OpenAPI version this generator writes. See the class doc for why 3.1 and not 3.0. */\nconst OPENAPI_VERSION = '3.1.0';\n\n/**\n * {@link GenerationInputs} -> one OpenAPI 3.1.0 document per `@ApiType` some contract declares.\n *\n * ## 3.1, because it is what the model can state HONESTLY\n *\n * `type: [T, \"null\"]` instead of a `nullable` keyword; a `description` legal beside a `$ref`; a\n * top-level `webhooks:` block. Each of those is a fact the model holds that 3.0 would have forced\n * this renderer to drop or to lie about. 3.1's schema dialect is also JSON Schema 2020-12, which is\n * what MCP `tools/list` speaks.\n *\n * ## ONE render function, called once per document\n *\n * ```\n * ApiDocModel --> render(selection) --+--> full-private-openapi.json SVC_TO_SVC contracts\n * +--> public-openapi.json EXTERNAL_CUSTOMER contracts, minus hidden methods\n * +--> mcp-openapi.json MCP contracts\n * ```\n *\n * `components.schemas` is built by walking OUTWARD from the operations the selection accepted, so an\n * unselected operation's DTOs are never constructed. See {@link DocumentSelection} for why that\n * asymmetry — a bug emits nothing, rather than shipping an unreleased feature's schemas with only\n * its URL removed — decides the whole design.\n *\n * ## It REFUSES to write an unmapped field\n *\n * An unmapped type renders as an empty schema, which in JSON Schema means \"anything\". Publishing one\n * is a green build handing a partner a field with no shape, so the guard names the JSON pointer of\n * every one and exits non-zero. There is deliberately NO flag to switch it off: the cure is at the\n * contract, by naming the type.\n */\nexport class OpenApiGenerator {\n private readonly security = new SecurityDeriver();\n\n /**\n * ONE document per `@ApiType` some contract declares, and no others.\n *\n * A document nobody asked for is not written empty — it is not written. That is the ONE\n * conditional in the whole pipeline, and it lives here rather than in three places: no contract\n * declaring `MCP` means no `mcp-openapi.json`, with no second \"is it empty?\" rule to keep in step\n * with the first.\n */\n generate(inputs: GenerationInputs): GeneratedDocuments {\n const documents: GeneratedDocument[] = [];\n for (const selection of DocumentSelection.all()) {\n const contracts = inputs.contracts.filter((each: ContractModel) =>\n selection.acceptsContract(each.model),\n );\n if (contracts.length === 0) {\n continue;\n }\n documents.push(\n new GeneratedDocument(\n selection.fileName,\n this.render(inputs, contracts, selection),\n ),\n );\n }\n return new GeneratedDocuments(documents);\n }\n\n /** ONE document, from the operations this selection accepts and nothing else. */\n private render(\n inputs: GenerationInputs,\n contracts: readonly ContractModel[],\n selection: DocumentSelection,\n ): JsonObject {\n const types = this.mergedTypes(inputs);\n const schemas = new SchemaRenderer(types);\n const operations = new OperationRenderer(\n schemas,\n selection.includeMcpExtensions,\n selection.internalNotice,\n );\n const apiKey = this.singleApiKey(inputs, contracts, selection);\n const schemeNames = inputs.manifest.securitySchemeNames;\n const hoisted =\n apiKey !== undefined && this.everyOperationIsCovered(contracts, selection)\n ? this.security.requirement(schemeNames)\n : undefined;\n\n const contract = new ResponseContract(\n this.errorResponses(inputs),\n this.errorSchemaRef(inputs, schemas),\n this.headerRefs(inputs),\n );\n\n const paths = new JsonObject();\n const webhooks = new JsonObject();\n const tags = new JsonObject();\n for (const contractModel of contracts) {\n this.renderContract(\n contractModel,\n selection,\n operations,\n contract,\n this.webhookContract(),\n hoisted === undefined ? this.security.requirement(schemeNames) : undefined,\n paths,\n webhooks,\n tags,\n );\n }\n\n // The schemas are built HERE, after the operations, because building them is what walks a\n // DTO's fields — and an unmapped field can only be found by that walk. A guard that ran\n // before it would see only the top-level request and response refs and miss every one.\n const renderedSchemas = schemas.components().orUndefined();\n this.refuseUnmappedFields(schemas.unmapped(), inputs, selection);\n\n const components = new JsonObject()\n .set('schemas', renderedSchemas)\n .set(\n 'securitySchemes',\n apiKey === undefined\n ? undefined\n : this.security.schemes(apiKey, schemeNames, inputs.manifestPath),\n )\n .set('headers', this.headers(inputs));\n\n return new JsonObject()\n .set('openapi', OPENAPI_VERSION)\n .set('info', this.info(inputs, selection))\n .set('servers', this.servers(inputs))\n .set('tags', this.tagList(tags))\n .set('security', hoisted === undefined ? undefined : hoisted.slice())\n .set('paths', paths)\n .set('webhooks', webhooks.orUndefined())\n .set('components', components.orUndefined());\n }\n\n /**\n * A webhook is served by the PARTNER, so neither our failure envelope nor the header we stamp on\n * our own responses is true of it. Publishing either would document their server, not ours.\n */\n private webhookContract(): ResponseContract {\n return new ResponseContract(new Map<string, string>(), undefined, undefined);\n }\n\n /** One contract's ACCEPTED endpoints, into `paths` or into `webhooks`. */\n private renderContract(\n contractModel: ContractModel,\n selection: DocumentSelection,\n operations: OperationRenderer,\n contract: ResponseContract,\n webhookContract: ResponseContract,\n perOperationSecurity: readonly JsonValue[] | undefined,\n paths: JsonObject,\n webhooks: JsonObject,\n tags: JsonObject,\n ): void {\n const model = contractModel.model;\n const tag = contractModel.entry.tag;\n for (const endpoint of model.endpoints) {\n if (!selection.acceptsEndpoint(endpoint)) {\n continue;\n }\n // A tag is added only by an operation that survived, so a document never advertises a\n // section of its sidebar that turns out to be empty.\n tags.set(tag, this.tagProse(model) ?? '');\n if (contractModel.entry.isWebhook()) {\n webhooks.set(\n this.eventName(endpoint),\n new JsonObject().set(\n 'post',\n operations.webhook(endpoint, model.contractName, tag, webhookContract),\n ),\n );\n continue;\n }\n const url = `${model.basePath}${endpoint.path}`;\n const existing = paths.get(url);\n const item = existing instanceof JsonObject ? existing : new JsonObject();\n item.set(\n endpoint.httpMethod.toLowerCase(),\n operations.operation(\n endpoint,\n model.contractName,\n tag,\n contract,\n endpoint.auth?.apiKey === undefined ? undefined : perOperationSecurity,\n ),\n );\n paths.set(url, item);\n }\n }\n\n /**\n * A webhook is keyed by the EVENT NAME, with the `@ApiPath` base deliberately NOT prepended.\n *\n * There is no url of ours here — the partner hosts the endpoint, at whatever path they choose —\n * so publishing `/our-base/delivered` would document a route that exists nowhere. What we are\n * naming is the event we will send.\n */\n private eventName(endpoint: DocumentedEndpoint): string {\n return endpoint.path.replace(/^\\/+/, '');\n }\n\n /**\n * Every named type from every contract, merged.\n *\n * Two contracts declaring DIFFERENT types under one name is a hard failure, not a first-wins\n * merge: `components.schemas` is keyed by name, so one of the two would be published as the\n * other's shape, and the operation referring to it would be quietly wrong.\n */\n private mergedTypes(inputs: GenerationInputs): ReadonlyMap<string, DocumentedType> {\n const merged = new Map<string, DocumentedType>();\n const sources = inputs.contracts.map((each: ContractModel) => each.model);\n const all = inputs.errorType === undefined ? sources : sources.concat([inputs.errorType]);\n for (const model of all) {\n for (const name of Array.from(model.types.keys())) {\n const incoming = model.types.get(name)!;\n const existing = merged.get(name);\n if (\n existing !== undefined &&\n this.signature(existing) !== this.signature(incoming)\n ) {\n throw new OpenApiGenerationError(\n `two different types are both named '${name}'`,\n `${model.contractName} (${inputs.manifestPath})`,\n 'Rename one of them. `components.schemas` is keyed by name, so one shape ' +\n 'would be published as the other.',\n );\n }\n merged.set(name, incoming);\n }\n }\n return merged;\n }\n\n /** Enough of a type's shape to tell two same-named types apart without comparing prose. */\n private signature(type: DocumentedType): string {\n const fields = type.fields.map((field: DocumentedField) => field.name).join(',');\n return `${fields}|${type.enumValues.join(',')}|${type.unionRefNames.join(',')}`;\n }\n\n /**\n * The ONE api-key regime this document publishes.\n *\n * Two regimes in one document is a hard failure: `securitySchemeNames` is a single ordered list,\n * so there is no honest way to say which regime a given name belongs to, and a document that\n * guessed would publish one regime's header names under the other's scheme keys.\n */\n private singleApiKey(\n inputs: GenerationInputs,\n contracts: readonly ContractModel[],\n selection: DocumentSelection,\n ): DocumentedApiKey | undefined {\n let found: DocumentedApiKey | undefined;\n for (const endpoint of this.selectedEndpoints(contracts, selection)) {\n const apiKey = endpoint.auth?.apiKey;\n if (apiKey === undefined) {\n continue;\n }\n if (found !== undefined && found.regime !== apiKey.regime) {\n throw new OpenApiGenerationError(\n `the ${selection.fileName} document mixes the api-key regimes ` +\n `'${found.regime}' ` +\n `and '${apiKey.regime}'`,\n inputs.manifestPath,\n 'Publish one regime per document — split the manifest, or move the ' +\n \"other regime's contract out of `apis`.\",\n );\n }\n found = apiKey;\n }\n return found;\n }\n\n /**\n * True when EVERY selected operation demands the credential, which is what licenses hoisting the\n * requirement to the document. A document that mixes credentialled and uncredentialled routes\n * stamps it per-operation instead — hoisting there would tell a partner that a public endpoint\n * needs a key.\n *\n * Judged per DOCUMENT, over the operations that document actually contains, because that is the\n * only question the document's own `security` block answers.\n */\n private everyOperationIsCovered(\n contracts: readonly ContractModel[],\n selection: DocumentSelection,\n ): boolean {\n const served = this.selectedEndpoints(contracts, selection);\n return (\n served.length > 0 &&\n served.every((e: DocumentedEndpoint) => e.auth?.apiKey !== undefined)\n );\n }\n\n /**\n * The endpoints this selection accepts that are routes on OUR server. A webhook is somebody\n * else's route, so it never counts toward our security requirement or our regime.\n */\n private selectedEndpoints(\n contracts: readonly ContractModel[],\n selection: DocumentSelection,\n ): readonly DocumentedEndpoint[] {\n const served: DocumentedEndpoint[] = [];\n for (const contract of contracts) {\n if (contract.entry.isWebhook()) {\n continue;\n }\n for (const endpoint of contract.model.endpoints) {\n if (selection.acceptsEndpoint(endpoint)) {\n served.push(endpoint);\n }\n }\n }\n return served;\n }\n\n /** `info`, whose `description` is the ONE piece of prose that differs between the documents. */\n private info(inputs: GenerationInputs, selection: DocumentSelection): JsonObject {\n return new JsonObject()\n .set('title', inputs.manifest.title)\n .set('version', inputs.manifest.version)\n .set('description', selection.description(inputs.description));\n }\n\n private servers(inputs: GenerationInputs): readonly JsonValue[] | undefined {\n if (inputs.manifest.servers.length === 0) {\n return undefined;\n }\n return inputs.manifest.servers.map((server: ServerEntry) =>\n new JsonObject().set('url', server.url).set('description', server.description),\n );\n }\n\n /**\n * `tags[]` in MANIFEST ORDER, because that order IS the published sidebar — one of the two\n * orders in this document that carries meaning and is therefore NOT sorted.\n */\n private tagList(tags: JsonObject): readonly JsonValue[] | undefined {\n const list: JsonValue[] = [];\n for (const name of tags.keys()) {\n const prose = tags.get(name);\n list.push(\n new JsonObject()\n .set('name', name)\n .set('description', prose === '' ? undefined : prose),\n );\n }\n return list.length === 0 ? undefined : list;\n }\n\n private tagProse(model: ApiDocModel): string | undefined {\n return model.description.trim() === '' ? undefined : model.description;\n }\n\n private errorResponses(inputs: GenerationInputs): ReadonlyMap<string, string> {\n const responses = new Map<string, string>();\n for (const response of inputs.manifest.errors?.responses ?? []) {\n responses.set(response.status, response.description);\n }\n return responses;\n }\n\n private errorSchemaRef(\n inputs: GenerationInputs,\n schemas: SchemaRenderer,\n ): JsonObject | undefined {\n const errors = inputs.manifest.errors;\n if (errors === undefined || inputs.errorType === undefined) {\n return undefined;\n }\n // Rendering the reference is what puts the error body into `components.schemas`, so the\n // published shape is the compiler's answer about that TS type and not a hand-copied one.\n return schemas.type(TypeRef.ref(errors.type), '#/components/schemas/error');\n }\n\n /** `components.headers`, one entry per declared response header. */\n private headers(inputs: GenerationInputs): JsonObject | undefined {\n const headers = new JsonObject();\n for (const header of inputs.responseHeaders) {\n headers.set(\n header.headerName,\n new JsonObject()\n .set('description', header.description)\n .set('schema', new JsonObject().set('type', 'string')),\n );\n }\n return headers.orUndefined();\n }\n\n /** The `$ref`s every SUCCESS response carries at those headers. */\n private headerRefs(inputs: GenerationInputs): JsonObject | undefined {\n const refs = new JsonObject();\n for (const header of inputs.responseHeaders) {\n refs.set(\n header.headerName,\n new JsonObject().set('$ref', `#/components/headers/${header.headerName}`),\n );\n }\n return refs.orUndefined();\n }\n\n /**\n * The guard. An unmapped field is a published partner-facing field with no shape, and there is\n * deliberately no flag to switch this off — the cure is at the contract, by naming the type.\n */\n private refuseUnmappedFields(\n unmapped: readonly UnmappedField[],\n inputs: GenerationInputs,\n selection: DocumentSelection,\n ): void {\n if (unmapped.length === 0) {\n return;\n }\n const pointers = unmapped.map(\n (field: UnmappedField) => `${field.pointer} (${field.typeText})`,\n );\n throw new OpenApiGenerationError(\n `${unmapped.length} field(s) in ${selection.fileName} have no schema it can state`,\n inputs.manifestPath,\n 'Give each one a type a document can carry — a named DTO, an array of one, a ' +\n 'string-literal union, or Record<string, X>. An untyped field publishes as \"anything\".',\n pointers,\n );\n }\n}\n"]}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { DocumentedEndpoint } from '@webpieces/api-doc-model';
|
|
2
|
+
import { JsonObject, JsonValue } from '../json/JsonObject';
|
|
3
|
+
import { SchemaRenderer } from './SchemaRenderer';
|
|
4
|
+
/** `Promise<void>` on the wire, said once. */
|
|
5
|
+
/** The response document, and the responses a document-wide failure contract adds to it. */
|
|
6
|
+
export declare class ResponseContract {
|
|
7
|
+
/** Status -> description, from the manifest's `errors.responses`, in declared order. */
|
|
8
|
+
readonly errors: ReadonlyMap<string, string>;
|
|
9
|
+
/** `$ref` at the error body schema, when the manifest declared one. */
|
|
10
|
+
readonly errorSchemaRef: JsonObject | undefined;
|
|
11
|
+
/** Header name -> its `components.headers` `$ref`, on every success response. */
|
|
12
|
+
readonly headers: JsonObject | undefined;
|
|
13
|
+
constructor(
|
|
14
|
+
/** Status -> description, from the manifest's `errors.responses`, in declared order. */
|
|
15
|
+
errors: ReadonlyMap<string, string>,
|
|
16
|
+
/** `$ref` at the error body schema, when the manifest declared one. */
|
|
17
|
+
errorSchemaRef: JsonObject | undefined,
|
|
18
|
+
/** Header name -> its `components.headers` `$ref`, on every success response. */
|
|
19
|
+
headers: JsonObject | undefined);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* ONE {@link DocumentedEndpoint} -> one OpenAPI Operation Object.
|
|
23
|
+
*
|
|
24
|
+
* ## `summary` is the operation NAME, never a sentence
|
|
25
|
+
*
|
|
26
|
+
* A docs theme titles the endpoint's page from `summary` and puts `description` in the body. Writing
|
|
27
|
+
* the first sentence of the prose into `summary` therefore produces a sidebar of sentences and a page
|
|
28
|
+
* whose heading repeats its own first line. The method name is what a reader is looking for, and it
|
|
29
|
+
* is also what the generated client calls the method, so the two agree by construction.
|
|
30
|
+
*
|
|
31
|
+
* ## The MCP extensions ride HERE, on the operation
|
|
32
|
+
*
|
|
33
|
+
* `x-mcp-tool`, `x-mcp-hints`, `x-mcp-description` and `x-mcp-auth` are stamped on every operation
|
|
34
|
+
* carrying the tool decorator, hidden or not: an internal endpoint that is an agent tool is a
|
|
35
|
+
* legitimate agent tool, and requiring it to be published to partners in order to be one would be
|
|
36
|
+
* exactly the wrong coupling.
|
|
37
|
+
*
|
|
38
|
+
* They are stamped on the BASE, which every published document is derived from, and
|
|
39
|
+
* {@link DocumentDeriver} STRIPS them from the customer document. Stamping-then-stripping rather than
|
|
40
|
+
* rendering the customer document without them is the same choice the whole deriver rests on: one
|
|
41
|
+
* render of one operation, so two documents holding it cannot disagree about anything else in it.
|
|
42
|
+
*/
|
|
43
|
+
export declare class OperationRenderer {
|
|
44
|
+
private readonly schemas;
|
|
45
|
+
/** True only for the MCP document — see {@link withMcp}. */
|
|
46
|
+
private readonly includeMcp;
|
|
47
|
+
/** True only for the private document — see {@link triggerSentence}. */
|
|
48
|
+
private readonly includeTrigger;
|
|
49
|
+
constructor(schemas: SchemaRenderer,
|
|
50
|
+
/** True only for the MCP document — see {@link withMcp}. */
|
|
51
|
+
includeMcp: boolean,
|
|
52
|
+
/** True only for the private document — see {@link triggerSentence}. */
|
|
53
|
+
includeTrigger: boolean);
|
|
54
|
+
/** An ordinary served route. */
|
|
55
|
+
operation(endpoint: DocumentedEndpoint, contractName: string, tag: string, contract: ResponseContract, security: readonly JsonValue[] | undefined): JsonObject;
|
|
56
|
+
/**
|
|
57
|
+
* A call WE make to a PARTNER's server.
|
|
58
|
+
*
|
|
59
|
+
* It carries `x-webpieces-webhook: true`, which is the one fact about it a document cannot state
|
|
60
|
+
* any other way. It gets no trigger sentence and no security at all, because neither is true of
|
|
61
|
+
* it: nothing of ours triggers it and nothing of ours authenticates it — our api key must never
|
|
62
|
+
* be published as a guard on somebody else's endpoint.
|
|
63
|
+
*/
|
|
64
|
+
webhook(endpoint: DocumentedEndpoint, contractName: string, tag: string, contract: ResponseContract): JsonObject;
|
|
65
|
+
private common;
|
|
66
|
+
/**
|
|
67
|
+
* The JSDoc body, plus the sentences that are DERIVED rather than written.
|
|
68
|
+
*
|
|
69
|
+
* The retry sentence comes from the endpoint's declared `operation`, which every integration
|
|
70
|
+
* wants to know and most APIs state nowhere. It is a SENTENCE and not a vendor extension because
|
|
71
|
+
* Swagger UI and most themes do not render `x-` extensions and no standard generator reads
|
|
72
|
+
* `x-webpieces-*`: an extension would be invisible to the human and unread by the machine. The
|
|
73
|
+
* mapping it is derived from is published once, in `info.description`, so the rule is stated and
|
|
74
|
+
* not only its consequences.
|
|
75
|
+
*/
|
|
76
|
+
private describe;
|
|
77
|
+
/**
|
|
78
|
+
* WHAT FIRES this endpoint, in the private document only.
|
|
79
|
+
*
|
|
80
|
+
* A customer calls what they are given a url for; whether ours runs off a queue or a clock is our
|
|
81
|
+
* business and would only invite a question they cannot act on. It is a SENTENCE and not
|
|
82
|
+
* `x-webpieces-trigger` for the same reason the retry sentence is: Swagger UI and most themes do
|
|
83
|
+
* not render `x-` extensions, and no standard generator reads `x-webpieces-*`, so an extension is
|
|
84
|
+
* invisible to the human and unread by the machine.
|
|
85
|
+
*/
|
|
86
|
+
private triggerSentence;
|
|
87
|
+
/** An endpoint whose declared credential is "none", said out loud rather than left absent. */
|
|
88
|
+
private isPublic;
|
|
89
|
+
private retrySentence;
|
|
90
|
+
/**
|
|
91
|
+
* The MCP extensions, stamped only on the document that has an agent for a reader.
|
|
92
|
+
*
|
|
93
|
+
* ## The three side-effect hints are COMPUTED, never declared
|
|
94
|
+
*
|
|
95
|
+
* `mcpHintsForOperation` in `@webpieces/core-util` is the one place the mapping from
|
|
96
|
+
* `READ | WRITE_IDEMPOTENT | WRITE` to `readOnlyHint` / `destructiveHint` / `idempotentHint`
|
|
97
|
+
* lives, and this calls it rather than reimplementing it. The endpoint's `operation` already
|
|
98
|
+
* states whether repeating the call is safe; a hand-declared hint would be a second answer to a
|
|
99
|
+
* question the contract has answered, and the two could disagree. `openWorldHint` is the only one
|
|
100
|
+
* a human declares, because it is a judgement about the world rather than a consequence of the
|
|
101
|
+
* operation.
|
|
102
|
+
*
|
|
103
|
+
* ## There is no separate MCP description
|
|
104
|
+
*
|
|
105
|
+
* The agent reads the operation's `description` — the method's JSDoc — byte-identical to what the
|
|
106
|
+
* partner reads. `x-mcp-description` appears ONLY when the author wrote an explicit `@mcp` JSDoc
|
|
107
|
+
* tag, which is a deliberate act rather than a second copy of the same paragraph.
|
|
108
|
+
*/
|
|
109
|
+
private withMcp;
|
|
110
|
+
/**
|
|
111
|
+
* The endpoint's operation as the REAL exported constant.
|
|
112
|
+
*
|
|
113
|
+
* The model carries it as a string, verbatim from the source, because the model invents no
|
|
114
|
+
* taxonomy. Matching it back to the constant here — rather than casting — is what makes a rename
|
|
115
|
+
* of `WRITE_IDEMPOTENT` in `core-util` a compile error in this file.
|
|
116
|
+
*/
|
|
117
|
+
private operationOf;
|
|
118
|
+
/**
|
|
119
|
+
* The request DTO as a body. Only for `POST`: OpenAPI gives a `GET` body no defined semantics and
|
|
120
|
+
* most tooling drops it, so publishing one would document a request no generated client sends.
|
|
121
|
+
*/
|
|
122
|
+
private requestBody;
|
|
123
|
+
private responses;
|
|
124
|
+
private success;
|
|
125
|
+
private failure;
|
|
126
|
+
/** `Promise<void>` reaches the model as the `unknown` primitive — there is no body to document. */
|
|
127
|
+
private isVoid;
|
|
128
|
+
private prose;
|
|
129
|
+
}
|