@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.
Files changed (68) hide show
  1. package/README.md +118 -0
  2. package/package.json +32 -0
  3. package/src/OpenApiGenerationError.d.ts +41 -0
  4. package/src/OpenApiGenerationError.js +45 -0
  5. package/src/OpenApiGenerationError.js.map +1 -0
  6. package/src/cli/OpenApiCli.d.ts +33 -0
  7. package/src/cli/OpenApiCli.js +100 -0
  8. package/src/cli/OpenApiCli.js.map +1 -0
  9. package/src/cli/WpOpenApiMain.d.ts +25 -0
  10. package/src/cli/WpOpenApiMain.js +62 -0
  11. package/src/cli/WpOpenApiMain.js.map +1 -0
  12. package/src/cli/wp-openapi.d.ts +2 -0
  13. package/src/cli/wp-openapi.js +18 -0
  14. package/src/cli/wp-openapi.js.map +1 -0
  15. package/src/emit/ArtifactWriter.d.ts +44 -0
  16. package/src/emit/ArtifactWriter.js +75 -0
  17. package/src/emit/ArtifactWriter.js.map +1 -0
  18. package/src/generate/DocumentSelection.d.ts +96 -0
  19. package/src/generate/DocumentSelection.js +153 -0
  20. package/src/generate/DocumentSelection.js.map +1 -0
  21. package/src/generate/GenerationInputs.d.ts +67 -0
  22. package/src/generate/GenerationInputs.js +88 -0
  23. package/src/generate/GenerationInputs.js.map +1 -0
  24. package/src/generate/OpenApiGenerator.d.ts +113 -0
  25. package/src/generate/OpenApiGenerator.js +306 -0
  26. package/src/generate/OpenApiGenerator.js.map +1 -0
  27. package/src/generate/OperationRenderer.d.ts +129 -0
  28. package/src/generate/OperationRenderer.js +256 -0
  29. package/src/generate/OperationRenderer.js.map +1 -0
  30. package/src/generate/SchemaRenderer.d.ts +89 -0
  31. package/src/generate/SchemaRenderer.js +236 -0
  32. package/src/generate/SchemaRenderer.js.map +1 -0
  33. package/src/generate/SecurityDeriver.d.ts +39 -0
  34. package/src/generate/SecurityDeriver.js +81 -0
  35. package/src/generate/SecurityDeriver.js.map +1 -0
  36. package/src/index.d.ts +32 -0
  37. package/src/index.js +70 -0
  38. package/src/index.js.map +1 -0
  39. package/src/json/JsonObject.d.ts +35 -0
  40. package/src/json/JsonObject.js +32 -0
  41. package/src/json/JsonObject.js.map +1 -0
  42. package/src/json/JsonWriter.d.ts +18 -0
  43. package/src/json/JsonWriter.js +48 -0
  44. package/src/json/JsonWriter.js.map +1 -0
  45. package/src/json/YamlReader.d.ts +35 -0
  46. package/src/json/YamlReader.js +111 -0
  47. package/src/json/YamlReader.js.map +1 -0
  48. package/src/json/YamlWriter.d.ts +35 -0
  49. package/src/json/YamlWriter.js +88 -0
  50. package/src/json/YamlWriter.js.map +1 -0
  51. package/src/load/ExportedConstantFolder.d.ts +21 -0
  52. package/src/load/ExportedConstantFolder.js +57 -0
  53. package/src/load/ExportedConstantFolder.js.map +1 -0
  54. package/src/load/ForeignFailure.d.ts +24 -0
  55. package/src/load/ForeignFailure.js +41 -0
  56. package/src/load/ForeignFailure.js.map +1 -0
  57. package/src/load/InputsLoader.d.ts +43 -0
  58. package/src/load/InputsLoader.js +148 -0
  59. package/src/load/InputsLoader.js.map +1 -0
  60. package/src/manifest/JsonReader.d.ts +37 -0
  61. package/src/manifest/JsonReader.js +109 -0
  62. package/src/manifest/JsonReader.js.map +1 -0
  63. package/src/manifest/ManifestLoader.d.ts +20 -0
  64. package/src/manifest/ManifestLoader.js +69 -0
  65. package/src/manifest/ManifestLoader.js.map +1 -0
  66. package/src/manifest/OpenApiManifest.d.ts +116 -0
  67. package/src/manifest/OpenApiManifest.js +142 -0
  68. package/src/manifest/OpenApiManifest.js.map +1 -0
