@zudojs/openapi 0.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +340 -21
  3. package/dist/index.d.ts +26 -21
  4. package/dist/index.js +31 -13
  5. package/dist/openApiComponents/index.d.ts +2 -2
  6. package/dist/openApiComponents/index.js +1 -1
  7. package/dist/openApiConstants/index.d.ts +1 -1
  8. package/dist/openApiConstants/index.js +1 -1
  9. package/dist/openApiConstants/openApiConstants.core.d.ts +15 -3
  10. package/dist/openApiConstants/openApiConstants.core.js +22 -3
  11. package/dist/openApiDocument/index.d.ts +1 -1
  12. package/dist/openApiDocument/index.js +1 -1
  13. package/dist/openApiDocument/openApiDocument.builder.d.ts +39 -52
  14. package/dist/openApiDocument/openApiDocument.builder.js +73 -72
  15. package/dist/openApiErrors/index.d.ts +2 -2
  16. package/dist/openApiErrors/index.js +2 -1
  17. package/dist/openApiErrors/openApiError.base.d.ts +9 -0
  18. package/dist/openApiErrors/openApiError.base.js +7 -0
  19. package/dist/openApiErrors/openApiError.types.d.ts +34 -14
  20. package/dist/openApiErrors/openApiError.types.js +83 -35
  21. package/dist/openApiHttp/index.d.ts +2 -2
  22. package/dist/openApiHttp/index.js +2 -2
  23. package/dist/openApiHttp/openApiHttpAdapter.core.d.ts +129 -25
  24. package/dist/openApiHttp/openApiHttpAdapter.core.js +226 -16
  25. package/dist/openApiRegistry/openApiRegistry.core.d.ts +47 -5
  26. package/dist/openApiRegistry/openApiRegistry.core.js +180 -66
  27. package/dist/openApiRegistry/openApiRegistry.type.d.ts +18 -13
  28. package/dist/openApiRegistry/openApiRegistry.type.js +1 -2
  29. package/dist/openApiRouting/index.d.ts +2 -2
  30. package/dist/openApiRouting/index.js +1 -1
  31. package/dist/openApiRouting/routeConverter.core.d.ts +25 -7
  32. package/dist/openApiRouting/routeConverter.core.js +104 -58
  33. package/dist/openApiRouting/routeMetadata.type.d.ts +40 -17
  34. package/dist/openApiRouting/routeMetadata.type.js +9 -0
  35. package/dist/openApiRouting/routeScanner.core.d.ts +17 -47
  36. package/dist/openApiRouting/routeScanner.core.js +40 -55
  37. package/dist/openApiSchema/index.d.ts +4 -4
  38. package/dist/openApiSchema/index.js +2 -2
  39. package/dist/openApiSchema/references.core.d.ts +18 -1
  40. package/dist/openApiSchema/references.core.js +21 -1
  41. package/dist/openApiSchema/schemaConverter.core.d.ts +61 -3
  42. package/dist/openApiSchema/schemaConverter.core.js +523 -117
  43. package/dist/openApiSchema/schemaRegistry.core.d.ts +27 -10
  44. package/dist/openApiSchema/schemaRegistry.core.js +33 -14
  45. package/dist/openApiSerialization/openApiSerializer.core.d.ts +10 -4
  46. package/dist/openApiSerialization/openApiSerializer.core.js +158 -9
  47. package/dist/openApiTypes/index.d.ts +1 -1
  48. package/dist/openApiTypes/openApiDocument.type.d.ts +16 -0
  49. package/dist/openApiTypes/openApiSchema.type.d.ts +24 -1
  50. package/dist/openApiTypes/openApiSchema.type.js +3 -0
  51. package/dist/openApiUi/index.d.ts +8 -0
  52. package/dist/openApiUi/index.js +8 -0
  53. package/dist/openApiUi/openApiUi.brand.d.ts +26 -0
  54. package/dist/openApiUi/openApiUi.brand.js +44 -0
  55. package/dist/openApiUi/openApiUi.core.d.ts +47 -0
  56. package/dist/openApiUi/openApiUi.core.js +157 -0
  57. package/dist/openApiValidation/index.d.ts +2 -2
  58. package/dist/openApiValidation/index.js +1 -1
  59. package/dist/openApiValidation/openApiValidator.core.d.ts +33 -11
  60. package/dist/openApiValidation/openApiValidator.core.js +295 -66
  61. package/package.json +29 -18
  62. package/dist/.tsbuildinfo +0 -1
  63. package/dist/index.d.ts.map +0 -1
  64. package/dist/index.js.map +0 -1
  65. package/dist/openApiComponents/index.d.ts.map +0 -1
  66. package/dist/openApiComponents/index.js.map +0 -1
  67. package/dist/openApiConstants/index.d.ts.map +0 -1
  68. package/dist/openApiConstants/index.js.map +0 -1
  69. package/dist/openApiConstants/openApiConstants.core.d.ts.map +0 -1
  70. package/dist/openApiConstants/openApiConstants.core.js.map +0 -1
  71. package/dist/openApiDocument/index.d.ts.map +0 -1
  72. package/dist/openApiDocument/index.js.map +0 -1
  73. package/dist/openApiDocument/openApiDocument.builder.d.ts.map +0 -1
  74. package/dist/openApiDocument/openApiDocument.builder.js.map +0 -1
  75. package/dist/openApiErrors/index.d.ts.map +0 -1
  76. package/dist/openApiErrors/index.js.map +0 -1
  77. package/dist/openApiErrors/openApiError.base.d.ts.map +0 -1
  78. package/dist/openApiErrors/openApiError.base.js.map +0 -1
  79. package/dist/openApiErrors/openApiError.core.d.ts.map +0 -1
  80. package/dist/openApiErrors/openApiError.core.js.map +0 -1
  81. package/dist/openApiErrors/openApiError.types.d.ts.map +0 -1
  82. package/dist/openApiErrors/openApiError.types.js.map +0 -1
  83. package/dist/openApiHttp/index.d.ts.map +0 -1
  84. package/dist/openApiHttp/index.js.map +0 -1
  85. package/dist/openApiHttp/openApiHttpAdapter.core.d.ts.map +0 -1
  86. package/dist/openApiHttp/openApiHttpAdapter.core.js.map +0 -1
  87. package/dist/openApiRegistry/index.d.ts.map +0 -1
  88. package/dist/openApiRegistry/index.js.map +0 -1
  89. package/dist/openApiRegistry/openApiRegistry.core.d.ts.map +0 -1
  90. package/dist/openApiRegistry/openApiRegistry.core.js.map +0 -1
  91. package/dist/openApiRegistry/openApiRegistry.type.d.ts.map +0 -1
  92. package/dist/openApiRegistry/openApiRegistry.type.js.map +0 -1
  93. package/dist/openApiRouting/index.d.ts.map +0 -1
  94. package/dist/openApiRouting/index.js.map +0 -1
  95. package/dist/openApiRouting/routeConverter.core.d.ts.map +0 -1
  96. package/dist/openApiRouting/routeConverter.core.js.map +0 -1
  97. package/dist/openApiRouting/routeMetadata.type.d.ts.map +0 -1
  98. package/dist/openApiRouting/routeMetadata.type.js.map +0 -1
  99. package/dist/openApiRouting/routeScanner.core.d.ts.map +0 -1
  100. package/dist/openApiRouting/routeScanner.core.js.map +0 -1
  101. package/dist/openApiSchema/index.d.ts.map +0 -1
  102. package/dist/openApiSchema/index.js.map +0 -1
  103. package/dist/openApiSchema/references.core.d.ts.map +0 -1
  104. package/dist/openApiSchema/references.core.js.map +0 -1
  105. package/dist/openApiSchema/schemaConverter.core.d.ts.map +0 -1
  106. package/dist/openApiSchema/schemaConverter.core.js.map +0 -1
  107. package/dist/openApiSchema/schemaRegistry.core.d.ts.map +0 -1
  108. package/dist/openApiSchema/schemaRegistry.core.js.map +0 -1
  109. package/dist/openApiSerialization/index.d.ts.map +0 -1
  110. package/dist/openApiSerialization/index.js.map +0 -1
  111. package/dist/openApiSerialization/openApiSerializer.core.d.ts.map +0 -1
  112. package/dist/openApiSerialization/openApiSerializer.core.js.map +0 -1
  113. package/dist/openApiTypes/index.d.ts.map +0 -1
  114. package/dist/openApiTypes/index.js.map +0 -1
  115. package/dist/openApiTypes/openApiComponent.type.d.ts.map +0 -1
  116. package/dist/openApiTypes/openApiComponent.type.js.map +0 -1
  117. package/dist/openApiTypes/openApiDocument.type.d.ts.map +0 -1
  118. package/dist/openApiTypes/openApiDocument.type.js.map +0 -1
  119. package/dist/openApiTypes/openApiPath.type.d.ts.map +0 -1
  120. package/dist/openApiTypes/openApiPath.type.js.map +0 -1
  121. package/dist/openApiTypes/openApiSchema.type.d.ts.map +0 -1
  122. package/dist/openApiTypes/openApiSchema.type.js.map +0 -1
  123. package/dist/openApiTypes/openApiSecurity.type.d.ts.map +0 -1
  124. package/dist/openApiTypes/openApiSecurity.type.js.map +0 -1
  125. package/dist/openApiTypes/openApiTypes.core.d.ts.map +0 -1
  126. package/dist/openApiTypes/openApiTypes.core.js.map +0 -1
  127. package/dist/openApiValidation/index.d.ts.map +0 -1
  128. package/dist/openApiValidation/index.js.map +0 -1
  129. package/dist/openApiValidation/openApiValidator.core.d.ts.map +0 -1
  130. package/dist/openApiValidation/openApiValidator.core.js.map +0 -1
