@beignet/core 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/README.md +288 -0
- package/dist/application/index.d.ts +260 -0
- package/dist/application/index.d.ts.map +1 -0
- package/dist/application/index.js +324 -0
- package/dist/application/index.js.map +1 -0
- package/dist/client/client.d.ts +241 -0
- package/dist/client/client.d.ts.map +1 -0
- package/dist/client/client.js +531 -0
- package/dist/client/client.js.map +1 -0
- package/dist/client/index.d.ts +10 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +8 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/types.d.ts +139 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/client/types.js +2 -0
- package/dist/client/types.js.map +1 -0
- package/dist/config/index.d.ts +122 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +216 -0
- package/dist/config/index.js.map +1 -0
- package/dist/contracts/contract-builder.d.ts +121 -0
- package/dist/contracts/contract-builder.d.ts.map +1 -0
- package/dist/contracts/contract-builder.js +346 -0
- package/dist/contracts/contract-builder.js.map +1 -0
- package/dist/contracts/contract-group.d.ts +106 -0
- package/dist/contracts/contract-group.d.ts.map +1 -0
- package/dist/contracts/contract-group.js +240 -0
- package/dist/contracts/contract-group.js.map +1 -0
- package/dist/contracts/contract-like.d.ts +21 -0
- package/dist/contracts/contract-like.d.ts.map +1 -0
- package/dist/contracts/contract-like.js +9 -0
- package/dist/contracts/contract-like.js.map +1 -0
- package/dist/contracts/index.d.ts +15 -0
- package/dist/contracts/index.d.ts.map +1 -0
- package/dist/contracts/index.js +11 -0
- package/dist/contracts/index.js.map +1 -0
- package/dist/contracts/openapi-meta.d.ts +23 -0
- package/dist/contracts/openapi-meta.d.ts.map +1 -0
- package/dist/contracts/openapi-meta.js +2 -0
- package/dist/contracts/openapi-meta.js.map +1 -0
- package/dist/contracts/path-template.d.ts +17 -0
- package/dist/contracts/path-template.d.ts.map +1 -0
- package/dist/contracts/path-template.js +50 -0
- package/dist/contracts/path-template.js.map +1 -0
- package/dist/contracts/rate-limit.d.ts +50 -0
- package/dist/contracts/rate-limit.d.ts.map +1 -0
- package/dist/contracts/rate-limit.js +2 -0
- package/dist/contracts/rate-limit.js.map +1 -0
- package/dist/contracts/types.d.ts +97 -0
- package/dist/contracts/types.d.ts.map +1 -0
- package/dist/contracts/types.js +54 -0
- package/dist/contracts/types.js.map +1 -0
- package/dist/contracts/utils.d.ts +3 -0
- package/dist/contracts/utils.d.ts.map +1 -0
- package/dist/contracts/utils.js +44 -0
- package/dist/contracts/utils.js.map +1 -0
- package/dist/domain/entity.d.ts +87 -0
- package/dist/domain/entity.d.ts.map +1 -0
- package/dist/domain/entity.js +155 -0
- package/dist/domain/entity.js.map +1 -0
- package/dist/domain/events.d.ts +41 -0
- package/dist/domain/events.d.ts.map +1 -0
- package/dist/domain/events.js +21 -0
- package/dist/domain/events.js.map +1 -0
- package/dist/domain/index.d.ts +14 -0
- package/dist/domain/index.d.ts.map +1 -0
- package/dist/domain/index.js +14 -0
- package/dist/domain/index.js.map +1 -0
- package/dist/domain/value-object.d.ts +60 -0
- package/dist/domain/value-object.d.ts.map +1 -0
- package/dist/domain/value-object.js +87 -0
- package/dist/domain/value-object.js.map +1 -0
- package/dist/errors/catalog.d.ts +71 -0
- package/dist/errors/catalog.d.ts.map +1 -0
- package/dist/errors/catalog.js +71 -0
- package/dist/errors/catalog.js.map +1 -0
- package/dist/errors/http.d.ts +77 -0
- package/dist/errors/http.d.ts.map +1 -0
- package/dist/errors/http.js +74 -0
- package/dist/errors/http.js.map +1 -0
- package/dist/errors/index.d.ts +10 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +14 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/errors/response.d.ts +26 -0
- package/dist/errors/response.d.ts.map +1 -0
- package/dist/errors/response.js +34 -0
- package/dist/errors/response.js.map +1 -0
- package/dist/errors/validation.d.ts +18 -0
- package/dist/errors/validation.d.ts.map +1 -0
- package/dist/errors/validation.js +21 -0
- package/dist/errors/validation.js.map +1 -0
- package/dist/events/index.d.ts +58 -0
- package/dist/events/index.d.ts.map +1 -0
- package/dist/events/index.js +102 -0
- package/dist/events/index.js.map +1 -0
- package/dist/jobs/index.d.ts +56 -0
- package/dist/jobs/index.d.ts.map +1 -0
- package/dist/jobs/index.js +89 -0
- package/dist/jobs/index.js.map +1 -0
- package/dist/mail/index.d.ts +75 -0
- package/dist/mail/index.d.ts.map +1 -0
- package/dist/mail/index.js +84 -0
- package/dist/mail/index.js.map +1 -0
- package/dist/openapi/index.d.ts +207 -0
- package/dist/openapi/index.d.ts.map +1 -0
- package/dist/openapi/index.js +449 -0
- package/dist/openapi/index.js.map +1 -0
- package/dist/openapi/schema-introspector.d.ts +38 -0
- package/dist/openapi/schema-introspector.d.ts.map +1 -0
- package/dist/openapi/schema-introspector.js +67 -0
- package/dist/openapi/schema-introspector.js.map +1 -0
- package/dist/ports/audit.d.ts +58 -0
- package/dist/ports/audit.d.ts.map +1 -0
- package/dist/ports/audit.js +74 -0
- package/dist/ports/audit.js.map +1 -0
- package/dist/ports/auth.d.ts +23 -0
- package/dist/ports/auth.d.ts.map +1 -0
- package/dist/ports/auth.js +31 -0
- package/dist/ports/auth.js.map +1 -0
- package/dist/ports/builder.d.ts +61 -0
- package/dist/ports/builder.d.ts.map +1 -0
- package/dist/ports/builder.js +48 -0
- package/dist/ports/builder.js.map +1 -0
- package/dist/ports/cache.d.ts +15 -0
- package/dist/ports/cache.d.ts.map +1 -0
- package/dist/ports/cache.js +57 -0
- package/dist/ports/cache.js.map +1 -0
- package/dist/ports/clock.d.ts +10 -0
- package/dist/ports/clock.d.ts.map +1 -0
- package/dist/ports/clock.js +21 -0
- package/dist/ports/clock.js.map +1 -0
- package/dist/ports/events.d.ts +71 -0
- package/dist/ports/events.d.ts.map +1 -0
- package/dist/ports/events.js +2 -0
- package/dist/ports/events.js.map +1 -0
- package/dist/ports/id-generator.d.ts +12 -0
- package/dist/ports/id-generator.d.ts.map +1 -0
- package/dist/ports/id-generator.js +22 -0
- package/dist/ports/id-generator.js.map +1 -0
- package/dist/ports/index.d.ts +98 -0
- package/dist/ports/index.d.ts.map +1 -0
- package/dist/ports/index.js +67 -0
- package/dist/ports/index.js.map +1 -0
- package/dist/ports/logger.d.ts +22 -0
- package/dist/ports/logger.d.ts.map +1 -0
- package/dist/ports/logger.js +34 -0
- package/dist/ports/logger.js.map +1 -0
- package/dist/ports/mailer.d.ts +6 -0
- package/dist/ports/mailer.d.ts.map +1 -0
- package/dist/ports/mailer.js +2 -0
- package/dist/ports/mailer.js.map +1 -0
- package/dist/ports/policy.d.ts +53 -0
- package/dist/ports/policy.d.ts.map +1 -0
- package/dist/ports/policy.js +81 -0
- package/dist/ports/policy.js.map +1 -0
- package/dist/ports/rate-limit.d.ts +41 -0
- package/dist/ports/rate-limit.d.ts.map +1 -0
- package/dist/ports/rate-limit.js +37 -0
- package/dist/ports/rate-limit.js.map +1 -0
- package/dist/ports/redaction.d.ts +26 -0
- package/dist/ports/redaction.d.ts.map +1 -0
- package/dist/ports/redaction.js +126 -0
- package/dist/ports/redaction.js.map +1 -0
- package/dist/ports/schedules.d.ts +9 -0
- package/dist/ports/schedules.d.ts.map +1 -0
- package/dist/ports/schedules.js +2 -0
- package/dist/ports/schedules.js.map +1 -0
- package/dist/ports/storage.d.ts +47 -0
- package/dist/ports/storage.d.ts.map +1 -0
- package/dist/ports/storage.js +185 -0
- package/dist/ports/storage.js.map +1 -0
- package/dist/ports/testing.d.ts +73 -0
- package/dist/ports/testing.d.ts.map +1 -0
- package/dist/ports/testing.js +105 -0
- package/dist/ports/testing.js.map +1 -0
- package/dist/ports/unit-of-work.d.ts +56 -0
- package/dist/ports/unit-of-work.d.ts.map +1 -0
- package/dist/ports/unit-of-work.js +64 -0
- package/dist/ports/unit-of-work.js.map +1 -0
- package/dist/providers/index.d.ts +8 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +8 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/instrumentation.d.ts +91 -0
- package/dist/providers/instrumentation.d.ts.map +1 -0
- package/dist/providers/instrumentation.js +93 -0
- package/dist/providers/instrumentation.js.map +1 -0
- package/dist/providers/provider.d.ts +146 -0
- package/dist/providers/provider.d.ts.map +1 -0
- package/dist/providers/provider.js +31 -0
- package/dist/providers/provider.js.map +1 -0
- package/dist/schedules/index.d.ts +105 -0
- package/dist/schedules/index.d.ts.map +1 -0
- package/dist/schedules/index.js +178 -0
- package/dist/schedules/index.js.map +1 -0
- package/dist/server/contract-like.d.ts +5 -0
- package/dist/server/contract-like.d.ts.map +1 -0
- package/dist/server/contract-like.js +5 -0
- package/dist/server/contract-like.js.map +1 -0
- package/dist/server/health.d.ts +41 -0
- package/dist/server/health.d.ts.map +1 -0
- package/dist/server/health.js +46 -0
- package/dist/server/health.js.map +1 -0
- package/dist/server/hooks/auth.d.ts +42 -0
- package/dist/server/hooks/auth.d.ts.map +1 -0
- package/dist/server/hooks/auth.js +61 -0
- package/dist/server/hooks/auth.js.map +1 -0
- package/dist/server/hooks/cors.d.ts +13 -0
- package/dist/server/hooks/cors.d.ts.map +1 -0
- package/dist/server/hooks/cors.js +70 -0
- package/dist/server/hooks/cors.js.map +1 -0
- package/dist/server/hooks/errors.d.ts +66 -0
- package/dist/server/hooks/errors.d.ts.map +1 -0
- package/dist/server/hooks/errors.js +83 -0
- package/dist/server/hooks/errors.js.map +1 -0
- package/dist/server/hooks/index.d.ts +12 -0
- package/dist/server/hooks/index.d.ts.map +1 -0
- package/dist/server/hooks/index.js +12 -0
- package/dist/server/hooks/index.js.map +1 -0
- package/dist/server/hooks/logging.d.ts +33 -0
- package/dist/server/hooks/logging.d.ts.map +1 -0
- package/dist/server/hooks/logging.js +90 -0
- package/dist/server/hooks/logging.js.map +1 -0
- package/dist/server/hooks/rate-limit.d.ts +29 -0
- package/dist/server/hooks/rate-limit.d.ts.map +1 -0
- package/dist/server/hooks/rate-limit.js +93 -0
- package/dist/server/hooks/rate-limit.js.map +1 -0
- package/dist/server/hooks/utils.d.ts +9 -0
- package/dist/server/hooks/utils.d.ts.map +1 -0
- package/dist/server/hooks/utils.js +16 -0
- package/dist/server/hooks/utils.js.map +1 -0
- package/dist/server/hooks.d.ts +2 -0
- package/dist/server/hooks.d.ts.map +1 -0
- package/dist/server/hooks.js +2 -0
- package/dist/server/hooks.js.map +1 -0
- package/dist/server/http.d.ts +124 -0
- package/dist/server/http.d.ts.map +1 -0
- package/dist/server/http.js +2 -0
- package/dist/server/http.js.map +1 -0
- package/dist/server/index.d.ts +19 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +15 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/openapi.d.ts +32 -0
- package/dist/server/openapi.d.ts.map +1 -0
- package/dist/server/openapi.js +43 -0
- package/dist/server/openapi.js.map +1 -0
- package/dist/server/providers/index.d.ts +4 -0
- package/dist/server/providers/index.d.ts.map +1 -0
- package/dist/server/providers/index.js +4 -0
- package/dist/server/providers/index.js.map +1 -0
- package/dist/server/providers/loadProviderConfig.d.ts +7 -0
- package/dist/server/providers/loadProviderConfig.d.ts.map +1 -0
- package/dist/server/providers/loadProviderConfig.js +42 -0
- package/dist/server/providers/loadProviderConfig.js.map +1 -0
- package/dist/server/server.d.ts +86 -0
- package/dist/server/server.d.ts.map +1 -0
- package/dist/server/server.js +1031 -0
- package/dist/server/server.js.map +1 -0
- package/dist/server/types.d.ts +3 -0
- package/dist/server/types.d.ts.map +1 -0
- package/dist/server/types.js +3 -0
- package/dist/server/types.js.map +1 -0
- package/package.json +129 -0
- package/src/application/index.ts +747 -0
- package/src/client/client.ts +1105 -0
- package/src/client/index.ts +45 -0
- package/src/client/types.ts +305 -0
- package/src/config/index.ts +497 -0
- package/src/contracts/contract-builder.ts +583 -0
- package/src/contracts/contract-group.ts +502 -0
- package/src/contracts/contract-like.ts +29 -0
- package/src/contracts/index.ts +53 -0
- package/src/contracts/openapi-meta.ts +22 -0
- package/src/contracts/path-template.ts +91 -0
- package/src/contracts/rate-limit.ts +50 -0
- package/src/contracts/types.ts +207 -0
- package/src/contracts/utils.ts +56 -0
- package/src/domain/entity.ts +256 -0
- package/src/domain/events.ts +52 -0
- package/src/domain/index.ts +18 -0
- package/src/domain/value-object.ts +135 -0
- package/src/errors/catalog.ts +149 -0
- package/src/errors/http.ts +80 -0
- package/src/errors/index.ts +28 -0
- package/src/errors/response.ts +54 -0
- package/src/errors/validation.ts +35 -0
- package/src/events/index.ts +246 -0
- package/src/jobs/index.ts +211 -0
- package/src/mail/index.ts +177 -0
- package/src/openapi/index.ts +865 -0
- package/src/openapi/schema-introspector.ts +107 -0
- package/src/ports/audit.ts +176 -0
- package/src/ports/auth.ts +76 -0
- package/src/ports/builder.ts +97 -0
- package/src/ports/cache.ts +94 -0
- package/src/ports/clock.ts +34 -0
- package/src/ports/events.ts +100 -0
- package/src/ports/id-generator.ts +36 -0
- package/src/ports/index.ts +221 -0
- package/src/ports/logger.ts +67 -0
- package/src/ports/policy.ts +242 -0
- package/src/ports/rate-limit.ts +91 -0
- package/src/ports/redaction.ts +199 -0
- package/src/ports/storage.ts +282 -0
- package/src/ports/testing.ts +234 -0
- package/src/ports/unit-of-work.ts +134 -0
- package/src/providers/index.ts +40 -0
- package/src/providers/instrumentation.ts +248 -0
- package/src/providers/provider.ts +191 -0
- package/src/schedules/index.ts +442 -0
- package/src/server/contract-like.ts +8 -0
- package/src/server/health.ts +82 -0
- package/src/server/hooks/auth.ts +147 -0
- package/src/server/hooks/cors.ts +87 -0
- package/src/server/hooks/errors.ts +126 -0
- package/src/server/hooks/index.ts +43 -0
- package/src/server/hooks/logging.ts +121 -0
- package/src/server/hooks/rate-limit.ts +171 -0
- package/src/server/hooks/utils.ts +16 -0
- package/src/server/hooks.ts +1 -0
- package/src/server/http.ts +189 -0
- package/src/server/index.ts +35 -0
- package/src/server/openapi.ts +72 -0
- package/src/server/providers/index.ts +3 -0
- package/src/server/providers/loadProviderConfig.ts +72 -0
- package/src/server/server.ts +1521 -0
- package/src/server/types.ts +2 -0
|
@@ -0,0 +1,865 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @beignet/core/openapi
|
|
3
|
+
*
|
|
4
|
+
* OpenAPI 3.1 generation from Beignet contracts
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { type ZodTypeAny, z } from "zod";
|
|
8
|
+
import {
|
|
9
|
+
type AnyContract,
|
|
10
|
+
type ContractLike,
|
|
11
|
+
getContractHeaderSchemas,
|
|
12
|
+
methodSupportsRequestBody,
|
|
13
|
+
parsePathTemplate,
|
|
14
|
+
resolveContract,
|
|
15
|
+
STANDARD_ERROR_RESPONSE_SCHEMA,
|
|
16
|
+
} from "../contracts";
|
|
17
|
+
import {
|
|
18
|
+
createZodIntrospector,
|
|
19
|
+
type SchemaIntrospector,
|
|
20
|
+
} from "./schema-introspector";
|
|
21
|
+
|
|
22
|
+
// Re-export the introspector types for consumers who want custom implementations
|
|
23
|
+
export {
|
|
24
|
+
createZodIntrospector,
|
|
25
|
+
type SchemaIntrospector,
|
|
26
|
+
} from "./schema-introspector";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* OpenAPI 3.1 Info Object
|
|
30
|
+
*/
|
|
31
|
+
export interface OpenAPIInfo {
|
|
32
|
+
title: string;
|
|
33
|
+
version: string;
|
|
34
|
+
description?: string;
|
|
35
|
+
termsOfService?: string;
|
|
36
|
+
contact?: {
|
|
37
|
+
name?: string;
|
|
38
|
+
url?: string;
|
|
39
|
+
email?: string;
|
|
40
|
+
};
|
|
41
|
+
license?: {
|
|
42
|
+
name: string;
|
|
43
|
+
url?: string;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* OpenAPI 3.1 Server Object
|
|
49
|
+
*/
|
|
50
|
+
export interface OpenAPIServer {
|
|
51
|
+
url: string;
|
|
52
|
+
description?: string;
|
|
53
|
+
variables?: Record<
|
|
54
|
+
string,
|
|
55
|
+
{
|
|
56
|
+
default: string;
|
|
57
|
+
enum?: string[];
|
|
58
|
+
description?: string;
|
|
59
|
+
}
|
|
60
|
+
>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* OpenAPI 3.1 Security Scheme Object
|
|
65
|
+
*/
|
|
66
|
+
export type OpenAPISecurityScheme =
|
|
67
|
+
| {
|
|
68
|
+
type: "apiKey";
|
|
69
|
+
name: string;
|
|
70
|
+
in: "query" | "header" | "cookie";
|
|
71
|
+
description?: string;
|
|
72
|
+
}
|
|
73
|
+
| {
|
|
74
|
+
type: "http";
|
|
75
|
+
scheme: string;
|
|
76
|
+
bearerFormat?: string;
|
|
77
|
+
description?: string;
|
|
78
|
+
}
|
|
79
|
+
| {
|
|
80
|
+
type: "oauth2";
|
|
81
|
+
flows: Record<string, unknown>;
|
|
82
|
+
description?: string;
|
|
83
|
+
}
|
|
84
|
+
| {
|
|
85
|
+
type: "openIdConnect";
|
|
86
|
+
openIdConnectUrl: string;
|
|
87
|
+
description?: string;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Schema or Reference object for OpenAPI
|
|
92
|
+
*/
|
|
93
|
+
export type SchemaObject = Record<string, unknown>;
|
|
94
|
+
export type ReferenceObject = { $ref: string };
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* OpenAPI Parameter Object
|
|
98
|
+
*/
|
|
99
|
+
export interface ParameterObject {
|
|
100
|
+
name: string;
|
|
101
|
+
in: "path" | "query" | "header" | "cookie";
|
|
102
|
+
required?: boolean;
|
|
103
|
+
schema?: SchemaObject | ReferenceObject;
|
|
104
|
+
description?: string;
|
|
105
|
+
deprecated?: boolean;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* OpenAPI Request Body Object
|
|
110
|
+
*/
|
|
111
|
+
export interface RequestBodyObject {
|
|
112
|
+
required?: boolean;
|
|
113
|
+
description?: string;
|
|
114
|
+
content: Record<
|
|
115
|
+
string,
|
|
116
|
+
{
|
|
117
|
+
schema?: SchemaObject | ReferenceObject;
|
|
118
|
+
}
|
|
119
|
+
>;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* OpenAPI Response Object
|
|
124
|
+
*/
|
|
125
|
+
export interface ResponseObject {
|
|
126
|
+
description: string;
|
|
127
|
+
content?: Record<
|
|
128
|
+
string,
|
|
129
|
+
{
|
|
130
|
+
schema?: SchemaObject | ReferenceObject;
|
|
131
|
+
examples?: Record<
|
|
132
|
+
string,
|
|
133
|
+
{
|
|
134
|
+
summary?: string;
|
|
135
|
+
value?: unknown;
|
|
136
|
+
}
|
|
137
|
+
>;
|
|
138
|
+
}
|
|
139
|
+
>;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* OpenAPI Operation Object
|
|
144
|
+
*/
|
|
145
|
+
export interface OperationObject {
|
|
146
|
+
operationId?: string;
|
|
147
|
+
summary?: string;
|
|
148
|
+
description?: string;
|
|
149
|
+
tags?: string[];
|
|
150
|
+
deprecated?: boolean;
|
|
151
|
+
externalDocs?: {
|
|
152
|
+
description?: string;
|
|
153
|
+
url: string;
|
|
154
|
+
};
|
|
155
|
+
security?: Array<Record<string, string[]>>;
|
|
156
|
+
parameters?: ParameterObject[];
|
|
157
|
+
requestBody?: RequestBodyObject;
|
|
158
|
+
responses: Record<string, ResponseObject>;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* OpenAPI Path Item Object
|
|
163
|
+
*/
|
|
164
|
+
export interface PathItemObject {
|
|
165
|
+
get?: OperationObject;
|
|
166
|
+
post?: OperationObject;
|
|
167
|
+
put?: OperationObject;
|
|
168
|
+
patch?: OperationObject;
|
|
169
|
+
delete?: OperationObject;
|
|
170
|
+
head?: OperationObject;
|
|
171
|
+
options?: OperationObject;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* OpenAPI Paths Object
|
|
176
|
+
*/
|
|
177
|
+
export type PathsObject = Record<string, PathItemObject>;
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* OpenAPI Components Object
|
|
181
|
+
*/
|
|
182
|
+
export interface ComponentsObject {
|
|
183
|
+
schemas?: Record<string, SchemaObject>;
|
|
184
|
+
securitySchemes?: Record<string, OpenAPISecurityScheme>;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* OpenAPI 3.1 Document
|
|
189
|
+
*/
|
|
190
|
+
export interface OpenAPIObject {
|
|
191
|
+
openapi: "3.1.0";
|
|
192
|
+
info: OpenAPIInfo;
|
|
193
|
+
servers?: OpenAPIServer[];
|
|
194
|
+
paths: PathsObject;
|
|
195
|
+
components?: ComponentsObject;
|
|
196
|
+
security?: Array<Record<string, string[]>>;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Options for generating OpenAPI spec
|
|
201
|
+
*/
|
|
202
|
+
export interface OpenAPIGeneratorOptions {
|
|
203
|
+
/** API title */
|
|
204
|
+
title: string;
|
|
205
|
+
/** API version */
|
|
206
|
+
version: string;
|
|
207
|
+
/** API description */
|
|
208
|
+
description?: string;
|
|
209
|
+
/** Server configurations */
|
|
210
|
+
servers?: OpenAPIServer[];
|
|
211
|
+
/** Media type for JSON content (default: application/json) */
|
|
212
|
+
jsonMediaType?: string;
|
|
213
|
+
/** Security schemes for authentication */
|
|
214
|
+
securitySchemes?: Record<string, OpenAPISecurityScheme>;
|
|
215
|
+
/** Global security requirements */
|
|
216
|
+
security?: Array<Record<string, string[]>>;
|
|
217
|
+
/**
|
|
218
|
+
* Schema introspector for reading metadata from schema objects.
|
|
219
|
+
* Defaults to a Zod introspector. Provide a custom implementation
|
|
220
|
+
* to support other schema libraries.
|
|
221
|
+
*/
|
|
222
|
+
schemaIntrospector?: SchemaIntrospector;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Internal state for generator
|
|
227
|
+
*/
|
|
228
|
+
type GeneratorState = {
|
|
229
|
+
components: ComponentsObject;
|
|
230
|
+
jsonMediaType: string;
|
|
231
|
+
introspector: SchemaIntrospector;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
type SchemaIO = "input" | "output";
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Type for contracts that can be passed to the generator.
|
|
238
|
+
* Accepts both ContractBuilder instances and plain HttpContractConfig objects.
|
|
239
|
+
*/
|
|
240
|
+
export type ContractInput = ContractLike;
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Convert an array of contracts to an OpenAPI 3.1 document
|
|
244
|
+
*
|
|
245
|
+
* @param contracts - Array of HTTP contracts (ContractBuilder instances or configs)
|
|
246
|
+
* @param options - OpenAPI generation options
|
|
247
|
+
* @returns OpenAPI 3.1 document object
|
|
248
|
+
*
|
|
249
|
+
* @example
|
|
250
|
+
* ```ts
|
|
251
|
+
* import { contractsToOpenAPI } from "@beignet/core/openapi";
|
|
252
|
+
* import { getTodo, listTodos } from "./contracts";
|
|
253
|
+
*
|
|
254
|
+
* const spec = contractsToOpenAPI(
|
|
255
|
+
* [getTodo, listTodos],
|
|
256
|
+
* {
|
|
257
|
+
* title: "Todo API",
|
|
258
|
+
* version: "1.0.0",
|
|
259
|
+
* servers: [{ url: "https://api.example.com" }],
|
|
260
|
+
* }
|
|
261
|
+
* );
|
|
262
|
+
* ```
|
|
263
|
+
*/
|
|
264
|
+
export function contractsToOpenAPI(
|
|
265
|
+
contracts: readonly ContractInput[],
|
|
266
|
+
options: OpenAPIGeneratorOptions,
|
|
267
|
+
): OpenAPIObject {
|
|
268
|
+
const paths: PathsObject = {};
|
|
269
|
+
const components: ComponentsObject = {
|
|
270
|
+
schemas: {},
|
|
271
|
+
securitySchemes: options.securitySchemes ?? {},
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const state: GeneratorState = {
|
|
275
|
+
components,
|
|
276
|
+
jsonMediaType: options.jsonMediaType ?? "application/json",
|
|
277
|
+
introspector: options.schemaIntrospector ?? createZodIntrospector(),
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
for (const contract of contracts) {
|
|
281
|
+
const config = resolveContract(contract);
|
|
282
|
+
addContractToPaths(config, paths, state);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const openapi: OpenAPIObject = {
|
|
286
|
+
openapi: "3.1.0",
|
|
287
|
+
info: {
|
|
288
|
+
title: options.title,
|
|
289
|
+
version: options.version,
|
|
290
|
+
description: options.description,
|
|
291
|
+
},
|
|
292
|
+
servers: options.servers,
|
|
293
|
+
paths,
|
|
294
|
+
components:
|
|
295
|
+
Object.keys(components.schemas ?? {}).length > 0 ||
|
|
296
|
+
Object.keys(components.securitySchemes ?? {}).length > 0
|
|
297
|
+
? components
|
|
298
|
+
: undefined,
|
|
299
|
+
security: options.security,
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
return openapi;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// =============================================================================
|
|
306
|
+
// OpenAPI Generation Helpers
|
|
307
|
+
// =============================================================================
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Add a single contract to the paths object
|
|
311
|
+
*/
|
|
312
|
+
function addContractToPaths(
|
|
313
|
+
contract: AnyContract,
|
|
314
|
+
paths: PathsObject,
|
|
315
|
+
state: GeneratorState,
|
|
316
|
+
): void {
|
|
317
|
+
const pathKey = parsePathTemplate(contract.path).openApiPath;
|
|
318
|
+
if (!paths[pathKey]) {
|
|
319
|
+
paths[pathKey] = {};
|
|
320
|
+
}
|
|
321
|
+
const pathItem = paths[pathKey];
|
|
322
|
+
|
|
323
|
+
const meta = contract.metadata?.openapi;
|
|
324
|
+
|
|
325
|
+
const operation: OperationObject = {
|
|
326
|
+
operationId: meta?.operationId ?? contract.name,
|
|
327
|
+
summary: meta?.summary,
|
|
328
|
+
description: meta?.description,
|
|
329
|
+
tags: meta?.tags,
|
|
330
|
+
deprecated: meta?.deprecated,
|
|
331
|
+
externalDocs: meta?.externalDocs,
|
|
332
|
+
security: meta?.security,
|
|
333
|
+
parameters: [],
|
|
334
|
+
responses: {},
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
addPathParams(contract, operation, state);
|
|
338
|
+
addQueryParams(contract, operation, state);
|
|
339
|
+
addHeaderParams(contract, operation, state);
|
|
340
|
+
addRequestBody(contract, operation, state);
|
|
341
|
+
addResponses(contract, operation, state);
|
|
342
|
+
|
|
343
|
+
// Clean up empty parameters array
|
|
344
|
+
if (operation.parameters?.length === 0) {
|
|
345
|
+
delete operation.parameters;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const methodKey = contract.method.toLowerCase() as
|
|
349
|
+
| "get"
|
|
350
|
+
| "post"
|
|
351
|
+
| "put"
|
|
352
|
+
| "patch"
|
|
353
|
+
| "delete"
|
|
354
|
+
| "head"
|
|
355
|
+
| "options";
|
|
356
|
+
|
|
357
|
+
pathItem[methodKey] = operation;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function addParameter(
|
|
361
|
+
operation: OperationObject,
|
|
362
|
+
parameter: ParameterObject,
|
|
363
|
+
): void {
|
|
364
|
+
if (!operation.parameters) {
|
|
365
|
+
operation.parameters = [];
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const existingIndex = operation.parameters.findIndex(
|
|
369
|
+
(existing) =>
|
|
370
|
+
existing.in === parameter.in && existing.name === parameter.name,
|
|
371
|
+
);
|
|
372
|
+
if (existingIndex >= 0) {
|
|
373
|
+
operation.parameters[existingIndex] = parameter;
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
operation.parameters.push(parameter);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Add path parameters from contract to operation.
|
|
382
|
+
*/
|
|
383
|
+
function addPathParams(
|
|
384
|
+
contract: AnyContract,
|
|
385
|
+
operation: OperationObject,
|
|
386
|
+
state: GeneratorState,
|
|
387
|
+
): void {
|
|
388
|
+
const pathKeys = parsePathTemplate(contract.path).keys;
|
|
389
|
+
if (!contract.pathParams) {
|
|
390
|
+
for (const key of pathKeys) {
|
|
391
|
+
addParameter(operation, {
|
|
392
|
+
name: key,
|
|
393
|
+
in: "path",
|
|
394
|
+
required: true,
|
|
395
|
+
schema: { type: "string" },
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const shape = state.introspector.getShape(contract.pathParams);
|
|
402
|
+
if (!shape) {
|
|
403
|
+
if (pathKeys.length > 0) {
|
|
404
|
+
throw new Error(
|
|
405
|
+
`Unable to introspect pathParams for contract "${contract.name}" at "${contract.path}". OpenAPI path parameters must match the path template.`,
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const shapeKeys = Object.keys(shape);
|
|
412
|
+
const missingKeys = pathKeys.filter((key) => !shapeKeys.includes(key));
|
|
413
|
+
const extraKeys = shapeKeys.filter((key) => !pathKeys.includes(key));
|
|
414
|
+
if (missingKeys.length > 0 || extraKeys.length > 0) {
|
|
415
|
+
const details = [
|
|
416
|
+
missingKeys.length > 0
|
|
417
|
+
? `missing pathParams keys: ${missingKeys.join(", ")}`
|
|
418
|
+
: undefined,
|
|
419
|
+
extraKeys.length > 0
|
|
420
|
+
? `extra pathParams keys: ${extraKeys.join(", ")}`
|
|
421
|
+
: undefined,
|
|
422
|
+
]
|
|
423
|
+
.filter(Boolean)
|
|
424
|
+
.join("; ");
|
|
425
|
+
throw new Error(
|
|
426
|
+
`Path parameters for contract "${contract.name}" must match "${contract.path}" (${details}).`,
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
for (const key of pathKeys) {
|
|
431
|
+
const field = shape[key];
|
|
432
|
+
const paramSchemaRef = zodToSchemaRef(
|
|
433
|
+
field as ZodTypeAny,
|
|
434
|
+
`${contract.name}_path_${key}`,
|
|
435
|
+
state,
|
|
436
|
+
"input",
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
const description = state.introspector.getDescription(field);
|
|
440
|
+
|
|
441
|
+
const param: ParameterObject = {
|
|
442
|
+
name: key,
|
|
443
|
+
in: "path",
|
|
444
|
+
required: true,
|
|
445
|
+
schema: paramSchemaRef,
|
|
446
|
+
description,
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
addParameter(operation, param);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Add query parameters from contract to operation.
|
|
455
|
+
*/
|
|
456
|
+
function addQueryParams(
|
|
457
|
+
contract: AnyContract,
|
|
458
|
+
operation: OperationObject,
|
|
459
|
+
state: GeneratorState,
|
|
460
|
+
): void {
|
|
461
|
+
if (!contract.query) return;
|
|
462
|
+
|
|
463
|
+
const shape = state.introspector.getShape(contract.query);
|
|
464
|
+
if (!shape) return;
|
|
465
|
+
|
|
466
|
+
for (const key of Object.keys(shape)) {
|
|
467
|
+
const originalField = shape[key];
|
|
468
|
+
const optional = state.introspector.isOptional(originalField);
|
|
469
|
+
|
|
470
|
+
let description = state.introspector.getDescription(originalField);
|
|
471
|
+
|
|
472
|
+
const field = originalField;
|
|
473
|
+
|
|
474
|
+
// If outer optional didn't have description, check inner type
|
|
475
|
+
if (!description && optional) {
|
|
476
|
+
description = state.introspector.getDescription(field);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const paramSchemaRef = zodToSchemaRef(
|
|
480
|
+
field as ZodTypeAny,
|
|
481
|
+
`${contract.name}_query_${key}`,
|
|
482
|
+
state,
|
|
483
|
+
"input",
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
const param: ParameterObject = {
|
|
487
|
+
name: key,
|
|
488
|
+
in: "query",
|
|
489
|
+
required: !optional,
|
|
490
|
+
schema: paramSchemaRef,
|
|
491
|
+
description,
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
addParameter(operation, param);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Add header parameters from contract to operation.
|
|
500
|
+
*/
|
|
501
|
+
function addHeaderParams(
|
|
502
|
+
contract: AnyContract,
|
|
503
|
+
operation: OperationObject,
|
|
504
|
+
state: GeneratorState,
|
|
505
|
+
): void {
|
|
506
|
+
const headerSchemas = getContractHeaderSchemas(contract.headers);
|
|
507
|
+
for (const headerSchema of headerSchemas) {
|
|
508
|
+
const shape = state.introspector.getShape(headerSchema);
|
|
509
|
+
if (!shape) continue;
|
|
510
|
+
|
|
511
|
+
for (const key of Object.keys(shape)) {
|
|
512
|
+
const originalField = shape[key];
|
|
513
|
+
const optional = state.introspector.isOptional(originalField);
|
|
514
|
+
const description = state.introspector.getDescription(originalField);
|
|
515
|
+
|
|
516
|
+
const paramSchemaRef = zodToSchemaRef(
|
|
517
|
+
originalField as ZodTypeAny,
|
|
518
|
+
`${contract.name}_header_${key}`,
|
|
519
|
+
state,
|
|
520
|
+
"input",
|
|
521
|
+
);
|
|
522
|
+
|
|
523
|
+
addParameter(operation, {
|
|
524
|
+
name: key,
|
|
525
|
+
in: "header",
|
|
526
|
+
required: !optional,
|
|
527
|
+
schema: paramSchemaRef,
|
|
528
|
+
description,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Add request body from contract to operation
|
|
536
|
+
*/
|
|
537
|
+
function addRequestBody(
|
|
538
|
+
contract: AnyContract,
|
|
539
|
+
operation: OperationObject,
|
|
540
|
+
state: GeneratorState,
|
|
541
|
+
): void {
|
|
542
|
+
if (!contract.body) return;
|
|
543
|
+
if (!methodSupportsRequestBody(contract.method)) {
|
|
544
|
+
throw new Error(
|
|
545
|
+
`Request bodies are not supported for ${contract.method} contracts. Use POST, PUT, or PATCH for contract request bodies.`,
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const schemaRef = zodToSchemaRef(
|
|
550
|
+
contract.body as ZodTypeAny,
|
|
551
|
+
`${contract.name}_body`,
|
|
552
|
+
state,
|
|
553
|
+
"input",
|
|
554
|
+
);
|
|
555
|
+
|
|
556
|
+
const description = state.introspector.getDescription(contract.body);
|
|
557
|
+
|
|
558
|
+
operation.requestBody = {
|
|
559
|
+
required: true,
|
|
560
|
+
description,
|
|
561
|
+
content: {
|
|
562
|
+
[state.jsonMediaType]: {
|
|
563
|
+
schema: schemaRef,
|
|
564
|
+
},
|
|
565
|
+
},
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Add responses from contract to operation
|
|
571
|
+
*/
|
|
572
|
+
function addResponses(
|
|
573
|
+
contract: AnyContract,
|
|
574
|
+
operation: OperationObject,
|
|
575
|
+
state: GeneratorState,
|
|
576
|
+
): void {
|
|
577
|
+
// Process all responses (both success and error status codes)
|
|
578
|
+
for (const [statusKey, zodSchema] of Object.entries(contract.responses)) {
|
|
579
|
+
const status = statusKey; // Keep as string
|
|
580
|
+
const catalogErrors = getCatalogErrorsForStatus(contract, Number(status));
|
|
581
|
+
|
|
582
|
+
// null schema means void/empty response (e.g. .responses({ 204: null }))
|
|
583
|
+
const hasSchema = zodSchema != null;
|
|
584
|
+
|
|
585
|
+
const schemaRef = hasSchema
|
|
586
|
+
? catalogErrors.length > 0 && zodSchema === STANDARD_ERROR_RESPONSE_SCHEMA
|
|
587
|
+
? catalogErrorResponseSchemaRef(
|
|
588
|
+
catalogErrors,
|
|
589
|
+
`${contract.name}_response_${status}`,
|
|
590
|
+
state,
|
|
591
|
+
)
|
|
592
|
+
: schemaToSchemaRef(
|
|
593
|
+
zodSchema,
|
|
594
|
+
`${contract.name}_response_${status}`,
|
|
595
|
+
state,
|
|
596
|
+
"output",
|
|
597
|
+
)
|
|
598
|
+
: undefined;
|
|
599
|
+
|
|
600
|
+
const described = hasSchema
|
|
601
|
+
? state.introspector.getDescription(zodSchema)
|
|
602
|
+
: undefined;
|
|
603
|
+
|
|
604
|
+
let description: string;
|
|
605
|
+
|
|
606
|
+
if (described) {
|
|
607
|
+
description = described;
|
|
608
|
+
} else if (catalogErrors.length === 1) {
|
|
609
|
+
description = catalogErrors[0].message;
|
|
610
|
+
} else if (catalogErrors.length > 1) {
|
|
611
|
+
description = catalogErrors.map((error) => error.message).join("; ");
|
|
612
|
+
} else if (status === "200") {
|
|
613
|
+
description = "OK";
|
|
614
|
+
} else if (status === "201") {
|
|
615
|
+
description = "Created";
|
|
616
|
+
} else if (status === "204") {
|
|
617
|
+
description = "No Content";
|
|
618
|
+
} else if (status === "400") {
|
|
619
|
+
description = "Bad Request";
|
|
620
|
+
} else if (status === "401") {
|
|
621
|
+
description = "Unauthorized";
|
|
622
|
+
} else if (status === "403") {
|
|
623
|
+
description = "Forbidden";
|
|
624
|
+
} else if (status === "404") {
|
|
625
|
+
description = "Not Found";
|
|
626
|
+
} else if (status === "500") {
|
|
627
|
+
description = "Internal Server Error";
|
|
628
|
+
} else {
|
|
629
|
+
description = `HTTP ${status}`;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const examples = examplesFromCatalogErrors(catalogErrors);
|
|
633
|
+
operation.responses[status] = {
|
|
634
|
+
description,
|
|
635
|
+
content:
|
|
636
|
+
!hasSchema || status === "204"
|
|
637
|
+
? undefined
|
|
638
|
+
: {
|
|
639
|
+
[state.jsonMediaType]: {
|
|
640
|
+
schema: schemaRef,
|
|
641
|
+
...(examples ? { examples } : {}),
|
|
642
|
+
},
|
|
643
|
+
},
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
type CatalogErrorForOpenAPI = {
|
|
649
|
+
key: string;
|
|
650
|
+
code: string;
|
|
651
|
+
status: number;
|
|
652
|
+
message: string;
|
|
653
|
+
details?: unknown;
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
function getCatalogErrorsForStatus(
|
|
657
|
+
contract: AnyContract,
|
|
658
|
+
status: number,
|
|
659
|
+
): CatalogErrorForOpenAPI[] {
|
|
660
|
+
const errors = contract.metadata?.errors;
|
|
661
|
+
if (typeof errors !== "object" || errors === null) return [];
|
|
662
|
+
|
|
663
|
+
return Object.entries(errors)
|
|
664
|
+
.map(([key, error]) => {
|
|
665
|
+
if (
|
|
666
|
+
typeof error !== "object" ||
|
|
667
|
+
error === null ||
|
|
668
|
+
typeof (error as { code?: unknown }).code !== "string" ||
|
|
669
|
+
typeof (error as { status?: unknown }).status !== "number" ||
|
|
670
|
+
typeof (error as { message?: unknown }).message !== "string"
|
|
671
|
+
) {
|
|
672
|
+
return undefined;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const catalogError: CatalogErrorForOpenAPI = {
|
|
676
|
+
key,
|
|
677
|
+
code: (error as { code: string }).code,
|
|
678
|
+
status: (error as { status: number }).status,
|
|
679
|
+
message: (error as { message: string }).message,
|
|
680
|
+
};
|
|
681
|
+
const details = (error as { details?: unknown }).details;
|
|
682
|
+
if (details !== undefined) {
|
|
683
|
+
catalogError.details = details;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
return catalogError;
|
|
687
|
+
})
|
|
688
|
+
.filter(
|
|
689
|
+
(error): error is CatalogErrorForOpenAPI =>
|
|
690
|
+
error !== undefined && error.status === status,
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function catalogErrorResponseSchemaRef(
|
|
695
|
+
errors: CatalogErrorForOpenAPI[],
|
|
696
|
+
nameHint: string,
|
|
697
|
+
state: GeneratorState,
|
|
698
|
+
): SchemaObject | ReferenceObject {
|
|
699
|
+
const schema =
|
|
700
|
+
errors.length === 1
|
|
701
|
+
? catalogErrorResponseSchema(errors[0], nameHint, state)
|
|
702
|
+
: {
|
|
703
|
+
oneOf: errors.map((error) =>
|
|
704
|
+
catalogErrorResponseSchema(
|
|
705
|
+
error,
|
|
706
|
+
`${nameHint}_${error.key}`,
|
|
707
|
+
state,
|
|
708
|
+
),
|
|
709
|
+
),
|
|
710
|
+
};
|
|
711
|
+
|
|
712
|
+
if (!state.components.schemas) {
|
|
713
|
+
state.components.schemas = {};
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const schemaName = normalizeSchemaName(nameHint, state);
|
|
717
|
+
if (!state.components.schemas[schemaName]) {
|
|
718
|
+
state.components.schemas[schemaName] = schema;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
return { $ref: `#/components/schemas/${schemaName}` };
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function catalogErrorResponseSchema(
|
|
725
|
+
error: CatalogErrorForOpenAPI,
|
|
726
|
+
nameHint: string,
|
|
727
|
+
state: GeneratorState,
|
|
728
|
+
): SchemaObject {
|
|
729
|
+
return {
|
|
730
|
+
type: "object",
|
|
731
|
+
properties: {
|
|
732
|
+
code: { type: "string", const: error.code },
|
|
733
|
+
message: { type: "string" },
|
|
734
|
+
...(error.details
|
|
735
|
+
? {
|
|
736
|
+
details: schemaToSchemaRef(
|
|
737
|
+
error.details,
|
|
738
|
+
`${nameHint}_details`,
|
|
739
|
+
state,
|
|
740
|
+
"output",
|
|
741
|
+
),
|
|
742
|
+
}
|
|
743
|
+
: { details: {} }),
|
|
744
|
+
requestId: { type: "string" },
|
|
745
|
+
},
|
|
746
|
+
required: ["code", "message"],
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function examplesFromCatalogErrors(
|
|
751
|
+
errors: CatalogErrorForOpenAPI[],
|
|
752
|
+
): Record<string, { summary?: string; value?: unknown }> | undefined {
|
|
753
|
+
if (errors.length === 0) return undefined;
|
|
754
|
+
|
|
755
|
+
return Object.fromEntries(
|
|
756
|
+
errors.map((error) => [
|
|
757
|
+
normalizeExampleKey(error.key),
|
|
758
|
+
{
|
|
759
|
+
summary: error.message,
|
|
760
|
+
value: {
|
|
761
|
+
code: error.code,
|
|
762
|
+
message: error.message,
|
|
763
|
+
},
|
|
764
|
+
},
|
|
765
|
+
]),
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function normalizeExampleKey(key: string): string {
|
|
770
|
+
const normalized = key.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
771
|
+
return normalized || "error";
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* Convert a Zod schema to a JSON Schema reference, registering it in components
|
|
776
|
+
*/
|
|
777
|
+
function zodToSchemaRef(
|
|
778
|
+
schema: ZodTypeAny,
|
|
779
|
+
nameHint: string,
|
|
780
|
+
state: GeneratorState,
|
|
781
|
+
io: SchemaIO,
|
|
782
|
+
): SchemaObject | ReferenceObject {
|
|
783
|
+
// Ensure schemas object exists
|
|
784
|
+
if (!state.components.schemas) {
|
|
785
|
+
state.components.schemas = {};
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const schemaName = normalizeSchemaName(nameHint, state);
|
|
789
|
+
|
|
790
|
+
if (!state.components.schemas[schemaName]) {
|
|
791
|
+
const jsonSchema = z.toJSONSchema(schema, {
|
|
792
|
+
target: "draft-2020-12",
|
|
793
|
+
unrepresentable: "any",
|
|
794
|
+
io,
|
|
795
|
+
}) as SchemaObject;
|
|
796
|
+
|
|
797
|
+
// Remove $schema as it's not needed in OpenAPI
|
|
798
|
+
delete jsonSchema.$schema;
|
|
799
|
+
|
|
800
|
+
state.components.schemas[schemaName] = jsonSchema;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
return { $ref: `#/components/schemas/${schemaName}` };
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function schemaToSchemaRef(
|
|
807
|
+
schema: unknown,
|
|
808
|
+
nameHint: string,
|
|
809
|
+
state: GeneratorState,
|
|
810
|
+
io: SchemaIO,
|
|
811
|
+
): SchemaObject | ReferenceObject {
|
|
812
|
+
if (schema === STANDARD_ERROR_RESPONSE_SCHEMA) {
|
|
813
|
+
return standardErrorResponseSchemaRef(nameHint, state);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
return zodToSchemaRef(schema as ZodTypeAny, nameHint, state, io);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function standardErrorResponseSchemaRef(
|
|
820
|
+
nameHint: string,
|
|
821
|
+
state: GeneratorState,
|
|
822
|
+
): ReferenceObject {
|
|
823
|
+
if (!state.components.schemas) {
|
|
824
|
+
state.components.schemas = {};
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
const schemaName = normalizeSchemaName(nameHint, state);
|
|
828
|
+
if (!state.components.schemas[schemaName]) {
|
|
829
|
+
state.components.schemas[schemaName] = {
|
|
830
|
+
type: "object",
|
|
831
|
+
properties: {
|
|
832
|
+
code: { type: "string" },
|
|
833
|
+
message: { type: "string" },
|
|
834
|
+
details: {},
|
|
835
|
+
requestId: { type: "string" },
|
|
836
|
+
},
|
|
837
|
+
required: ["code", "message"],
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
return { $ref: `#/components/schemas/${schemaName}` };
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* Normalize schema name for use in components.
|
|
846
|
+
* Appends a counter suffix only when the base name already exists.
|
|
847
|
+
*/
|
|
848
|
+
function normalizeSchemaName(name: string, state: GeneratorState): string {
|
|
849
|
+
const base = name.replace(/[^A-Za-z0-9]/g, "_");
|
|
850
|
+
|
|
851
|
+
// Check if base name is already used
|
|
852
|
+
if (!state.components.schemas?.[base]) {
|
|
853
|
+
return base;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// Find a unique name by appending a counter
|
|
857
|
+
let counter = 1;
|
|
858
|
+
let uniqueName = `${base}_${counter}`;
|
|
859
|
+
while (state.components.schemas[uniqueName]) {
|
|
860
|
+
counter++;
|
|
861
|
+
uniqueName = `${base}_${counter}`;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
return uniqueName;
|
|
865
|
+
}
|