@@ -0,0 +1,256 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OperationRenderer = exports.ResponseContract = void 0;
4
+ const core_util_1 = require("@webpieces/core-util");
5
+ const JsonObject_1 = require("../json/JsonObject");
6
+ /** What a webhook's `void` return means on the wire, said once. */
7
+ const ACKNOWLEDGE = 'Return any 2xx to acknowledge delivery. The body is ignored.';
8
+ /** `Promise<void>` on the wire, said once. */
9
+ /** The response document, and the responses a document-wide failure contract adds to it. */
10
+ class ResponseContract {
11
+ errors;
12
+ errorSchemaRef;
13
+ headers;
14
+ constructor(
15
+ /** Status -> description, from the manifest's `errors.responses`, in declared order. */
16
+ errors,
17
+ /** `$ref` at the error body schema, when the manifest declared one. */
18
+ errorSchemaRef,
19
+ /** Header name -> its `components.headers` `$ref`, on every success response. */
20
+ headers) {
21
+ this.errors = errors;
22
+ this.errorSchemaRef = errorSchemaRef;
23
+ this.headers = headers;
24
+ }
25
+ }
26
+ exports.ResponseContract = ResponseContract;
27
+ /**
28
+ * ONE {@link DocumentedEndpoint} -> one OpenAPI Operation Object.
29
+ *
30
+ * ## `summary` is the operation NAME, never a sentence
31
+ *
32
+ * A docs theme titles the endpoint's page from `summary` and puts `description` in the body. Writing
33
+ * the first sentence of the prose into `summary` therefore produces a sidebar of sentences and a page
34
+ * whose heading repeats its own first line. The method name is what a reader is looking for, and it
35
+ * is also what the generated client calls the method, so the two agree by construction.
36
+ *
37
+ * ## The MCP extensions ride HERE, on the operation
38
+ *
39
+ * `x-mcp-tool`, `x-mcp-hints`, `x-mcp-description` and `x-mcp-auth` are stamped on every operation
40
+ * carrying the tool decorator, hidden or not: an internal endpoint that is an agent tool is a
41
+ * legitimate agent tool, and requiring it to be published to partners in order to be one would be
42
+ * exactly the wrong coupling.
43
+ *
44
+ * They are stamped on the BASE, which every published document is derived from, and
45
+ * {@link DocumentDeriver} STRIPS them from the customer document. Stamping-then-stripping rather than
46
+ * rendering the customer document without them is the same choice the whole deriver rests on: one
47
+ * render of one operation, so two documents holding it cannot disagree about anything else in it.
48
+ */
49
+ class OperationRenderer {
50
+ schemas;
51
+ includeMcp;
52
+ includeTrigger;
53
+ constructor(schemas,
54
+ /** True only for the MCP document — see {@link withMcp}. */
55
+ includeMcp,
56
+ /** True only for the private document — see {@link triggerSentence}. */
57
+ includeTrigger) {
58
+ this.schemas = schemas;
59
+ this.includeMcp = includeMcp;
60
+ this.includeTrigger = includeTrigger;
61
+ }
62
+ /** An ordinary served route. */
63
+ operation(endpoint, contractName, tag, contract, security) {
64
+ const operation = this.common(endpoint, contractName, tag, contract);
65
+ // An endpoint that authenticates NOBODY says so explicitly: `security: []` is OpenAPI's
66
+ // spelling of "no credential required", and leaving the key off instead means "inherit the
67
+ // document's", which is a different statement that happens to coincide today.
68
+ const explicit = this.isPublic(endpoint) ? [] : security;
69
+ operation.set('security', explicit === undefined ? undefined : explicit.slice());
70
+ return this.withMcp(operation, endpoint);
71
+ }
72
+ /**
73
+ * A call WE make to a PARTNER's server.
74
+ *
75
+ * It carries `x-webpieces-webhook: true`, which is the one fact about it a document cannot state
76
+ * any other way. It gets no trigger sentence and no security at all, because neither is true of
77
+ * it: nothing of ours triggers it and nothing of ours authenticates it — our api key must never
78
+ * be published as a guard on somebody else's endpoint.
79
+ */
80
+ webhook(endpoint, contractName, tag, contract) {
81
+ const operation = this.common(endpoint, contractName, tag, contract);
82
+ return this.withMcp(operation.set('x-webpieces-webhook', true), endpoint);
83
+ }
84
+ common(endpoint, contractName, tag, contract) {
85
+ return new JsonObject_1.JsonObject()
86
+ .set('operationId', `${contractName}_${endpoint.methodName}`)
87
+ .set('summary', endpoint.methodName)
88
+ .set('description', this.describe(endpoint))
89
+ .set('tags', [tag])
90
+ .set('requestBody', this.requestBody(endpoint, contractName))
91
+ .set('responses', this.responses(endpoint, contractName, contract));
92
+ }
93
+ /**
94
+ * The JSDoc body, plus the sentences that are DERIVED rather than written.
95
+ *
96
+ * The retry sentence comes from the endpoint's declared `operation`, which every integration
97
+ * wants to know and most APIs state nowhere. It is a SENTENCE and not a vendor extension because
98
+ * Swagger UI and most themes do not render `x-` extensions and no standard generator reads
99
+ * `x-webpieces-*`: an extension would be invisible to the human and unread by the machine. The
100
+ * mapping it is derived from is published once, in `info.description`, so the rule is stated and
101
+ * not only its consequences.
102
+ */
103
+ describe(endpoint) {
104
+ const parts = [];
105
+ const body = this.prose(endpoint.description);
106
+ if (body !== undefined) {
107
+ parts.push(body);
108
+ }
109
+ parts.push(this.retrySentence(endpoint));
110
+ if (this.includeTrigger) {
111
+ parts.push(this.triggerSentence(endpoint));
112
+ }
113
+ if (endpoint.openWorld) {
114
+ parts.push('Reaches beyond this service: it may act on an external system, so its effects ' +
115
+ 'are not all visible here.');
116
+ }
117
+ return parts.join('\n\n');
118
+ }
119
+ /**
120
+ * WHAT FIRES this endpoint, in the private document only.
121
+ *
122
+ * A customer calls what they are given a url for; whether ours runs off a queue or a clock is our
123
+ * business and would only invite a question they cannot act on. It is a SENTENCE and not
124
+ * `x-webpieces-trigger` for the same reason the retry sentence is: Swagger UI and most themes do
125
+ * not render `x-` extensions, and no standard generator reads `x-webpieces-*`, so an extension is
126
+ * invisible to the human and unread by the machine.
127
+ */
128
+ triggerSentence(endpoint) {
129
+ if (endpoint.kind === core_util_1.CLOUDTASKS) {
130
+ return 'Triggered by a queued task, delivered asynchronously.';
131
+ }
132
+ if (endpoint.kind === core_util_1.CRON) {
133
+ return 'Triggered on a schedule, not by a caller.';
134
+ }
135
+ if (endpoint.kind === core_util_1.EXTERNAL) {
136
+ const caller = endpoint.options.calledBy;
137
+ return caller === undefined
138
+ ? 'Triggered by an outside system.'
139
+ : `Triggered by an outside system: ${caller}.`;
140
+ }
141
+ return endpoint.kind === core_util_1.RPC
142
+ ? 'Triggered by a direct call.'
143
+ : `Triggered by: ${endpoint.kind}.`;
144
+ }
145
+ /** An endpoint whose declared credential is "none", said out loud rather than left absent. */
146
+ isPublic(endpoint) {
147
+ return endpoint.auth?.decorator === core_util_1.WpAuthPublic.name;
148
+ }
149
+ retrySentence(endpoint) {
150
+ if (endpoint.operation === core_util_1.READ) {
151
+ return 'Safe to retry: this operation is read-only and idempotent.';
152
+ }
153
+ if (endpoint.operation === core_util_1.WRITE_IDEMPOTENT) {
154
+ return 'Safe to retry: this operation changes state but is idempotent.';
155
+ }
156
+ return 'NOT safe to retry: this operation changes state and is not idempotent.';
157
+ }
158
+ /**
159
+ * The MCP extensions, stamped only on the document that has an agent for a reader.
160
+ *
161
+ * ## The three side-effect hints are COMPUTED, never declared
162
+ *
163
+ * `mcpHintsForOperation` in `@webpieces/core-util` is the one place the mapping from
164
+ * `READ | WRITE_IDEMPOTENT | WRITE` to `readOnlyHint` / `destructiveHint` / `idempotentHint`
165
+ * lives, and this calls it rather than reimplementing it. The endpoint's `operation` already
166
+ * states whether repeating the call is safe; a hand-declared hint would be a second answer to a
167
+ * question the contract has answered, and the two could disagree. `openWorldHint` is the only one
168
+ * a human declares, because it is a judgement about the world rather than a consequence of the
169
+ * operation.
170
+ *
171
+ * ## There is no separate MCP description
172
+ *
173
+ * The agent reads the operation's `description` — the method's JSDoc — byte-identical to what the
174
+ * partner reads. `x-mcp-description` appears ONLY when the author wrote an explicit `@mcp` JSDoc
175
+ * tag, which is a deliberate act rather than a second copy of the same paragraph.
176
+ */
177
+ withMcp(operation, endpoint) {
178
+ if (!this.includeMcp || endpoint.mcpTool === undefined) {
179
+ return operation;
180
+ }
181
+ const hints = (0, core_util_1.mcpHintsForOperation)(this.operationOf(endpoint), endpoint.openWorld);
182
+ return operation
183
+ .set('x-mcp-tool', endpoint.mcpTool.name)
184
+ .set('x-mcp-hints', new JsonObject_1.JsonObject()
185
+ .set('readOnlyHint', hints.readOnlyHint)
186
+ .set('destructiveHint', hints.destructiveHint)
187
+ .set('idempotentHint', hints.idempotentHint)
188
+ .set('openWorldHint', hints.openWorldHint))
189
+ .set('x-mcp-description', this.prose(endpoint.mcpDescription))
190
+ .set('x-mcp-auth', endpoint.mcpAuthText);
191
+ }
192
+ /**
193
+ * The endpoint's operation as the REAL exported constant.
194
+ *
195
+ * The model carries it as a string, verbatim from the source, because the model invents no
196
+ * taxonomy. Matching it back to the constant here — rather than casting — is what makes a rename
197
+ * of `WRITE_IDEMPOTENT` in `core-util` a compile error in this file.
198
+ */
199
+ operationOf(endpoint) {
200
+ if (endpoint.operation === core_util_1.READ) {
201
+ return core_util_1.READ;
202
+ }
203
+ return endpoint.operation === core_util_1.WRITE_IDEMPOTENT ? core_util_1.WRITE_IDEMPOTENT : core_util_1.WRITE;
204
+ }
205
+ /**
206
+ * The request DTO as a body. Only for `POST`: OpenAPI gives a `GET` body no defined semantics and
207
+ * most tooling drops it, so publishing one would document a request no generated client sends.
208
+ */
209
+ requestBody(endpoint, contractName) {
210
+ if (endpoint.httpMethod !== 'POST' || endpoint.request === undefined) {
211
+ return undefined;
212
+ }
213
+ const pointer = `#/paths/${contractName}_${endpoint.methodName}/requestBody`;
214
+ const media = endpoint.options.formPost
215
+ ? 'application/x-www-form-urlencoded'
216
+ : 'application/json';
217
+ return new JsonObject_1.JsonObject()
218
+ .set('required', true)
219
+ .set('content', new JsonObject_1.JsonObject().set(media, new JsonObject_1.JsonObject().set('schema', this.schemas.type(endpoint.request, pointer))));
220
+ }
221
+ responses(endpoint, contractName, contract) {
222
+ const responses = new JsonObject_1.JsonObject();
223
+ responses.set('200', this.success(endpoint, contractName, contract));
224
+ for (const status of Array.from(contract.errors.keys())) {
225
+ responses.set(status, this.failure(contract.errors.get(status), contract));
226
+ }
227
+ return responses;
228
+ }
229
+ success(endpoint, contractName, contract) {
230
+ const response = new JsonObject_1.JsonObject()
231
+ .set('description', this.isVoid(endpoint.response) ? ACKNOWLEDGE : 'Success.')
232
+ .set('headers', contract.headers);
233
+ if (this.isVoid(endpoint.response)) {
234
+ return response;
235
+ }
236
+ const pointer = `#/paths/${contractName}_${endpoint.methodName}/responses/200`;
237
+ return response.set('content', new JsonObject_1.JsonObject().set('application/json', new JsonObject_1.JsonObject().set('schema', this.schemas.type(endpoint.response, pointer))));
238
+ }
239
+ failure(description, contract) {
240
+ const response = new JsonObject_1.JsonObject().set('description', description);
241
+ if (contract.errorSchemaRef === undefined) {
242
+ return response;
243
+ }
244
+ return response.set('content', new JsonObject_1.JsonObject().set('application/json', new JsonObject_1.JsonObject().set('schema', contract.errorSchemaRef)));
245
+ }
246
+ /** `Promise<void>` reaches the model as the `unknown` primitive — there is no body to document. */
247
+ isVoid(response) {
248
+ return (response === undefined ||
249
+ (response.kind === 'primitive' && response.primitive === 'unknown'));
250
+ }
251
+ prose(text) {
252
+ return text === undefined || text.trim() === '' ? undefined : text;
253
+ }
254
+ }
255
+ exports.OperationRenderer = OperationRenderer;
256
+ //# sourceMappingURL=OperationRenderer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"OperationRenderer.js","sourceRoot":"","sources":["../../../../../../packages/docs/openapi-generator/src/generate/OperationRenderer.ts"],"names":[],"mappings":"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","sourcesContent":["import { DocumentedEndpoint, TypeRef } from '@webpieces/api-doc-model';\nimport {\n CLOUDTASKS,\n CRON,\n EXTERNAL,\n mcpHintsForOperation,\n POST,\n READ,\n RPC,\n WpAuthPublic,\n WRITE,\n WRITE_IDEMPOTENT,\n} from '@webpieces/core-util';\nimport { JsonObject, JsonValue } from '../json/JsonObject';\nimport { SchemaRenderer } from './SchemaRenderer';\n\n/** What a webhook's `void` return means on the wire, said once. */\nconst ACKNOWLEDGE = 'Return any 2xx to acknowledge delivery. The body is ignored.';\n\n/** `Promise<void>` on the wire, said once. */\n\n/** The response document, and the responses a document-wide failure contract adds to it. */\nexport class ResponseContract {\n constructor(\n /** Status -> description, from the manifest's `errors.responses`, in declared order. */\n readonly errors: ReadonlyMap<string, string>,\n /** `$ref` at the error body schema, when the manifest declared one. */\n readonly errorSchemaRef: JsonObject | undefined,\n /** Header name -> its `components.headers` `$ref`, on every success response. */\n readonly headers: JsonObject | undefined,\n ) {}\n}\n\n/**\n * ONE {@link DocumentedEndpoint} -> one OpenAPI Operation Object.\n *\n * ## `summary` is the operation NAME, never a sentence\n *\n * A docs theme titles the endpoint's page from `summary` and puts `description` in the body. Writing\n * the first sentence of the prose into `summary` therefore produces a sidebar of sentences and a page\n * whose heading repeats its own first line. The method name is what a reader is looking for, and it\n * is also what the generated client calls the method, so the two agree by construction.\n *\n * ## The MCP extensions ride HERE, on the operation\n *\n * `x-mcp-tool`, `x-mcp-hints`, `x-mcp-description` and `x-mcp-auth` are stamped on every operation\n * carrying the tool decorator, hidden or not: an internal endpoint that is an agent tool is a\n * legitimate agent tool, and requiring it to be published to partners in order to be one would be\n * exactly the wrong coupling.\n *\n * They are stamped on the BASE, which every published document is derived from, and\n * {@link DocumentDeriver} STRIPS them from the customer document. Stamping-then-stripping rather than\n * rendering the customer document without them is the same choice the whole deriver rests on: one\n * render of one operation, so two documents holding it cannot disagree about anything else in it.\n */\nexport class OperationRenderer {\n constructor(\n private readonly schemas: SchemaRenderer,\n /** True only for the MCP document — see {@link withMcp}. */\n private readonly includeMcp: boolean,\n /** True only for the private document — see {@link triggerSentence}. */\n private readonly includeTrigger: boolean,\n ) {}\n\n /** An ordinary served route. */\n operation(\n endpoint: DocumentedEndpoint,\n contractName: string,\n tag: string,\n contract: ResponseContract,\n security: readonly JsonValue[] | undefined,\n ): JsonObject {\n const operation = this.common(endpoint, contractName, tag, contract);\n // An endpoint that authenticates NOBODY says so explicitly: `security: []` is OpenAPI's\n // spelling of \"no credential required\", and leaving the key off instead means \"inherit the\n // document's\", which is a different statement that happens to coincide today.\n const explicit = this.isPublic(endpoint) ? [] : security;\n operation.set('security', explicit === undefined ? undefined : explicit.slice());\n return this.withMcp(operation, endpoint);\n }\n\n /**\n * A call WE make to a PARTNER's server.\n *\n * It carries `x-webpieces-webhook: true`, which is the one fact about it a document cannot state\n * any other way. It gets no trigger sentence and no security at all, because neither is true of\n * it: nothing of ours triggers it and nothing of ours authenticates it — our api key must never\n * be published as a guard on somebody else's endpoint.\n */\n webhook(\n endpoint: DocumentedEndpoint,\n contractName: string,\n tag: string,\n contract: ResponseContract,\n ): JsonObject {\n const operation = this.common(endpoint, contractName, tag, contract);\n return this.withMcp(operation.set('x-webpieces-webhook', true), endpoint);\n }\n\n private common(\n endpoint: DocumentedEndpoint,\n contractName: string,\n tag: string,\n contract: ResponseContract,\n ): JsonObject {\n return new JsonObject()\n .set('operationId', `${contractName}_${endpoint.methodName}`)\n .set('summary', endpoint.methodName)\n .set('description', this.describe(endpoint))\n .set('tags', [tag])\n .set('requestBody', this.requestBody(endpoint, contractName))\n .set('responses', this.responses(endpoint, contractName, contract));\n }\n\n /**\n * The JSDoc body, plus the sentences that are DERIVED rather than written.\n *\n * The retry sentence comes from the endpoint's declared `operation`, which every integration\n * wants to know and most APIs state nowhere. It is a SENTENCE and not a vendor extension because\n * Swagger UI and most themes do not render `x-` extensions and no standard generator reads\n * `x-webpieces-*`: an extension would be invisible to the human and unread by the machine. The\n * mapping it is derived from is published once, in `info.description`, so the rule is stated and\n * not only its consequences.\n */\n private describe(endpoint: DocumentedEndpoint): string | undefined {\n const parts: string[] = [];\n const body = this.prose(endpoint.description);\n if (body !== undefined) {\n parts.push(body);\n }\n parts.push(this.retrySentence(endpoint));\n if (this.includeTrigger) {\n parts.push(this.triggerSentence(endpoint));\n }\n if (endpoint.openWorld) {\n parts.push(\n 'Reaches beyond this service: it may act on an external system, so its effects ' +\n 'are not all visible here.',\n );\n }\n return parts.join('\\n\\n');\n }\n\n /**\n * WHAT FIRES this endpoint, in the private document only.\n *\n * A customer calls what they are given a url for; whether ours runs off a queue or a clock is our\n * business and would only invite a question they cannot act on. It is a SENTENCE and not\n * `x-webpieces-trigger` for the same reason the retry sentence is: Swagger UI and most themes do\n * not render `x-` extensions, and no standard generator reads `x-webpieces-*`, so an extension is\n * invisible to the human and unread by the machine.\n */\n private triggerSentence(endpoint: DocumentedEndpoint): string {\n if (endpoint.kind === CLOUDTASKS) {\n return 'Triggered by a queued task, delivered asynchronously.';\n }\n if (endpoint.kind === CRON) {\n return 'Triggered on a schedule, not by a caller.';\n }\n if (endpoint.kind === EXTERNAL) {\n const caller = endpoint.options.calledBy;\n return caller === undefined\n ? 'Triggered by an outside system.'\n : `Triggered by an outside system: ${caller}.`;\n }\n return endpoint.kind === RPC\n ? 'Triggered by a direct call.'\n : `Triggered by: ${endpoint.kind}.`;\n }\n\n /** An endpoint whose declared credential is \"none\", said out loud rather than left absent. */\n private isPublic(endpoint: DocumentedEndpoint): boolean {\n return endpoint.auth?.decorator === WpAuthPublic.name;\n }\n\n private retrySentence(endpoint: DocumentedEndpoint): string {\n if (endpoint.operation === READ) {\n return 'Safe to retry: this operation is read-only and idempotent.';\n }\n if (endpoint.operation === WRITE_IDEMPOTENT) {\n return 'Safe to retry: this operation changes state but is idempotent.';\n }\n return 'NOT safe to retry: this operation changes state and is not idempotent.';\n }\n\n /**\n * The MCP extensions, stamped only on the document that has an agent for a reader.\n *\n * ## The three side-effect hints are COMPUTED, never declared\n *\n * `mcpHintsForOperation` in `@webpieces/core-util` is the one place the mapping from\n * `READ | WRITE_IDEMPOTENT | WRITE` to `readOnlyHint` / `destructiveHint` / `idempotentHint`\n * lives, and this calls it rather than reimplementing it. The endpoint's `operation` already\n * states whether repeating the call is safe; a hand-declared hint would be a second answer to a\n * question the contract has answered, and the two could disagree. `openWorldHint` is the only one\n * a human declares, because it is a judgement about the world rather than a consequence of the\n * operation.\n *\n * ## There is no separate MCP description\n *\n * The agent reads the operation's `description` — the method's JSDoc — byte-identical to what the\n * partner reads. `x-mcp-description` appears ONLY when the author wrote an explicit `@mcp` JSDoc\n * tag, which is a deliberate act rather than a second copy of the same paragraph.\n */\n private withMcp(operation: JsonObject, endpoint: DocumentedEndpoint): JsonObject {\n if (!this.includeMcp || endpoint.mcpTool === undefined) {\n return operation;\n }\n const hints = mcpHintsForOperation(this.operationOf(endpoint), endpoint.openWorld);\n return operation\n .set('x-mcp-tool', endpoint.mcpTool.name)\n .set(\n 'x-mcp-hints',\n new JsonObject()\n .set('readOnlyHint', hints.readOnlyHint)\n .set('destructiveHint', hints.destructiveHint)\n .set('idempotentHint', hints.idempotentHint)\n .set('openWorldHint', hints.openWorldHint),\n )\n .set('x-mcp-description', this.prose(endpoint.mcpDescription))\n .set('x-mcp-auth', endpoint.mcpAuthText);\n }\n\n /**\n * The endpoint's operation as the REAL exported constant.\n *\n * The model carries it as a string, verbatim from the source, because the model invents no\n * taxonomy. Matching it back to the constant here — rather than casting — is what makes a rename\n * of `WRITE_IDEMPOTENT` in `core-util` a compile error in this file.\n */\n private operationOf(\n endpoint: DocumentedEndpoint,\n ): typeof READ | typeof WRITE_IDEMPOTENT | typeof WRITE {\n if (endpoint.operation === READ) {\n return READ;\n }\n return endpoint.operation === WRITE_IDEMPOTENT ? WRITE_IDEMPOTENT : WRITE;\n }\n\n /**\n * The request DTO as a body. Only for `POST`: OpenAPI gives a `GET` body no defined semantics and\n * most tooling drops it, so publishing one would document a request no generated client sends.\n */\n private requestBody(\n endpoint: DocumentedEndpoint,\n contractName: string,\n ): JsonObject | undefined {\n if (endpoint.httpMethod !== 'POST' || endpoint.request === undefined) {\n return undefined;\n }\n const pointer = `#/paths/${contractName}_${endpoint.methodName}/requestBody`;\n const media = endpoint.options.formPost\n ? 'application/x-www-form-urlencoded'\n : 'application/json';\n return new JsonObject()\n .set('required', true)\n .set(\n 'content',\n new JsonObject().set(\n media,\n new JsonObject().set('schema', this.schemas.type(endpoint.request, pointer)),\n ),\n );\n }\n\n private responses(\n endpoint: DocumentedEndpoint,\n contractName: string,\n contract: ResponseContract,\n ): JsonObject {\n const responses = new JsonObject();\n responses.set('200', this.success(endpoint, contractName, contract));\n for (const status of Array.from(contract.errors.keys())) {\n responses.set(status, this.failure(contract.errors.get(status)!, contract));\n }\n return responses;\n }\n\n private success(\n endpoint: DocumentedEndpoint,\n contractName: string,\n contract: ResponseContract,\n ): JsonObject {\n const response = new JsonObject()\n .set('description', this.isVoid(endpoint.response) ? ACKNOWLEDGE : 'Success.')\n .set('headers', contract.headers);\n if (this.isVoid(endpoint.response)) {\n return response;\n }\n const pointer = `#/paths/${contractName}_${endpoint.methodName}/responses/200`;\n return response.set(\n 'content',\n new JsonObject().set(\n 'application/json',\n new JsonObject().set('schema', this.schemas.type(endpoint.response!, pointer)),\n ),\n );\n }\n\n private failure(description: string, contract: ResponseContract): JsonObject {\n const response = new JsonObject().set('description', description);\n if (contract.errorSchemaRef === undefined) {\n return response;\n }\n return response.set(\n 'content',\n new JsonObject().set(\n 'application/json',\n new JsonObject().set('schema', contract.errorSchemaRef),\n ),\n );\n }\n\n /** `Promise<void>` reaches the model as the `unknown` primitive — there is no body to document. */\n private isVoid(response: TypeRef | undefined): boolean {\n return (\n response === undefined ||\n (response.kind === 'primitive' && response.primitive === 'unknown')\n );\n }\n\n private prose(text: string | undefined): string | undefined {\n return text === undefined || text.trim() === '' ? undefined : text;\n }\n}\n"]}
@@ -0,0 +1,89 @@
1
+ import { DocumentedField, DocumentedType, TypeRef } from '@webpieces/api-doc-model';
2
+ import { JsonObject } from '../json/JsonObject';
3
+ /** ONE field the renderer could not give a shape to, and WHERE in the document it would have sat. */
4
+ export declare class UnmappedField {
5
+ /** A JSON pointer into the document that would have been written. */
6
+ readonly pointer: string;
7
+ /** The verbatim TypeScript type text, so the cure names the thing to go and type. */
8
+ readonly typeText: string;
9
+ constructor(
10
+ /** A JSON pointer into the document that would have been written. */
11
+ pointer: string,
12
+ /** The verbatim TypeScript type text, so the cure names the thing to go and type. */
13
+ typeText: string);
14
+ }
15
+ /**
16
+ * {@link TypeRef} / {@link DocumentedType} -> JSON Schema 2020-12, which IS OpenAPI 3.1's schema
17
+ * dialect and IS what MCP `tools/list` speaks. That identity is the reason #982 specifies 3.1 rather
18
+ * than 3.0: the two are not "similar dialects", they are the same schemas, so the MCP projection
19
+ * (#984) is a selection and an inline pass, not a translation.
20
+ *
21
+ * ## Three things 3.1 lets this renderer state HONESTLY that 3.0 could not
22
+ *
23
+ * - `type: [T, "null"]` for a nullable field, instead of 3.0's `nullable: true` keyword, which is not
24
+ * JSON Schema at all.
25
+ * - `description` BESIDE a `$ref`. In 3.0 a sibling of `$ref` is ignored, so a documented field whose
26
+ * type is a named DTO simply lost its prose.
27
+ * - OPTIONAL and NULLABLE as separate facts — absent from `required` versus `"null"` in `type`. `{}`
28
+ * and `{x: null}` are different wire documents and a schema that conflates them rejects one.
29
+ *
30
+ * ## It records what it cannot map rather than emitting an untyped field
31
+ *
32
+ * An unmapped type yields an EMPTY schema, which in JSON Schema means "anything". That is precisely
33
+ * the green-build-publishes-a-shapeless-field defect the guard exists to stop, so every one is
34
+ * recorded with the pointer it would have occupied and {@link OpenApiGenerator} refuses on the set.
35
+ */
36
+ export declare class SchemaRenderer {
37
+ private readonly types;
38
+ private readonly reached;
39
+ private readonly unmappedFields;
40
+ constructor(types: ReadonlyMap<string, DocumentedType>);
41
+ /** Named types this renderer has been asked for, directly or through another schema. */
42
+ reachedTypes(): ReadonlySet<string>;
43
+ /** Every field with no shape, with its pointer. Empty means the document is fully typed. */
44
+ unmapped(): readonly UnmappedField[];
45
+ /**
46
+ * `components.schemas` for everything reachable from whatever has been rendered so far, expanded
47
+ * to a FIXPOINT.
48
+ *
49
+ * A worklist and not recursion-with-a-guard because rendering a type reaches more types, and the
50
+ * set has to close over that. It terminates for the same reason the extractor's walk does: a
51
+ * named type is registered once, so the worklist strictly shrinks.
52
+ */
53
+ components(): JsonObject;
54
+ /** One named type: an object DTO, a string enum, or a union with its DERIVED discriminator. */
55
+ private namedType;
56
+ /**
57
+ * A union, with a `discriminator` ONLY when the model derived one — which it does only when every
58
+ * branch carries the same property typed as a single string literal. An invented discriminator
59
+ * would claim a narrowing TypeScript itself cannot do.
60
+ */
61
+ private union;
62
+ /**
63
+ * One FIELD: its type, plus the prose and the constraints that hang off the field rather than the
64
+ * type. `@format`, `@WpMin` and `@WpMax` land on the SCALAR — on an array, on the ITEM — because
65
+ * `minimum` on an array means nothing and a reader would have to guess which half was meant.
66
+ */
67
+ field(field: DocumentedField, pointer: string): JsonObject;
68
+ /** `description`, and the `@mcp` override as `x-mcp-description` when the author wrote one. */
69
+ private described;
70
+ private constrain;
71
+ /** `type: [T, "null"]` where there is a type to widen; `anyOf` where the shape is a `$ref`. */
72
+ private nullable;
73
+ /** One resolved type, with no field-level prose or constraints on it. */
74
+ type(ref: TypeRef, pointer: string): JsonObject;
75
+ /**
76
+ * A union, as a `$ref` at the NAMED alias when the model registered one.
77
+ *
78
+ * The model records a `type X = A | B` alias as its own entry — carrying the DERIVED
79
+ * discriminator — while the FIELD that used it holds a bare union of branch names. Rendering the
80
+ * field's union inline would therefore publish the `oneOf` and silently drop the discriminator,
81
+ * which is the one part of a union a client actually needs to narrow on. So the alias is looked
82
+ * up by its branch list and referenced.
83
+ */
84
+ private namedUnion;
85
+ private primitive;
86
+ private reference;
87
+ /** Empty prose is ABSENT prose. An empty `description` key is noise in every rendered page. */
88
+ private prose;
89
+ }
@@ -0,0 +1,236 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SchemaRenderer = exports.UnmappedField = void 0;
4
+ const JsonObject_1 = require("../json/JsonObject");
5
+ /** ONE field the renderer could not give a shape to, and WHERE in the document it would have sat. */
6
+ class UnmappedField {
7
+ pointer;
8
+ typeText;
9
+ constructor(
10
+ /** A JSON pointer into the document that would have been written. */
11
+ pointer,
12
+ /** The verbatim TypeScript type text, so the cure names the thing to go and type. */
13
+ typeText) {
14
+ this.pointer = pointer;
15
+ this.typeText = typeText;
16
+ }
17
+ }
18
+ exports.UnmappedField = UnmappedField;
19
+ /**
20
+ * {@link TypeRef} / {@link DocumentedType} -> JSON Schema 2020-12, which IS OpenAPI 3.1's schema
21
+ * dialect and IS what MCP `tools/list` speaks. That identity is the reason #982 specifies 3.1 rather
22
+ * than 3.0: the two are not "similar dialects", they are the same schemas, so the MCP projection
23
+ * (#984) is a selection and an inline pass, not a translation.
24
+ *
25
+ * ## Three things 3.1 lets this renderer state HONESTLY that 3.0 could not
26
+ *
27
+ * - `type: [T, "null"]` for a nullable field, instead of 3.0's `nullable: true` keyword, which is not
28
+ * JSON Schema at all.
29
+ * - `description` BESIDE a `$ref`. In 3.0 a sibling of `$ref` is ignored, so a documented field whose
30
+ * type is a named DTO simply lost its prose.
31
+ * - OPTIONAL and NULLABLE as separate facts — absent from `required` versus `"null"` in `type`. `{}`
32
+ * and `{x: null}` are different wire documents and a schema that conflates them rejects one.
33
+ *
34
+ * ## It records what it cannot map rather than emitting an untyped field
35
+ *
36
+ * An unmapped type yields an EMPTY schema, which in JSON Schema means "anything". That is precisely
37
+ * the green-build-publishes-a-shapeless-field defect the guard exists to stop, so every one is
38
+ * recorded with the pointer it would have occupied and {@link OpenApiGenerator} refuses on the set.
39
+ */
40
+ class SchemaRenderer {
41
+ types;
42
+ reached = new Set();
43
+ unmappedFields = [];
44
+ constructor(types) {
45
+ this.types = types;
46
+ }
47
+ /** Named types this renderer has been asked for, directly or through another schema. */
48
+ reachedTypes() {
49
+ return this.reached;
50
+ }
51
+ /** Every field with no shape, with its pointer. Empty means the document is fully typed. */
52
+ unmapped() {
53
+ return this.unmappedFields;
54
+ }
55
+ /**
56
+ * `components.schemas` for everything reachable from whatever has been rendered so far, expanded
57
+ * to a FIXPOINT.
58
+ *
59
+ * A worklist and not recursion-with-a-guard because rendering a type reaches more types, and the
60
+ * set has to close over that. It terminates for the same reason the extractor's walk does: a
61
+ * named type is registered once, so the worklist strictly shrinks.
62
+ */
63
+ components() {
64
+ const rendered = new Map();
65
+ let grew = true;
66
+ while (grew) {
67
+ grew = false;
68
+ for (const name of Array.from(this.reached)) {
69
+ if (rendered.has(name)) {
70
+ continue;
71
+ }
72
+ const type = this.types.get(name);
73
+ // Rendering it can add MORE names to `reached`, which is what the outer loop is for.
74
+ rendered.set(name, type === undefined ? undefined : this.namedType(type));
75
+ grew = true;
76
+ }
77
+ }
78
+ const schemas = new JsonObject_1.JsonObject();
79
+ // Alphabetical, because `components.schemas` is a lookup table and nothing reads it in order
80
+ // — whereas discovery order would move every time an operation was added above another.
81
+ for (const name of Array.from(rendered.keys()).sort()) {
82
+ schemas.set(name, rendered.get(name));
83
+ }
84
+ return schemas;
85
+ }
86
+ /** One named type: an object DTO, a string enum, or a union with its DERIVED discriminator. */
87
+ namedType(type) {
88
+ const pointer = `#/components/schemas/${type.name}`;
89
+ if (type.enumValues.length > 0) {
90
+ return new JsonObject_1.JsonObject()
91
+ .set('type', 'string')
92
+ .set('description', this.prose(type.description))
93
+ .set('enum', type.enumValues.slice());
94
+ }
95
+ if (type.unionRefNames.length > 0) {
96
+ return this.union(type);
97
+ }
98
+ const properties = new JsonObject_1.JsonObject();
99
+ const required = [];
100
+ for (const field of type.fields) {
101
+ properties.set(field.name, this.field(field, `${pointer}/properties/${field.name}`));
102
+ if (!field.optional) {
103
+ required.push(field.name);
104
+ }
105
+ }
106
+ return new JsonObject_1.JsonObject()
107
+ .set('type', 'object')
108
+ .set('description', this.prose(type.description))
109
+ .set('properties', properties.isEmpty() ? undefined : properties)
110
+ .set('required', required.length === 0 ? undefined : required)
111
+ .set('additionalProperties', type.indexSignatureValue === undefined
112
+ ? undefined
113
+ : this.type(type.indexSignatureValue, `${pointer}/additionalProperties`));
114
+ }
115
+ /**
116
+ * A union, with a `discriminator` ONLY when the model derived one — which it does only when every
117
+ * branch carries the same property typed as a single string literal. An invented discriminator
118
+ * would claim a narrowing TypeScript itself cannot do.
119
+ */
120
+ union(type) {
121
+ const branches = type.unionRefNames.map((name) => this.reference(name));
122
+ const schema = new JsonObject_1.JsonObject()
123
+ .set('description', this.prose(type.description))
124
+ .set('oneOf', branches);
125
+ if (type.discriminator === undefined) {
126
+ return schema;
127
+ }
128
+ const mapping = new JsonObject_1.JsonObject();
129
+ for (const branch of type.unionRefNames) {
130
+ const value = type.discriminator.branchValues.get(branch);
131
+ if (value !== undefined) {
132
+ mapping.set(value, `#/components/schemas/${branch}`);
133
+ }
134
+ }
135
+ return schema.set('discriminator', new JsonObject_1.JsonObject()
136
+ .set('propertyName', type.discriminator.propertyName)
137
+ .set('mapping', mapping));
138
+ }
139
+ /**
140
+ * One FIELD: its type, plus the prose and the constraints that hang off the field rather than the
141
+ * type. `@format`, `@WpMin` and `@WpMax` land on the SCALAR — on an array, on the ITEM — because
142
+ * `minimum` on an array means nothing and a reader would have to guess which half was meant.
143
+ */
144
+ field(field, pointer) {
145
+ const isArray = field.type.kind === 'array';
146
+ const leafPointer = isArray ? `${pointer}/items` : pointer;
147
+ const leaf = this.constrain(isArray ? this.type(field.type.items, leafPointer) : this.type(field.type, pointer), field);
148
+ const schema = isArray
149
+ ? new JsonObject_1.JsonObject().set('type', 'array').set('items', leaf)
150
+ : this.nullable(leaf, field.nullable);
151
+ if (isArray && field.nullable) {
152
+ return this.described(this.nullable(schema, true), field);
153
+ }
154
+ return this.described(schema, field);
155
+ }
156
+ /** `description`, and the `@mcp` override as `x-mcp-description` when the author wrote one. */
157
+ described(schema, field) {
158
+ return schema
159
+ .set('description', this.prose(field.description))
160
+ .set('x-mcp-description', this.prose(field.mcpDescription));
161
+ }
162
+ constrain(leaf, field) {
163
+ return leaf.set('format', field.format).set('minimum', field.min).set('maximum', field.max);
164
+ }
165
+ /** `type: [T, "null"]` where there is a type to widen; `anyOf` where the shape is a `$ref`. */
166
+ nullable(schema, isNullable) {
167
+ if (!isNullable) {
168
+ return schema;
169
+ }
170
+ const declared = schema.get('type');
171
+ if (typeof declared === 'string') {
172
+ return schema.set('type', [declared, 'null']);
173
+ }
174
+ return new JsonObject_1.JsonObject().set('anyOf', [schema, new JsonObject_1.JsonObject().set('type', 'null')]);
175
+ }
176
+ /** One resolved type, with no field-level prose or constraints on it. */
177
+ type(ref, pointer) {
178
+ switch (ref.kind) {
179
+ case 'primitive':
180
+ return this.primitive(ref);
181
+ case 'ref':
182
+ return this.reference(ref.refName);
183
+ case 'array':
184
+ return new JsonObject_1.JsonObject()
185
+ .set('type', 'array')
186
+ .set('items', this.type(ref.items, `${pointer}/items`));
187
+ case 'openMap':
188
+ return new JsonObject_1.JsonObject()
189
+ .set('type', 'object')
190
+ .set('additionalProperties', this.type(ref.values, `${pointer}/additionalProperties`));
191
+ case 'enum':
192
+ return new JsonObject_1.JsonObject().set('type', 'string').set('enum', ref.enumValues.slice());
193
+ case 'union':
194
+ return this.namedUnion(ref);
195
+ default:
196
+ this.unmappedFields.push(new UnmappedField(pointer, ref.unmappedText ?? '<unknown>'));
197
+ return new JsonObject_1.JsonObject();
198
+ }
199
+ }
200
+ /**
201
+ * A union, as a `$ref` at the NAMED alias when the model registered one.
202
+ *
203
+ * The model records a `type X = A | B` alias as its own entry — carrying the DERIVED
204
+ * discriminator — while the FIELD that used it holds a bare union of branch names. Rendering the
205
+ * field's union inline would therefore publish the `oneOf` and silently drop the discriminator,
206
+ * which is the one part of a union a client actually needs to narrow on. So the alias is looked
207
+ * up by its branch list and referenced.
208
+ */
209
+ namedUnion(ref) {
210
+ const branches = ref.unionRefNames.join(',');
211
+ for (const name of Array.from(this.types.keys())) {
212
+ if (this.types.get(name).unionRefNames.join(',') === branches) {
213
+ return this.reference(name);
214
+ }
215
+ }
216
+ return new JsonObject_1.JsonObject().set('oneOf', ref.unionRefNames.map((each) => this.reference(each)));
217
+ }
218
+ primitive(ref) {
219
+ if (ref.primitive === 'unknown') {
220
+ // No `type` at all: JSON Schema's honest spelling of "any shape". It is only ever reached
221
+ // by a `void` return, which is why it is not the unmapped refusal.
222
+ return new JsonObject_1.JsonObject();
223
+ }
224
+ return new JsonObject_1.JsonObject().set('type', ref.integer ? 'integer' : ref.primitive);
225
+ }
226
+ reference(name) {
227
+ this.reached.add(name);
228
+ return new JsonObject_1.JsonObject().set('$ref', `#/components/schemas/${name}`);
229
+ }
230
+ /** Empty prose is ABSENT prose. An empty `description` key is noise in every rendered page. */
231
+ prose(text) {
232
+ return text === undefined || text.trim() === '' ? undefined : text;
233
+ }
234
+ }
235
+ exports.SchemaRenderer = SchemaRenderer;
236
+ //# sourceMappingURL=SchemaRenderer.js.map