@@ -1,61 +1,271 @@
1
- import { OpenAPIDocumentBuilder } from "../openApiDocument/openApiDocument.builder.js";
2
1
  import { OpenAPIRegistryImpl } from "../openApiRegistry/openApiRegistry.core.js";
3
2
  import { OpenAPIRouteScannerImpl } from "../openApiRouting/routeScanner.core.js";
4
- import { OpenAPIValidatorImpl } from "../openApiValidation/openApiValidator.core.js";
3
+ import { OpenAPIValidatorImpl, } from "../openApiValidation/openApiValidator.core.js";
4
+ import { SchemaRegistryImpl } from "../openApiSchema/schemaRegistry.core.js";
5
5
  import { toOpenAPIJSON, toOpenAPIYAML, } from "../openApiSerialization/openApiSerializer.core.js";
6
+ import { DEFAULT_MEDIA_TYPE, DEFAULT_OPENAPI_VERSION, DOCUMENT_CACHE_TTL_MS, } from "../openApiConstants/openApiConstants.core.js";
7
+ import { renderOpenAPIUI, zudoLogo, } from "../openApiUi/openApiUi.core.js";
6
8
  /**
7
- * High-level OpenAPI manager that coordinates generation, validation, and serving.
9
+ * High-level OpenAPI manager that coordinates generation, validation, and
10
+ * serving.
11
+ *
12
+ * Every mutation invalidates the cached document, so a route added after a
13
+ * first `generate()` appears in the next one, and regeneration is idempotent
14
+ * rather than throwing on the routes it registered last time.
8
15
  */
