@zudojs/openapi 0.0.1 → 1.1.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 +327 -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 +127 -25
  24. package/dist/openApiHttp/openApiHttpAdapter.core.js +212 -16
  25. package/dist/openApiRegistry/openApiRegistry.core.d.ts +38 -5
  26. package/dist/openApiRegistry/openApiRegistry.core.js +167 -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 +51 -3
  42. package/dist/openApiSchema/schemaConverter.core.js +475 -118
  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 +135 -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 +143 -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 +274 -66
  61. package/package.json +25 -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,27 +1,62 @@
1
1
  /**
2
2
  * Specific OpenAPI error subclasses.
3
+ *
4
+ * Each subclass supplies a default `code`, `statusCode` and `expose`, and each
5
+ * lets the caller override them — the options are spread *after* the defaults,
6
+ * so a value passed in is the value used.
3
7
  */
4
8
  import { OpenAPIError } from "./openApiError.base.js";
5
- /** Error thrown when OpenAPI validation fails. */
9
+ /** Renders an issue path the way a reader would write it. */
10
+ export function formatIssuePath(path) {
11
+ return path
12
+ .map((segment) => (typeof segment === "number" ? `[${segment}]` : segment))
13
+ .join(".")
14
+ .replace(/\.\[/g, "[");
15
+ }
16
+ /**
17
+ * Error thrown when OpenAPI validation fails.
18
+ *
19
+ * The issues are kept on the error. Discarding them left callers with the
20
+ * message "OpenAPI validation failed." and no way to learn what was wrong
21
+ * short of re-running the validator.
22
+ */
6
23
  export class OpenAPIValidationError extends OpenAPIError {
7
- constructor(message, issues, options = {}) {
24
+ issues;
25
+ constructor(message, issues = [], options = {}) {
8
26
  super(message, {
27
+ code: "OPENAPI_VALIDATION",
28
+ statusCode: 500,
29
+ expose: false,
9
30
  ...options,
10
- code: options.code ?? "OPENAPI_VALIDATION",
11
- statusCode: 400,
12
- expose: true,
31
+ metadata: {
32
+ issueCount: issues.length,
33
+ issues: issues.map((issue) => ({
34
+ path: formatIssuePath(issue.path),
35
+ message: issue.message,
36
+ severity: issue.severity,
37
+ })),
38
+ ...(options.metadata ?? {}),
39
+ },
13
40
  });
14
41
  this.name = "OpenAPIValidationError";
42
+ this.issues = Object.freeze([...issues]);
43
+ }
44
+ /** A multi-line summary suitable for a log line or a CLI. */
45
+ format() {
46
+ if (this.issues.length === 0)
47
+ return this.message;
48
+ const lines = this.issues.map((issue) => ` ${issue.severity}: ${formatIssuePath(issue.path)} — ${issue.message}`);
49
+ return [this.message, ...lines].join("\n");
15
50
  }
16
51
  }
17
52
  /** Error thrown when the OpenAPI document is invalid. */
18
53
  export class OpenAPIDocumentError extends OpenAPIError {
19
54
  constructor(message, options = {}) {
20
55
  super(message, {
21
- ...options,
22
- code: options.code ?? "OPENAPI_DOCUMENT",
56
+ code: "OPENAPI_DOCUMENT",
23
57
  statusCode: 500,
24
58
  expose: false,
59
+ ...options,
25
60
  });
26
61
  this.name = "OpenAPIDocumentError";
27
62
  }
@@ -30,46 +65,55 @@ export class OpenAPIDocumentError extends OpenAPIError {
30
65
  export class OpenAPIComponentError extends OpenAPIError {
31
66
  constructor(message, options = {}) {
32
67
  super(message, {
33
- ...options,
34
- code: options.code ?? "OPENAPI_COMPONENT",
68
+ code: "OPENAPI_COMPONENT",
35
69
  statusCode: 500,
36
70
  expose: false,
71
+ ...options,
37
72
  });
38
73
  this.name = "OpenAPIComponentError";
39
74
  }
40
75
  }
41
- /** Error thrown when an OpenAPI component conflicts with an existing component. */
76
+ /** Error thrown when a component name is already registered. */
42
77
  export class OpenAPIComponentConflictError extends OpenAPIError {
43
- constructor(message, options = {}) {
44
- super(message, {
78
+ section;
79
+ componentName;
80
+ constructor(section, componentName, options = {}) {
81
+ super(`A component named "${componentName}" is already registered under "${section}". ` +
82
+ `Component names must be unique within a section.`, {
83
+ code: "OPENAPI_COMPONENT_CONFLICT",
84
+ statusCode: 500,
85
+ expose: false,
45
86
  ...options,
46
- code: options.code ?? "OPENAPI_COMPONENT_CONFLICT",
47
- statusCode: 409,
48
- expose: true,
87
+ metadata: { section, componentName, ...(options.metadata ?? {}) },
49
88
  });
50
89
  this.name = "OpenAPIComponentConflictError";
90
+ this.section = section;
91
+ this.componentName = componentName;
51
92
  }
52
93
  }
53
- /** Error thrown when an OpenAPI reference is invalid. */
94
+ /** Error thrown when an OpenAPI reference is invalid or cannot be resolved. */
54
95
  export class OpenAPIReferenceError extends OpenAPIError {
55
- constructor(message, options = {}) {
56
- super(message, {
96
+ reference;
97
+ constructor(reference, options = {}) {
98
+ super(`Unresolvable OpenAPI reference: "${reference}".`, {
99
+ code: "OPENAPI_REFERENCE",
100
+ statusCode: 500,
101
+ expose: false,
57
102
  ...options,
58
- code: options.code ?? "OPENAPI_REFERENCE",
59
- statusCode: 400,
60
- expose: true,
103
+ metadata: { reference, ...(options.metadata ?? {}) },
61
104
  });
62
105
  this.name = "OpenAPIReferenceError";
106
+ this.reference = reference;
63
107
  }
64
108
  }
65
- /** Error thrown when an OpenAPI route is invalid. */
109
+ /** Error thrown when an OpenAPI route cannot be converted. */
66
110
  export class OpenAPIRouteError extends OpenAPIError {
67
111
  constructor(message, options = {}) {
68
112
  super(message, {
69
- ...options,
70
- code: options.code ?? "OPENAPI_ROUTE",
113
+ code: "OPENAPI_ROUTE",
71
114
  statusCode: 500,
72
115
  expose: false,
116
+ ...options,
73
117
  });
74
118
  this.name = "OpenAPIRouteError";
75
119
  }
@@ -78,10 +122,10 @@ export class OpenAPIRouteError extends OpenAPIError {
78
122
  export class OpenAPISchemaError extends OpenAPIError {
79
123
  constructor(message, options = {}) {
80
124
  super(message, {
81
- ...options,
82
- code: options.code ?? "OPENAPI_SCHEMA",
125
+ code: "OPENAPI_SCHEMA",
83
126
  statusCode: 500,
84
127
  expose: false,
128
+ ...options,
85
129
  });
86
130
  this.name = "OpenAPISchemaError";
87
131
  }
@@ -90,34 +134,38 @@ export class OpenAPISchemaError extends OpenAPIError {
90
134
  export class OpenAPISerializationError extends OpenAPIError {
91
135
  constructor(message, options = {}) {
92
136
  super(message, {
93
- ...options,
94
- code: options.code ?? "OPENAPI_SERIALIZATION",
137
+ code: "OPENAPI_SERIALIZATION",
95
138
  statusCode: 500,
96
139
  expose: false,
140
+ ...options,
97
141
  });
98
142
  this.name = "OpenAPISerializationError";
99
143
  }
100
144
  }
101
145
  /** Error thrown when the OpenAPI version is unsupported. */
102
146
  export class OpenAPIVersionError extends OpenAPIError {
103
- constructor(message, options = {}) {
104
- super(message, {
147
+ version;
148
+ constructor(version, supported = [], options = {}) {
149
+ super(`Unsupported OpenAPI version "${version}".` +
150
+ (supported.length > 0 ? ` Supported: ${supported.join(", ")}.` : ""), {
151
+ code: "OPENAPI_VERSION",
152
+ statusCode: 500,
153
+ expose: false,
105
154
  ...options,
106
- code: options.code ?? "OPENAPI_VERSION",
107
- statusCode: 400,
108
- expose: true,
155
+ metadata: { version, supported, ...(options.metadata ?? {}) },
109
156
  });
110
157
  this.name = "OpenAPIVersionError";
158
+ this.version = version;
111
159
  }
112
160
  }
113
161
  /** Error thrown when an OpenAPI operation is invalid. */
114
162
  export class OpenAPIOperationError extends OpenAPIError {
115
163
  constructor(message, options = {}) {
116
164
  super(message, {
117
- ...options,
118
- code: options.code ?? "OPENAPI_OPERATION",
165
+ code: "OPENAPI_OPERATION",
119
166
  statusCode: 500,
120
167
  expose: false,
168
+ ...options,
121
169
  });
122
170
  this.name = "OpenAPIOperationError";
123
171
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @zudojs/openapi/openApiHttp
3
3
  *
4
- * HTTP integration for OpenAPI document serving.
4
+ * Document coordination and HTTP serving.
5
5
  */
6
- export { OpenAPIManager } from "./openApiHttpAdapter.core.js";
6
+ export { OpenAPIManager, createOpenAPIManager, type OpenAPIManagerOptions, type OpenAPIDocumentResponse, type OpenAPIUIResponse, } from "./openApiHttpAdapter.core.js";
7
7
  //# sourceMappingURL=index.d.ts.map
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @zudojs/openapi/openApiHttp
3
3
  *
4
- * HTTP integration for OpenAPI document serving.
4
+ * Document coordination and HTTP serving.
5
5
  */
6
- export { OpenAPIManager } from "./openApiHttpAdapter.core.js";
6
+ export { OpenAPIManager, createOpenAPIManager, } from "./openApiHttpAdapter.core.js";
7
7
  //# sourceMappingURL=index.js.map
@@ -1,38 +1,140 @@
1
- import type { OpenAPIDocument } from "../openApiTypes/openApiTypes.core.js";
2
- import { OpenAPIRegistryImpl } from "../openApiRegistry/openApiRegistry.core.js";
3
- import { OpenAPIRouteScannerImpl } from "../openApiRouting/routeScanner.core.js";
1
+ import type { OpenAPIDocument, OpenAPIInfo, OpenAPILogo, OpenAPISchema, OpenAPISecurityRequirement, OpenAPISecurityScheme, OpenAPIServer, OpenAPITag } from "../openApiTypes/openApiTypes.core.js";
2
+ import type { RouteInfo, OpenAPIHttpMethod } from "../openApiRouting/routeMetadata.type.js";
3
+ import { type OpenAPIValidationResult } from "../openApiValidation/openApiValidator.core.js";
4
+ import { type OpenAPIUIOptions } from "../openApiUi/openApiUi.core.js";
5
+ /** Options for {@link OpenAPIManager}. */
6
+ export interface OpenAPIManagerOptions {
7
+ /** Specification version to emit. Default: 3.1.0. */
8
+ readonly version?: string;
9
+ /** Document metadata. Without it the document is titled "API" at 0.0.0. */
10
+ readonly info?: OpenAPIInfo;
11
+ readonly servers?: readonly OpenAPIServer[];
12
+ readonly tags?: readonly OpenAPITag[];
13
+ readonly security?: readonly OpenAPISecurityRequirement[];
14
+ /**
15
+ * How long a generated document is reused before it is rebuilt, in ms.
16
+ * Default: five minutes. `0` disables caching. Mutating the manager
17
+ * invalidates the cache regardless.
18
+ */
19
+ readonly cacheTtlMs?: number;
20
+ /** Reports what a schema conversion could not express. */
21
+ readonly onSchemaWarning?: (name: string, warnings: readonly string[]) => void;
22
+ /** Supplies the clock, for tests. */
23
+ readonly now?: () => number;
24
+ /**
25
+ * Logo written to `info["x-logo"]` so viewers such as ReDoc and Scalar
26
+ * show it, and used by {@link OpenAPIManager.toUIResponse}.
27
+ * Default: the Zudo mark. Pass `false` to emit no logo, or an
28
+ * {@link OpenAPILogo} to use your own.
29
+ */
30
+ readonly branding?: boolean | OpenAPILogo;
31
+ }
32
+ /** An HTTP response carrying a rendered documentation page. */
33
+ export interface OpenAPIUIResponse {
34
+ readonly status: 200;
35
+ readonly headers: Readonly<Record<string, string>>;
36
+ readonly body: string;
37
+ }
38
+ /** An HTTP response carrying the specification document. */
39
+ export interface OpenAPIDocumentResponse {
40
+ readonly status: 200;
41
+ readonly headers: Readonly<Record<string, string>>;
42
+ readonly body: string;
43
+ }
4
44
  /**
5
- * High-level OpenAPI manager that coordinates generation, validation, and serving.
45
+ * High-level OpenAPI manager that coordinates generation, validation, and
46
+ * serving.
47
+ *
48
+ * Every mutation invalidates the cached document, so a route added after a
49
+ * first `generate()` appears in the next one, and regeneration is idempotent
50
+ * rather than throwing on the routes it registered last time.
6
51
  */
7
52
  export declare class OpenAPIManager {
8
53
  private readonly registry;
9
54
  private readonly scanner;
10
55
  private readonly validator;
56
+ private readonly schemas;
57
+ private readonly cacheTtlMs;
58
+ private readonly now;
59
+ private readonly logo;
11
60
  private cachedDocument?;
12
- constructor(version?: string);
13
- get registryInstance(): OpenAPIRegistryImpl;
14
- get scannerInstance(): OpenAPIRouteScannerImpl;
15
- addRoute(route: {
16
- readonly method: "get" | "put" | "post" | "delete" | "options" | "head" | "patch" | "trace";
17
- readonly path: string;
18
- readonly metadata?: {
19
- readonly openapi?: {
20
- readonly operationId?: string;
21
- readonly summary?: string;
22
- readonly description?: string;
23
- readonly tags?: readonly string[];
24
- readonly responses?: Record<string, unknown>;
25
- readonly requestBody?: unknown;
26
- readonly parameters?: readonly unknown[];
27
- readonly security?: readonly Record<string, readonly string[]>[];
28
- };
29
- };
30
- }): void;
31
- addSchema(name: string, schema: unknown): void;
61
+ private cachedAt;
62
+ /** Whether the cached document was produced by a validating generate. */
63
+ private cachedValidated;
64
+ constructor(versionOrOptions?: string | OpenAPIManagerOptions);
65
+ /** The document version this manager emits. */
66
+ get version(): string;
67
+ /** Sets the document's `info` object. */
68
+ setInfo(info: OpenAPIInfo): this;
69
+ addServer(server: OpenAPIServer): this;
70
+ addTag(tag: OpenAPITag): this;
71
+ addSecurityRequirement(requirement: OpenAPISecurityRequirement): this;
72
+ addSecurityScheme(name: string, scheme: OpenAPISecurityScheme): this;
73
+ /** Registers a route. Duplicate method+path combinations are rejected. */
74
+ addRoute(route: RouteInfo): this;
75
+ /** Registers a route, replacing any existing one for the same method+path. */
76
+ setRoute(route: RouteInfo): this;
77
+ /** Removes a route. Returns whether one was removed. */
78
+ removeRoute(method: OpenAPIHttpMethod, path: string): boolean;
79
+ /**
80
+ * Registers a component schema.
81
+ *
82
+ * `schema` is converted from a `@zudojs/schema` schema; pass an already
83
+ * converted {@link OpenAPISchema} to {@link OpenAPIManager.addRawSchema}.
84
+ */
85
+ addSchema(name: string, schema: unknown): this;
86
+ /** Registers an already-converted OpenAPI schema. */
87
+ addRawSchema(name: string, schema: OpenAPISchema): this;
88
+ /** Conversion warnings, keyed by component name. */
89
+ schemaWarnings(): ReadonlyMap<string, readonly string[]>;
90
+ /**
91
+ * Builds the document from the registered routes and components.
92
+ *
93
+ * Safe to call repeatedly: routes are replaced rather than re-added.
94
+ */
32
95
  generate(validate?: boolean): OpenAPIDocument;
33
- getDocument(validate?: boolean): Readonly<OpenAPIDocument>;
96
+ /**
97
+ * Returns the document, rebuilding it when the cache is stale, absent, or
98
+ * was produced without the validation this call asks for.
99
+ */
100
+ getDocument(validate?: boolean): OpenAPIDocument;
101
+ private isCacheFresh;
102
+ /** Validates the current document without throwing. */
103
+ validate(): OpenAPIValidationResult;
104
+ /** Drops the cached document. Called automatically by every mutation. */
105
+ invalidateCache(): this;
106
+ /** Drops every registered route, component and the cached document. */
107
+ reset(): this;
108
+ /**
109
+ * @deprecated Use {@link OpenAPIManager.reset} — the old name cleared all
110
+ * registered routes, which is not what "invalidate" suggests.
111
+ */
34
112
  invalidate(): void;
35
113
  toJSON(validate?: boolean): string;
36
114
  toYAML(validate?: boolean): string;
115
+ /**
116
+ * Builds an HTTP response carrying the document.
117
+ *
118
+ * Framework-agnostic on purpose: `{ status, headers, body }` is what every
119
+ * adapter in this monorepo can turn into its own response type.
120
+ */
121
+ toResponse(options?: {
122
+ readonly format?: "json" | "yaml";
123
+ readonly validate?: boolean;
124
+ readonly cacheControl?: string;
125
+ }): OpenAPIDocumentResponse;
126
+ /**
127
+ * Builds an HTTP response carrying a branded documentation page (Swagger UI
128
+ * by default, ReDoc on request) that loads the specification from
129
+ * `options.specUrl`. Pair it with {@link OpenAPIManager.toResponse}:
130
+ *
131
+ * ```ts
132
+ * app.get("/openapi.json", () => manager.toResponse());
133
+ * app.get("/docs", () => manager.toUIResponse({ specUrl: "/openapi.json" }));
134
+ * ```
135
+ */
136
+ toUIResponse(options: OpenAPIUIOptions): OpenAPIUIResponse;
37
137
  }
138
+ /** Creates an OpenAPI manager. */
139
+ export declare function createOpenAPIManager(options?: OpenAPIManagerOptions): OpenAPIManager;
38
140
  //# sourceMappingURL=openApiHttpAdapter.core.d.ts.map
@@ -1,61 +1,257 @@
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;
13
24
  cachedDocument;
14
- constructor(version = "3.1.0") {
15
- this.registry = new OpenAPIRegistryImpl(version);
25
+ cachedAt = 0;
26
+ /** Whether the cached document was produced by a validating generate. */
27
+ cachedValidated = false;
28
+ constructor(versionOrOptions = {}) {
29
+ const options = typeof versionOrOptions === "string"
30
+ ? { version: versionOrOptions }
31
+ : versionOrOptions;
32
+ this.registry = new OpenAPIRegistryImpl(options.version ?? DEFAULT_OPENAPI_VERSION);
16
33
  this.scanner = new OpenAPIRouteScannerImpl();
17
34
  this.validator = new OpenAPIValidatorImpl();
35
+ this.schemas = new SchemaRegistryImpl({
36
+ version: options.version ?? DEFAULT_OPENAPI_VERSION,
37
+ onWarning: options.onSchemaWarning,
38
+ });
39
+ this.cacheTtlMs = options.cacheTtlMs ?? DOCUMENT_CACHE_TTL_MS;
40
+ this.now = options.now ?? (() => Date.now());
41
+ this.logo =
42
+ options.branding === false
43
+ ? undefined
44
+ : options.branding === true || options.branding === undefined
45
+ ? zudoLogo()
46
+ : options.branding;
47
+ if (options.info)
48
+ this.registry.setInfo(options.info);
49
+ for (const server of options.servers ?? [])
50
+ this.registry.addServer(server);
51
+ for (const tag of options.tags ?? [])
52
+ this.registry.setTag(tag);
53
+ for (const requirement of options.security ?? []) {
54
+ this.registry.addSecurityRequirement(requirement);
55
+ }
56
+ }
57
+ /** The document version this manager emits. */
58
+ get version() {
59
+ return this.registry.version;
60
+ }
61
+ /* ── Mutation ────────────────────────────────────────────────────────── */
62
+ /** Sets the document's `info` object. */
63
+ setInfo(info) {
64
+ this.registry.setInfo(info);
65
+ return this.invalidateCache();
66
+ }
67
+ addServer(server) {
68
+ this.registry.addServer(server);
69
+ return this.invalidateCache();
70
+ }
71
+ addTag(tag) {
72
+ this.registry.setTag(tag);
73
+ return this.invalidateCache();
18
74
  }
19
- get registryInstance() {
20
- return this.registry;
75
+ addSecurityRequirement(requirement) {
76
+ this.registry.addSecurityRequirement(requirement);
77
+ return this.invalidateCache();
21
78
  }
22
- get scannerInstance() {
23
- return this.scanner;
79
+ addSecurityScheme(name, scheme) {
80
+ this.registry.registerSecurityScheme(name, scheme);
81
+ return this.invalidateCache();
24
82
  }
83
+ /** Registers a route. Duplicate method+path combinations are rejected. */
25
84
  addRoute(route) {
26
85
  this.scanner.addRoute(route);
86
+ return this.invalidateCache();
27
87
  }
88
+ /** Registers a route, replacing any existing one for the same method+path. */
89
+ setRoute(route) {
90
+ this.scanner.setRoute(route);
91
+ return this.invalidateCache();
92
+ }
93
+ /** Removes a route. Returns whether one was removed. */
94
+ removeRoute(method, path) {
95
+ const removed = this.scanner.removeRoute(method, path);
96
+ if (removed)
97
+ this.invalidateCache();
98
+ return removed;
99
+ }
100
+ /**
101
+ * Registers a component schema.
102
+ *
103
+ * `schema` is converted from a `@zudojs/schema` schema; pass an already
104
+ * converted {@link OpenAPISchema} to {@link OpenAPIManager.addRawSchema}.
105
+ */
28
106
  addSchema(name, schema) {
107
+ const converted = this.schemas.register(name, schema);
108
+ this.registry.registerSchema(name, converted);
109
+ return this.invalidateCache();
110
+ }
111
+ /** Registers an already-converted OpenAPI schema. */
112
+ addRawSchema(name, schema) {
29
113
  this.registry.registerSchema(name, schema);
114
+ return this.invalidateCache();
30
115
  }
116
+ /** Conversion warnings, keyed by component name. */
117
+ schemaWarnings() {
118
+ return this.schemas.warnings();
119
+ }
120
+ /* ── Generation ──────────────────────────────────────────────────────── */
121
+ /**
122
+ * Builds the document from the registered routes and components.
123
+ *
124
+ * Safe to call repeatedly: routes are replaced rather than re-added.
125
+ */
31
126
  generate(validate = false) {
32
- const routes = this.scanner.scan();
33
- for (const route of routes) {
34
- this.registry.registerRoute(route);
127
+ for (const route of this.scanner.scan()) {
128
+ this.registry.setRoute(route);
129
+ }
130
+ // Brand the document unless the caller supplied a logo or opted out.
131
+ const info = this.registry.getInfo();
132
+ if (this.logo && !info["x-logo"]) {
133
+ this.registry.setInfo({ ...info, "x-logo": this.logo });
35
134
  }
36
135
  const document = this.registry.generate();
37
- if (validate) {
136
+ if (validate)
38
137
  this.validator.assertValid(document);
39
- }
40
138
  this.cachedDocument = document;
139
+ this.cachedAt = this.now();
140
+ this.cachedValidated = validate;
41
141
  return document;
42
142
  }
143
+ /**
144
+ * Returns the document, rebuilding it when the cache is stale, absent, or
145
+ * was produced without the validation this call asks for.
146
+ */
43
147
  getDocument(validate = false) {
44
- if (this.cachedDocument) {
148
+ if (this.cachedDocument && this.isCacheFresh()) {
149
+ if (!validate || this.cachedValidated)
150
+ return this.cachedDocument;
151
+ // The cached document was never validated; validating it now is
152
+ // cheaper than rebuilding, and skipping the check silently is what a
153
+ // caller passing `true` is explicitly asking us not to do.
154
+ this.validator.assertValid(this.cachedDocument);
155
+ this.cachedValidated = true;
45
156
  return this.cachedDocument;
46
157
  }
47
158
  return this.generate(validate);
48
159
  }
49
- invalidate() {
160
+ isCacheFresh() {
161
+ if (this.cacheTtlMs <= 0)
162
+ return false;
163
+ return this.now() - this.cachedAt < this.cacheTtlMs;
164
+ }
165
+ /** Validates the current document without throwing. */
166
+ validate() {
167
+ return this.validator.validate(this.getDocument());
168
+ }
169
+ /** Drops the cached document. Called automatically by every mutation. */
170
+ invalidateCache() {
50
171
  this.cachedDocument = undefined;
172
+ this.cachedAt = 0;
173
+ this.cachedValidated = false;
174
+ return this;
175
+ }
176
+ /** Drops every registered route, component and the cached document. */
177
+ reset() {
51
178
  this.scanner.clear();
52
179
  this.registry.clear();
180
+ this.schemas.clear();
181
+ return this.invalidateCache();
53
182
  }
183
+ /**
184
+ * @deprecated Use {@link OpenAPIManager.reset} — the old name cleared all
185
+ * registered routes, which is not what "invalidate" suggests.
186
+ */
187
+ invalidate() {
188
+ this.reset();
189
+ }
190
+ /* ── Serialization ───────────────────────────────────────────────────── */
54
191
  toJSON(validate = false) {
55
192
  return toOpenAPIJSON(this.getDocument(validate));
56
193
  }
57
194
  toYAML(validate = false) {
58
195
  return toOpenAPIYAML(this.getDocument(validate));
59
196
  }
197
+ /* ── Serving ─────────────────────────────────────────────────────────── */
198
+ /**
199
+ * Builds an HTTP response carrying the document.
200
+ *
201
+ * Framework-agnostic on purpose: `{ status, headers, body }` is what every
202
+ * adapter in this monorepo can turn into its own response type.
203
+ */
204
+ toResponse(options) {
205
+ const format = options?.format ?? "json";
206
+ const body = format === "yaml"
207
+ ? this.toYAML(options?.validate ?? false)
208
+ : this.toJSON(options?.validate ?? false);
209
+ return Object.freeze({
210
+ status: 200,
211
+ headers: Object.freeze({
212
+ "content-type": format === "yaml"
213
+ ? "application/yaml; charset=utf-8"
214
+ : `${DEFAULT_MEDIA_TYPE}; charset=utf-8`,
215
+ "cache-control": options?.cacheControl ?? "public, max-age=300",
216
+ }),
217
+ body,
218
+ });
219
+ }
220
+ /**
221
+ * Builds an HTTP response carrying a branded documentation page (Swagger UI
222
+ * by default, ReDoc on request) that loads the specification from
223
+ * `options.specUrl`. Pair it with {@link OpenAPIManager.toResponse}:
224
+ *
225
+ * ```ts
226
+ * app.get("/openapi.json", () => manager.toResponse());
227
+ * app.get("/docs", () => manager.toUIResponse({ specUrl: "/openapi.json" }));
228
+ * ```
229
+ */
230
+ toUIResponse(options) {
231
+ const info = this.registry.getInfo();
232
+ const body = renderOpenAPIUI({
233
+ title: info.title && info.title !== "API"
234
+ ? `${info.title} · API reference`
235
+ : undefined,
236
+ ...options,
237
+ logo: options.logo !== undefined
238
+ ? options.logo
239
+ : this.logo === undefined
240
+ ? false
241
+ : undefined,
242
+ });
243
+ return Object.freeze({
244
+ status: 200,
245
+ headers: Object.freeze({
246
+ "content-type": "text/html; charset=utf-8",
247
+ "cache-control": "public, max-age=300",
248
+ }),
249
+ body,
250
+ });
251
+ }
252
+ }
253
+ /** Creates an OpenAPI manager. */
254
+ export function createOpenAPIManager(options) {
255
+ return new OpenAPIManager(options ?? {});
60
256
  }
61
257
  //# sourceMappingURL=openApiHttpAdapter.core.js.map