9
16
  export class OpenAPIManager {
10
17
  registry;
11
18
  scanner;
12
19
  validator;
20
+ schemas;
21
+ cacheTtlMs;
22
+ now;
23
+ logo;
24
+ /** True when `branding` was a caller-supplied logo rather than the default. */
25
+ customLogo;
13
26
  cachedDocument;
14
- constructor(version = "3.1.0") {
15
- this.registry = new OpenAPIRegistryImpl(version);
27
+ cachedAt = 0;
28
+ /** Whether the cached document was produced by a validating generate. */
29
+ cachedValidated = false;
30
+ constructor(versionOrOptions = {}) {
31
+ const options = typeof versionOrOptions === "string"
32
+ ? { version: versionOrOptions }
33
+ : versionOrOptions;
34
+ this.registry = new OpenAPIRegistryImpl(options.version ?? DEFAULT_OPENAPI_VERSION);
16
35
  this.scanner = new OpenAPIRouteScannerImpl();
17
36
  this.validator = new OpenAPIValidatorImpl();
37
+ this.schemas = new SchemaRegistryImpl({
38
+ version: options.version ?? DEFAULT_OPENAPI_VERSION,
39
+ onWarning: options.onSchemaWarning,
40
+ });
41
+ this.cacheTtlMs = options.cacheTtlMs ?? DOCUMENT_CACHE_TTL_MS;
42
+ this.now = options.now ?? (() => Date.now());
43
+ this.logo =
44
+ options.branding === false
45
+ ? undefined
46
+ : options.branding === true || options.branding === undefined
47
+ ? zudoLogo()
48
+ : options.branding;
49
+ this.customLogo =
50
+ typeof options.branding === "object" && options.branding !== null;
51
+ if (options.info)
52
+ this.registry.setInfo(options.info);
53
+ for (const server of options.servers ?? [])
54
+ this.registry.addServer(server);
55
+ for (const tag of options.tags ?? [])
56
+ this.registry.setTag(tag);
57
+ for (const requirement of options.security ?? []) {
58
+ this.registry.addSecurityRequirement(requirement);
59
+ }
60
+ }
61
+ /** The document version this manager emits. */
62
+ get version() {
63
+ return this.registry.version;
64
+ }
65
+ /* ── Mutation ────────────────────────────────────────────────────────── */
66
+ /** Sets the document's `info` object. */
67
+ setInfo(info) {
68
+ this.registry.setInfo(info);
69
+ return this.invalidateCache();
70
+ }
71
+ addServer(server) {
72
+ this.registry.addServer(server);
73
+ return this.invalidateCache();
74
+ }
75
+ addTag(tag) {
76
+ this.registry.setTag(tag);
77
+ return this.invalidateCache();
18
78
  }
19
- get registryInstance() {
20
- return this.registry;
79
+ addSecurityRequirement(requirement) {
80
+ this.registry.addSecurityRequirement(requirement);
81
+ return this.invalidateCache();
21
82
  }
22
- get scannerInstance() {
23
- return this.scanner;
83
+ addSecurityScheme(name, scheme) {
84
+ this.registry.registerSecurityScheme(name, scheme);
85
+ return this.invalidateCache();
24
86
  }
87
+ /** Registers a route. Duplicate method+path combinations are rejected. */
25
88
  addRoute(route) {
26
89
  this.scanner.addRoute(route);
90
+ return this.invalidateCache();
27
91
  }
92
+ /** Registers a route, replacing any existing one for the same method+path. */
93
+ setRoute(route) {
94
+ this.scanner.setRoute(route);
95
+ return this.invalidateCache();
96
+ }
97
+ /** Removes a route. Returns whether one was removed. */
98
+ removeRoute(method, path) {
99
+ const removed = this.scanner.removeRoute(method, path);
100
+ if (removed)
101
+ this.invalidateCache();
102
+ return removed;
103
+ }
104
+ /**
105
+ * Registers a component schema.
106
+ *
107
+ * `schema` is converted from a `@zudojs/schema` schema; pass an already
108
+ * converted {@link OpenAPISchema} to {@link OpenAPIManager.addRawSchema}.
109
+ */
28
110
  addSchema(name, schema) {
111
+ const converted = this.schemas.register(name, schema);
112
+ this.registry.registerSchema(name, converted);
113
+ return this.invalidateCache();
114
+ }
115
+ /** Registers an already-converted OpenAPI schema. */
116
+ addRawSchema(name, schema) {
29
117
  this.registry.registerSchema(name, schema);
118
+ return this.invalidateCache();
30
119
  }
120
+ /** Conversion warnings, keyed by component name. */
121
+ schemaWarnings() {
122
+ return this.schemas.warnings();
123
+ }
124
+ /* ── Generation ──────────────────────────────────────────────────────── */
125
+ /**
126
+ * Builds the document from the registered routes and components.
127
+ *
128
+ * Safe to call repeatedly: routes are replaced rather than re-added.
129
+ */
31
130
  generate(validate = false) {
32
- const routes = this.scanner.scan();
33
- for (const route of routes) {
34
- this.registry.registerRoute(route);
131
+ // The scanner is the source of truth for routes. Re-setting on top of
132
+ // the previous route set kept routes that had since been removed or
133
+ // hidden, so `removeRoute()` had no effect once a document had been
134
+ // generated.
135
+ this.registry.clearRoutes();
136
+ for (const route of this.scanner.scan()) {
137
+ this.registry.setRoute(route);
138
+ }
139
+ // Brand the document unless the caller supplied a logo or opted out.
140
+ const info = this.registry.getInfo();
141
+ if (this.logo && !info["x-logo"]) {
142
+ this.registry.setInfo({ ...info, "x-logo": this.logo });
35
143
  }
36
144
  const document = this.registry.generate();
37
- if (validate) {
145
+ if (validate)
38
146
  this.validator.assertValid(document);
39
- }
40
147
  this.cachedDocument = document;
148
+ this.cachedAt = this.now();
149
+ this.cachedValidated = validate;
41
150
  return document;
42
151
  }
152
+ /**
153
+ * Returns the document, rebuilding it when the cache is stale, absent, or
154
+ * was produced without the validation this call asks for.
155
+ */
43
156
  getDocument(validate = false) {
44
- if (this.cachedDocument) {
157
+ if (this.cachedDocument && this.isCacheFresh()) {
158
+ if (!validate || this.cachedValidated)
159
+ return this.cachedDocument;
160
+ // The cached document was never validated; validating it now is
161
+ // cheaper than rebuilding, and skipping the check silently is what a
162
+ // caller passing `true` is explicitly asking us not to do.
163
+ this.validator.assertValid(this.cachedDocument);
164
+ this.cachedValidated = true;
45
165
  return this.cachedDocument;
46
166
  }
47
167
  return this.generate(validate);
48
168
  }
49
- invalidate() {
169
+ isCacheFresh() {
170
+ if (this.cacheTtlMs <= 0)
171
+ return false;
172
+ return this.now() - this.cachedAt < this.cacheTtlMs;
173
+ }
174
+ /** Validates the current document without throwing. */
175
+ validate() {
176
+ return this.validator.validate(this.getDocument());
177
+ }
178
+ /** Drops the cached document. Called automatically by every mutation. */
179
+ invalidateCache() {
50
180
  this.cachedDocument = undefined;
181
+ this.cachedAt = 0;
182
+ this.cachedValidated = false;
183
+ return this;
184
+ }
185
+ /** Drops every registered route, component and the cached document. */
186
+ reset() {
51
187
  this.scanner.clear();
52
188
  this.registry.clear();
189
+ this.schemas.clear();
190
+ return this.invalidateCache();
53
191
  }
192
+ /**
193
+ * @deprecated Use {@link OpenAPIManager.reset} — the old name cleared all
194
+ * registered routes, which is not what "invalidate" suggests.
195
+ */
196
+ invalidate() {
197
+ this.reset();
198
+ }
199
+ /* ── Serialization ───────────────────────────────────────────────────── */
54
200
  toJSON(validate = false) {
55
201
  return toOpenAPIJSON(this.getDocument(validate));
56
202
  }
57
203
  toYAML(validate = false) {
58
204
  return toOpenAPIYAML(this.getDocument(validate));
59
205
  }
206
+ /* ── Serving ─────────────────────────────────────────────────────────── */
207
+ /**
208
+ * Builds an HTTP response carrying the document.
209
+ *
210
+ * Framework-agnostic on purpose: `{ status, headers, body }` is what every
211
+ * adapter in this monorepo can turn into its own response type.
212
+ */
213
+ toResponse(options) {
214
+ const format = options?.format ?? "json";
215
+ const body = format === "yaml"
216
+ ? this.toYAML(options?.validate ?? false)
217
+ : this.toJSON(options?.validate ?? false);
218
+ return Object.freeze({
219
+ status: 200,
220
+ headers: Object.freeze({
221
+ "content-type": format === "yaml"
222
+ ? "application/yaml; charset=utf-8"
223
+ : `${DEFAULT_MEDIA_TYPE}; charset=utf-8`,
224
+ "cache-control": options?.cacheControl ?? "public, max-age=300",
225
+ }),
226
+ body,
227
+ });
228
+ }
229
+ /**
230
+ * Builds an HTTP response carrying a branded documentation page (Swagger UI
231
+ * by default, ReDoc on request) that loads the specification from
232
+ * `options.specUrl`. Pair it with {@link OpenAPIManager.toResponse}:
233
+ *
234
+ * ```ts
235
+ * app.get("/openapi.json", () => manager.toResponse());
236
+ * app.get("/docs", () => manager.toUIResponse({ specUrl: "/openapi.json" }));
237
+ * ```
238
+ */
239
+ toUIResponse(options) {
240
+ const info = this.registry.getInfo();
241
+ const body = renderOpenAPIUI({
242
+ title: info.title && info.title !== "API"
243
+ ? `${info.title} · API reference`
244
+ : undefined,
245
+ ...options,
246
+ // Precedence: an explicit page logo, then the manager's `branding`
247
+ // (a custom logo is used as-is; `false` renders none), then the
248
+ // page's own default wordmark.
249
+ logo: options.logo !== undefined
250
+ ? options.logo
251
+ : this.logo === undefined
252
+ ? false
253
+ : this.customLogo
254
+ ? this.logo
255
+ : undefined,
256
+ });
257
+ return Object.freeze({
258
+ status: 200,
259
+ headers: Object.freeze({
260
+ "content-type": "text/html; charset=utf-8",
261
+ "cache-control": "public, max-age=300",
262
+ }),
263
+ body,
264
+ });
265
+ }
266
+ }
267
+ /** Creates an OpenAPI manager. */
268
+ export function createOpenAPIManager(options) {
269
+ return new OpenAPIManager(options ?? {});
60
270
  }
61
271
  //# sourceMappingURL=openApiHttpAdapter.core.js.map
@@ -1,8 +1,12 @@
1
- import type { OpenAPIDocument, OpenAPIParameter, OpenAPIRequestBody, OpenAPISchema, OpenAPISecurityScheme, OpenAPITag, OpenAPIReference } from "../openApiTypes/openApiTypes.core.js";
1
+ import type { OpenAPIDocument, OpenAPIExample, OpenAPIHeader, OpenAPIInfo, OpenAPILink, OpenAPIParameter, OpenAPIPathItem, OpenAPIRequestBody, OpenAPIResponse, OpenAPISchema, OpenAPISecurityRequirement, OpenAPISecurityScheme, OpenAPIServer, OpenAPITag, OpenAPIReference } from "../openApiTypes/openApiTypes.core.js";
2
2
  import type { OpenAPIRoute, OpenAPIRegistry } from "./openApiRegistry.type.js";
3
+ import type { ComponentSection } from "../openApiSchema/references.core.js";
3
4
  /** Default OpenAPI registry implementation. */
4
5
  export declare class OpenAPIRegistryImpl implements OpenAPIRegistry {
5
6
  readonly version: string;
7
+ private info;
8
+ private readonly servers;
9
+ private readonly security;
6
10
  private readonly routes;
7
11
  private readonly schemas;
8
12
  private readonly responses;
@@ -15,16 +19,54 @@ export declare class OpenAPIRegistryImpl implements OpenAPIRegistry {
15
19
  private readonly links;
16
20
  private readonly callbacks;
17
21
  constructor(version?: string);
22
+ /** Sets the document's `info` object. */
23
+ setInfo(info: OpenAPIInfo): void;
24
+ /** The document's current `info` object. */
25
+ getInfo(): OpenAPIInfo;
26
+ addServer(server: OpenAPIServer): void;
27
+ addSecurityRequirement(requirement: OpenAPISecurityRequirement): void;
28
+ private static routeKey;
18
29
  registerRoute(route: OpenAPIRoute): void;
30
+ /**
31
+ * Registers a route, replacing any existing one.
32
+ *
33
+ * This is what makes regeneration idempotent: re-registering the same routes
34
+ * into a registry that already holds them used to throw, so calling
35
+ * `generate()` twice failed.
36
+ */
37
+ setRoute(route: OpenAPIRoute): void;
38
+ /** Removes a route. Returns whether one was registered. */
39
+ removeRoute(method: string, path: string): boolean;
40
+ /**
41
+ * Drops every registered route while keeping components, servers, tags
42
+ * and security. `OpenAPIManager.generate()` rebuilds the route set from
43
+ * its scanner on each call; without this a route removed from the
44
+ * scanner lived on in the registry and in every later document.
45
+ */
46
+ clearRoutes(): void;
47
+ private static register;
19
48
  registerSchema(name: string, schema: OpenAPISchema): void;
20
- registerResponse(name: string, response: unknown): void;
49
+ registerResponse(name: string, response: OpenAPIResponse): void;
21
50
  registerParameter(name: string, parameter: OpenAPIParameter): void;
22
51
  registerRequestBody(name: string, body: OpenAPIRequestBody): void;
23
- registerHeader(name: string, header: unknown): void;
24
- registerExample(name: string, example: unknown): void;
52
+ registerHeader(name: string, header: OpenAPIHeader): void;
53
+ registerExample(name: string, example: OpenAPIExample): void;
25
54
  registerSecurityScheme(name: string, scheme: OpenAPISecurityScheme): void;
55
+ registerLink(name: string, link: OpenAPILink): void;
56
+ registerCallback(name: string, callback: Readonly<Record<string, OpenAPIPathItem>>): void;
57
+ /**
58
+ * Registers a tag.
59
+ *
60
+ * Re-registering a tag name conflicts, like every other component: quietly
61
+ * keeping the first definition discarded corrected descriptions without a
62
+ * word.
63
+ */
26
64
  registerTag(tag: OpenAPITag): void;
27
- ref(section: "schemas" | "responses" | "parameters" | "requestBodies" | "headers" | "examples" | "securitySchemes" | "links" | "callbacks", name: string): OpenAPIReference;
65
+ /** Registers a tag, replacing any existing one with the same name. */
66
+ setTag(tag: OpenAPITag): void;
67
+ ref(section: ComponentSection, name: string): OpenAPIReference;
68
+ hasComponent(section: ComponentSection, name: string): boolean;
69
+ private componentMap;
28
70
  generate(): OpenAPIDocument;
29
71
  clear(): void;
30
72
  }
@@ -1,8 +1,14 @@
1
- import { COMPONENT_REF_PREFIX, DEFAULT_OPENAPI_VERSION, } from "../openApiConstants/openApiConstants.core.js";
2
- import { OpenAPIComponentConflictError, OpenAPIDocumentError, OpenAPIOperationError, OpenAPIVersionError, } from "../openApiErrors/openApiError.core.js";
1
+ import { createComponentReference } from "../openApiSchema/references.core.js";
2
+ import { DEFAULT_OPENAPI_VERSION, SUPPORTED_OPENAPI_VERSIONS, } from "../openApiConstants/openApiConstants.core.js";
3
+ import { OpenAPIComponentConflictError, OpenAPIOperationError, OpenAPIVersionError, } from "../openApiErrors/openApiError.types.js";
4
+ /** Placeholder used until {@link OpenAPIRegistryImpl.setInfo} is called. */
5
+ const DEFAULT_INFO = { title: "API", version: "0.0.0" };
3
6
  /** Default OpenAPI registry implementation. */
4
7
  export class OpenAPIRegistryImpl {
5
8
  version;
9
+ info = DEFAULT_INFO;
10
+ servers = [];
11
+ security = [];
6
12
  routes = new Map();
7
13
  schemas = new Map();
8
14
  responses = new Map();
@@ -15,14 +21,55 @@ export class OpenAPIRegistryImpl {
15
21
  links = new Map();
16
22
  callbacks = new Map();
17
23
  constructor(version = DEFAULT_OPENAPI_VERSION) {
24
+ if (!SUPPORTED_OPENAPI_VERSIONS.includes(version)) {
25
+ // Fail where the version is chosen, not later inside generate().
26
+ throw new OpenAPIVersionError(version, SUPPORTED_OPENAPI_VERSIONS);
27
+ }
18
28
  this.version = version;
19
29
  }
30
+ /** Sets the document's `info` object. */
31
+ setInfo(info) {
32
+ this.info = { ...info };
33
+ }
34
+ /** The document's current `info` object. */
35
+ getInfo() {
36
+ return { ...this.info };
37
+ }
38
+ addServer(server) {
39
+ this.servers.push({ ...server });
40
+ }
41
+ addSecurityRequirement(requirement) {
42
+ this.security.push({ ...requirement });
43
+ }
44
+ static routeKey(route) {
45
+ return `${route.method.toLowerCase()}:${route.path}`;
46
+ }
20
47
  registerRoute(route) {
21
- const key = `${route.method}:${route.path}`;
48
+ const key = OpenAPIRegistryImpl.routeKey(route);
22
49
  const existing = this.routes.get(key);
23
- if (existing)
24
- throw new OpenAPIOperationError(`Duplicate operation for ${route.method.toUpperCase()} ${route.path}: ${existing.operation.operationId ?? "unknown"}.`);
25
- this.routes.set(key, {
50
+ if (existing) {
51
+ throw new OpenAPIOperationError(`Duplicate operation for ${route.method.toUpperCase()} ${route.path}` +
52
+ (existing.operation.operationId
53
+ ? ` (already registered as "${existing.operation.operationId}")`
54
+ : ""), {
55
+ metadata: {
56
+ method: route.method,
57
+ path: route.path,
58
+ operationId: existing.operation.operationId,
59
+ },
60
+ });
61
+ }
62
+ this.setRoute(route);
63
+ }
64
+ /**
65
+ * Registers a route, replacing any existing one.
66
+ *
67
+ * This is what makes regeneration idempotent: re-registering the same routes
68
+ * into a registry that already holds them used to throw, so calling
69
+ * `generate()` twice failed.
70
+ */
71
+ setRoute(route) {
72
+ this.routes.set(OpenAPIRegistryImpl.routeKey(route), {
26
73
  ...route,
27
74
  operation: Object.freeze({
28
75
  ...route.operation,
@@ -30,72 +77,134 @@ export class OpenAPIRegistryImpl {
30
77
  }),
31
78
  });
32
79
  }
80
+ /** Removes a route. Returns whether one was registered. */
81
+ removeRoute(method, path) {
82
+ return this.routes.delete(OpenAPIRegistryImpl.routeKey({ method, path }));
83
+ }
84
+ /**
85
+ * Drops every registered route while keeping components, servers, tags
86
+ * and security. `OpenAPIManager.generate()` rebuilds the route set from
87
+ * its scanner on each call; without this a route removed from the
88
+ * scanner lived on in the registry and in every later document.
89
+ */
90
+ clearRoutes() {
91
+ this.routes.clear();
92
+ }
93
+ static register(map, section, name, value) {
94
+ if (map.has(name)) {
95
+ throw new OpenAPIComponentConflictError(section, name);
96
+ }
97
+ map.set(name, Object.freeze(value));
98
+ }
33
99
  registerSchema(name, schema) {
34
- if (this.schemas.has(name))
35
- throw new OpenAPIComponentConflictError(`schemas/${name}`);
36
- this.schemas.set(name, Object.freeze({ ...schema }));
100
+ OpenAPIRegistryImpl.register(this.schemas, "schemas", name, { ...schema });
37
101
  }
38
102
  registerResponse(name, response) {
39
- if (this.responses.has(name))
40
- throw new OpenAPIComponentConflictError(`responses/${name}`);
41
- this.responses.set(name, Object.freeze({ ...response }));
103
+ OpenAPIRegistryImpl.register(this.responses, "responses", name, {
104
+ ...response,
105
+ });
42
106
  }
43
107
  registerParameter(name, parameter) {
44
- if (this.parameters.has(name))
45
- throw new OpenAPIComponentConflictError(`parameters/${name}`);
46
- this.parameters.set(name, Object.freeze({ ...parameter }));
108
+ OpenAPIRegistryImpl.register(this.parameters, "parameters", name, {
109
+ ...parameter,
110
+ });
47
111
  }
48
112
  registerRequestBody(name, body) {
49
- if (this.requestBodies.has(name))
50
- throw new OpenAPIComponentConflictError(`requestBodies/${name}`);
51
- this.requestBodies.set(name, Object.freeze({ ...body }));
113
+ OpenAPIRegistryImpl.register(this.requestBodies, "requestBodies", name, {
114
+ ...body,
115
+ });
52
116
  }
53
117
  registerHeader(name, header) {
54
- if (this.headers.has(name))
55
- throw new OpenAPIComponentConflictError(`headers/${name}`);
56
- this.headers.set(name, Object.freeze({ ...header }));
118
+ OpenAPIRegistryImpl.register(this.headers, "headers", name, { ...header });
57
119
  }
58
120
  registerExample(name, example) {
59
- if (this.examples.has(name))
60
- throw new OpenAPIComponentConflictError(`examples/${name}`);
61
- this.examples.set(name, Object.freeze({ ...example }));
121
+ OpenAPIRegistryImpl.register(this.examples, "examples", name, {
122
+ ...example,
123
+ });
62
124
  }
63
125
  registerSecurityScheme(name, scheme) {
64
- if (this.securitySchemes.has(name))
65
- throw new OpenAPIComponentConflictError(`securitySchemes/${name}`);
66
- this.securitySchemes.set(name, Object.freeze({ ...scheme }));
126
+ OpenAPIRegistryImpl.register(this.securitySchemes, "securitySchemes", name, {
127
+ ...scheme,
128
+ });
129
+ }
130
+ registerLink(name, link) {
131
+ OpenAPIRegistryImpl.register(this.links, "links", name, { ...link });
132
+ }
133
+ registerCallback(name, callback) {
134
+ OpenAPIRegistryImpl.register(this.callbacks, "callbacks", name, {
135
+ ...callback,
136
+ });
67
137
  }
138
+ /**
139
+ * Registers a tag.
140
+ *
141
+ * Re-registering a tag name conflicts, like every other component: quietly
142
+ * keeping the first definition discarded corrected descriptions without a
143
+ * word.
144
+ */
68
145
  registerTag(tag) {
69
- if (!this.tags.has(tag.name))
70
- this.tags.set(tag.name, Object.freeze({ ...tag }));
146
+ if (this.tags.has(tag.name)) {
147
+ throw new OpenAPIComponentConflictError("tags", tag.name);
148
+ }
149
+ this.tags.set(tag.name, Object.freeze({ ...tag }));
150
+ }
151
+ /** Registers a tag, replacing any existing one with the same name. */
152
+ setTag(tag) {
153
+ this.tags.set(tag.name, Object.freeze({ ...tag }));
71
154
  }
72
155
  ref(section, name) {
73
- return { $ref: `${COMPONENT_REF_PREFIX}/${section}/${name}` };
156
+ return createComponentReference(section, name);
157
+ }
158
+ hasComponent(section, name) {
159
+ return this.componentMap(section)?.has(name) ?? false;
160
+ }
161
+ componentMap(section) {
162
+ switch (section) {
163
+ case "schemas":
164
+ return this.schemas;
165
+ case "responses":
166
+ return this.responses;
167
+ case "parameters":
168
+ return this.parameters;
169
+ case "requestBodies":
170
+ return this.requestBodies;
171
+ case "headers":
172
+ return this.headers;
173
+ case "examples":
174
+ return this.examples;
175
+ case "securitySchemes":
176
+ return this.securitySchemes;
177
+ case "links":
178
+ return this.links;
179
+ case "callbacks":
180
+ return this.callbacks;
181
+ default:
182
+ return undefined;
183
+ }
74
184
  }
75
185
  generate() {
76
- if (!["3.0.0", "3.0.1", "3.0.2", "3.0.3", "3.1.0", "3.1.1"].includes(this.version))
77
- throw new OpenAPIVersionError(this.version);
78
- const paths = {};
79
- for (const [, route] of this.routes) {
186
+ // A `Map` rather than an object literal: a route registered at the path
187
+ // `__proto__` assigned to a literal sets the object's prototype instead
188
+ // of adding an entry, so the path disappears from the document with no
189
+ // error raised anywhere. `Object.fromEntries` defines own properties and
190
+ // has no such hole.
191
+ const paths = new Map();
192
+ for (const route of this.routes.values()) {
80
193
  const operation = Object.freeze({
81
194
  ...route.operation,
82
195
  responses: Object.freeze({ ...route.operation.responses }),
83
196
  });
84
- const existing = paths[route.path] ?? {};
85
- paths[route.path] = Object.freeze({
86
- ...existing,
87
- ...(route.method === "get" ? { get: operation } : {}),
88
- ...(route.method === "put" ? { put: operation } : {}),
89
- ...(route.method === "post" ? { post: operation } : {}),
90
- ...(route.method === "delete" ? { delete: operation } : {}),
91
- ...(route.method === "options" ? { options: operation } : {}),
92
- ...(route.method === "head" ? { head: operation } : {}),
93
- ...(route.method === "patch" ? { patch: operation } : {}),
94
- ...(route.method === "trace" ? { trace: operation } : {}),
95
- ...(route.operation.parameters?.length
96
- ? { parameters: Object.freeze([...route.operation.parameters]) }
97
- : {}),
98
- });
197
+ // Operations keep their own parameters. Hoisting them to the path item
198
+ // makes them apply to every method on that path, so two methods with
199
+ // different parameters overwrote one another.
200
+ //
201
+ // The method is lower-cased here as well as in the cache key: a path
202
+ // item field is defined in lower case, and `GET` would emit a field no
203
+ // consumer recognises while still colliding on the key.
204
+ paths.set(route.path, Object.freeze({
205
+ ...(paths.get(route.path) ?? {}),
206
+ [route.method.toLowerCase()]: operation,
207
+ }));
99
208
  }
100
209
  const components = {};
101
210
  const componentMaps = [
@@ -113,24 +222,28 @@ export class OpenAPIRegistryImpl {
113
222
  if (map.size > 0)
114
223
  components[key] = Object.freeze(Object.fromEntries(map));
115
224
  }
116
- try {
117
- return Object.freeze({
118
- openapi: this.version,
119
- info: { title: "Zudojs API", version: "1.0.0" },
120
- paths: Object.freeze(paths),
121
- ...(Object.keys(components).length > 0
122
- ? { components: Object.freeze(components) }
123
- : {}),
124
- ...(this.tags.size > 0
125
- ? { tags: Object.freeze(Array.from(this.tags.values())) }
126
- : {}),
127
- });
128
- }
129
- catch (error) {
130
- throw new OpenAPIDocumentError(`Failed to generate OpenAPI document: ${error.message}`);
131
- }
225
+ return Object.freeze({
226
+ openapi: this.version,
227
+ info: Object.freeze({ ...this.info }),
228
+ ...(this.servers.length > 0
229
+ ? { servers: Object.freeze([...this.servers]) }
230
+ : {}),
231
+ paths: Object.freeze(Object.fromEntries(paths)),
232
+ ...(Object.keys(components).length > 0
233
+ ? { components: Object.freeze(components) }
234
+ : {}),
235
+ ...(this.security.length > 0
236
+ ? { security: Object.freeze([...this.security]) }
237
+ : {}),
238
+ ...(this.tags.size > 0
239
+ ? { tags: Object.freeze([...this.tags.values()]) }
240
+ : {}),
241
+ });
132
242
  }
133
243
  clear() {
244
+ this.info = DEFAULT_INFO;
245
+ this.servers.length = 0;
246
+ this.security.length = 0;
134
247
  for (const map of [
135
248
  this.routes,
136
249
  this.schemas,
@@ -143,8 +256,9 @@ export class OpenAPIRegistryImpl {
143
256
  this.tags,
144
257
  this.links,
145
258
  this.callbacks,
146
- ])
259
+ ]) {
147
260
  map.clear();
261
+ }
148
262
  }
149
263
  }
150
264
  //# sourceMappingURL=openApiRegistry.core.js.map