@contractspec/lib.contracts-transformers 1.57.0 → 1.58.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.
- package/dist/browser/common/index.js +86 -0
- package/dist/browser/index.js +2414 -0
- package/dist/browser/openapi/index.js +2404 -0
- package/dist/common/index.d.ts +6 -3
- package/dist/common/index.d.ts.map +1 -0
- package/dist/common/index.js +87 -3
- package/dist/common/types.d.ts +119 -120
- package/dist/common/types.d.ts.map +1 -1
- package/dist/common/utils.d.ts +11 -14
- package/dist/common/utils.d.ts.map +1 -1
- package/dist/index.d.ts +18 -18
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2415 -18
- package/dist/node/common/index.js +86 -0
- package/dist/node/index.js +2414 -0
- package/dist/node/openapi/index.js +2404 -0
- package/dist/openapi/differ.d.ts +21 -21
- package/dist/openapi/differ.d.ts.map +1 -1
- package/dist/openapi/exporter/data-views.d.ts +24 -25
- package/dist/openapi/exporter/data-views.d.ts.map +1 -1
- package/dist/openapi/exporter/events.d.ts +15 -15
- package/dist/openapi/exporter/events.d.ts.map +1 -1
- package/dist/openapi/exporter/features.d.ts +23 -24
- package/dist/openapi/exporter/features.d.ts.map +1 -1
- package/dist/openapi/exporter/forms.d.ts +17 -17
- package/dist/openapi/exporter/forms.d.ts.map +1 -1
- package/dist/openapi/exporter/index.d.ts +12 -0
- package/dist/openapi/exporter/index.d.ts.map +1 -0
- package/dist/openapi/exporter/operations.d.ts +29 -29
- package/dist/openapi/exporter/operations.d.ts.map +1 -1
- package/dist/openapi/exporter/presentations.d.ts +16 -17
- package/dist/openapi/exporter/presentations.d.ts.map +1 -1
- package/dist/openapi/exporter/registries.d.ts +13 -14
- package/dist/openapi/exporter/registries.d.ts.map +1 -1
- package/dist/openapi/exporter/workflows.d.ts +23 -24
- package/dist/openapi/exporter/workflows.d.ts.map +1 -1
- package/dist/openapi/exporter.d.ts +24 -23
- package/dist/openapi/exporter.d.ts.map +1 -1
- package/dist/openapi/exporter.test.d.ts +2 -0
- package/dist/openapi/exporter.test.d.ts.map +1 -0
- package/dist/openapi/importer/analyzer.d.ts +14 -0
- package/dist/openapi/importer/analyzer.d.ts.map +1 -0
- package/dist/openapi/importer/events.d.ts +7 -0
- package/dist/openapi/importer/events.d.ts.map +1 -0
- package/dist/openapi/importer/generator.d.ts +8 -0
- package/dist/openapi/importer/generator.d.ts.map +1 -0
- package/dist/openapi/importer/grouping.d.ts +27 -0
- package/dist/openapi/importer/grouping.d.ts.map +1 -0
- package/dist/openapi/importer/index.d.ts +11 -9
- package/dist/openapi/importer/index.d.ts.map +1 -1
- package/dist/openapi/importer/models.d.ts +7 -0
- package/dist/openapi/importer/models.d.ts.map +1 -0
- package/dist/openapi/importer/schemas.d.ts +15 -0
- package/dist/openapi/importer/schemas.d.ts.map +1 -0
- package/dist/openapi/importer.d.ts +6 -0
- package/dist/openapi/importer.d.ts.map +1 -0
- package/dist/openapi/index.d.ts +12 -16
- package/dist/openapi/index.d.ts.map +1 -0
- package/dist/openapi/index.js +2405 -18
- package/dist/openapi/parser/document.d.ts +5 -9
- package/dist/openapi/parser/document.d.ts.map +1 -1
- package/dist/openapi/parser/index.d.ts +6 -0
- package/dist/openapi/parser/index.d.ts.map +1 -0
- package/dist/openapi/parser/operation.d.ts +7 -0
- package/dist/openapi/parser/operation.d.ts.map +1 -0
- package/dist/openapi/parser/parameters.d.ts +11 -0
- package/dist/openapi/parser/parameters.d.ts.map +1 -0
- package/dist/openapi/parser/resolvers.d.ts +21 -0
- package/dist/openapi/parser/resolvers.d.ts.map +1 -0
- package/dist/openapi/parser/utils.d.ts +9 -8
- package/dist/openapi/parser/utils.d.ts.map +1 -1
- package/dist/openapi/parser.d.ts +6 -0
- package/dist/openapi/parser.d.ts.map +1 -0
- package/dist/openapi/schema-converter.d.ts +45 -45
- package/dist/openapi/schema-converter.d.ts.map +1 -1
- package/dist/openapi/schema-generators/index.d.ts +115 -0
- package/dist/openapi/schema-generators/index.d.ts.map +1 -0
- package/dist/openapi/schema-generators.test.d.ts +2 -0
- package/dist/openapi/schema-generators.test.d.ts.map +1 -0
- package/dist/openapi/types.d.ts +198 -198
- package/dist/openapi/types.d.ts.map +1 -1
- package/package.json +53 -20
- package/dist/common/utils.js +0 -103
- package/dist/common/utils.js.map +0 -1
- package/dist/openapi/differ.js +0 -222
- package/dist/openapi/differ.js.map +0 -1
- package/dist/openapi/exporter/data-views.js +0 -47
- package/dist/openapi/exporter/data-views.js.map +0 -1
- package/dist/openapi/exporter/events.js +0 -39
- package/dist/openapi/exporter/events.js.map +0 -1
- package/dist/openapi/exporter/features.js +0 -46
- package/dist/openapi/exporter/features.js.map +0 -1
- package/dist/openapi/exporter/forms.js +0 -49
- package/dist/openapi/exporter/forms.js.map +0 -1
- package/dist/openapi/exporter/index.js +0 -8
- package/dist/openapi/exporter/operations.js +0 -143
- package/dist/openapi/exporter/operations.js.map +0 -1
- package/dist/openapi/exporter/presentations.js +0 -60
- package/dist/openapi/exporter/presentations.js.map +0 -1
- package/dist/openapi/exporter/registries.js +0 -29
- package/dist/openapi/exporter/registries.js.map +0 -1
- package/dist/openapi/exporter/workflows.js +0 -54
- package/dist/openapi/exporter/workflows.js.map +0 -1
- package/dist/openapi/exporter.js +0 -122
- package/dist/openapi/exporter.js.map +0 -1
- package/dist/openapi/importer/analyzer.js +0 -28
- package/dist/openapi/importer/analyzer.js.map +0 -1
- package/dist/openapi/importer/events.js +0 -40
- package/dist/openapi/importer/events.js.map +0 -1
- package/dist/openapi/importer/generator.js +0 -105
- package/dist/openapi/importer/generator.js.map +0 -1
- package/dist/openapi/importer/grouping.js +0 -72
- package/dist/openapi/importer/grouping.js.map +0 -1
- package/dist/openapi/importer/index.js +0 -175
- package/dist/openapi/importer/index.js.map +0 -1
- package/dist/openapi/importer/models.js +0 -22
- package/dist/openapi/importer/models.js.map +0 -1
- package/dist/openapi/importer/schemas.js +0 -60
- package/dist/openapi/importer/schemas.js.map +0 -1
- package/dist/openapi/parser/document.js +0 -95
- package/dist/openapi/parser/document.js.map +0 -1
- package/dist/openapi/parser/index.js +0 -5
- package/dist/openapi/parser/operation.js +0 -59
- package/dist/openapi/parser/operation.js.map +0 -1
- package/dist/openapi/parser/parameters.js +0 -37
- package/dist/openapi/parser/parameters.js.map +0 -1
- package/dist/openapi/parser/resolvers.js +0 -63
- package/dist/openapi/parser/resolvers.js.map +0 -1
- package/dist/openapi/parser/utils.js +0 -48
- package/dist/openapi/parser/utils.js.map +0 -1
- package/dist/openapi/parser.js +0 -6
- package/dist/openapi/schema-converter.js +0 -161
- package/dist/openapi/schema-converter.js.map +0 -1
- package/dist/openapi/schema-generators/index.js +0 -461
- package/dist/openapi/schema-generators/index.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,18 +1,2415 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
1
|
+
// @bun
|
|
2
|
+
// src/common/utils.ts
|
|
3
|
+
function toPascalCase(str) {
|
|
4
|
+
return str.replace(/[-_./\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^./, (c) => c.toUpperCase());
|
|
5
|
+
}
|
|
6
|
+
function toCamelCase(str) {
|
|
7
|
+
const pascal = toPascalCase(str);
|
|
8
|
+
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
9
|
+
}
|
|
10
|
+
function toKebabCase(str) {
|
|
11
|
+
return str.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[\s_./]+/g, "-").toLowerCase();
|
|
12
|
+
}
|
|
13
|
+
function toSnakeCase(str) {
|
|
14
|
+
return str.replace(/([a-z])([A-Z])/g, "$1_$2").replace(/[\s\-./]+/g, "_").toLowerCase();
|
|
15
|
+
}
|
|
16
|
+
function toValidIdentifier(str) {
|
|
17
|
+
let result = str.replace(/[^a-zA-Z0-9_$]/g, "_");
|
|
18
|
+
if (/^[0-9]/.test(result)) {
|
|
19
|
+
result = "_" + result;
|
|
20
|
+
}
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
function toSpecKey(operationId, prefix) {
|
|
24
|
+
const key = toCamelCase(operationId);
|
|
25
|
+
return prefix ? `${prefix}.${key}` : key;
|
|
26
|
+
}
|
|
27
|
+
function toFileName(specName) {
|
|
28
|
+
return toKebabCase(specName.replace(/\./g, "-")) + ".ts";
|
|
29
|
+
}
|
|
30
|
+
function deepEqual(a, b) {
|
|
31
|
+
if (a === b)
|
|
32
|
+
return true;
|
|
33
|
+
if (a === null || b === null)
|
|
34
|
+
return false;
|
|
35
|
+
if (typeof a !== typeof b)
|
|
36
|
+
return false;
|
|
37
|
+
if (typeof a === "object") {
|
|
38
|
+
const aObj = a;
|
|
39
|
+
const bObj = b;
|
|
40
|
+
const aKeys = Object.keys(aObj);
|
|
41
|
+
const bKeys = Object.keys(bObj);
|
|
42
|
+
if (aKeys.length !== bKeys.length)
|
|
43
|
+
return false;
|
|
44
|
+
for (const key of aKeys) {
|
|
45
|
+
if (!bKeys.includes(key))
|
|
46
|
+
return false;
|
|
47
|
+
if (!deepEqual(aObj[key], bObj[key]))
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
function getByPath(obj, path) {
|
|
55
|
+
const parts = path.split(".").filter(Boolean);
|
|
56
|
+
let current = obj;
|
|
57
|
+
for (const part of parts) {
|
|
58
|
+
if (current === null || current === undefined)
|
|
59
|
+
return;
|
|
60
|
+
if (typeof current !== "object")
|
|
61
|
+
return;
|
|
62
|
+
current = current[part];
|
|
63
|
+
}
|
|
64
|
+
return current;
|
|
65
|
+
}
|
|
66
|
+
function extractPathParams(path) {
|
|
67
|
+
const matches = path.match(/\{([^}]+)\}/g) || [];
|
|
68
|
+
return matches.map((m) => m.slice(1, -1));
|
|
69
|
+
}
|
|
70
|
+
function normalizePath(path) {
|
|
71
|
+
let normalized = path.replace(/^\/+|\/+$/g, "");
|
|
72
|
+
normalized = normalized.replace(/\/+/g, "/");
|
|
73
|
+
return "/" + normalized;
|
|
74
|
+
}
|
|
75
|
+
// src/openapi/parser/utils.ts
|
|
76
|
+
import { parse as parseYaml } from "yaml";
|
|
77
|
+
var HTTP_METHODS = [
|
|
78
|
+
"get",
|
|
79
|
+
"post",
|
|
80
|
+
"put",
|
|
81
|
+
"delete",
|
|
82
|
+
"patch",
|
|
83
|
+
"head",
|
|
84
|
+
"options",
|
|
85
|
+
"trace"
|
|
86
|
+
];
|
|
87
|
+
function parseOpenApiString(content, format = "json") {
|
|
88
|
+
if (format === "yaml") {
|
|
89
|
+
return parseYaml(content);
|
|
90
|
+
}
|
|
91
|
+
return JSON.parse(content);
|
|
92
|
+
}
|
|
93
|
+
function detectFormat(content) {
|
|
94
|
+
const trimmed = content.trim();
|
|
95
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
96
|
+
return "json";
|
|
97
|
+
}
|
|
98
|
+
return "yaml";
|
|
99
|
+
}
|
|
100
|
+
function detectVersion(doc) {
|
|
101
|
+
const version = doc.openapi;
|
|
102
|
+
if (version.startsWith("3.1")) {
|
|
103
|
+
return "3.1";
|
|
104
|
+
}
|
|
105
|
+
return "3.0";
|
|
106
|
+
}
|
|
107
|
+
function generateOperationId(method, path) {
|
|
108
|
+
const pathParts = path.split("/").filter(Boolean).map((part) => {
|
|
109
|
+
if (part.startsWith("{") && part.endsWith("}")) {
|
|
110
|
+
return "By" + part.slice(1, -1).charAt(0).toUpperCase() + part.slice(2, -1);
|
|
111
|
+
}
|
|
112
|
+
return part.charAt(0).toUpperCase() + part.slice(1);
|
|
113
|
+
});
|
|
114
|
+
return method + pathParts.join("");
|
|
115
|
+
}
|
|
116
|
+
// src/openapi/parser/resolvers.ts
|
|
117
|
+
function isReference(obj) {
|
|
118
|
+
return typeof obj === "object" && obj !== null && "$ref" in obj;
|
|
119
|
+
}
|
|
120
|
+
function resolveRef(doc, ref) {
|
|
121
|
+
if (!ref.startsWith("#/")) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const path = ref.slice(2).split("/");
|
|
125
|
+
let current = doc;
|
|
126
|
+
for (const part of path) {
|
|
127
|
+
if (current === null || current === undefined)
|
|
128
|
+
return;
|
|
129
|
+
if (typeof current !== "object")
|
|
130
|
+
return;
|
|
131
|
+
current = current[part];
|
|
132
|
+
}
|
|
133
|
+
return current;
|
|
134
|
+
}
|
|
135
|
+
function dereferenceSchema(doc, schema, seen = new Set) {
|
|
136
|
+
if (!schema)
|
|
137
|
+
return;
|
|
138
|
+
if (isReference(schema)) {
|
|
139
|
+
if (seen.has(schema.$ref)) {
|
|
140
|
+
return schema;
|
|
141
|
+
}
|
|
142
|
+
const newSeen = new Set(seen);
|
|
143
|
+
newSeen.add(schema.$ref);
|
|
144
|
+
const resolved = resolveRef(doc, schema.$ref);
|
|
145
|
+
if (!resolved)
|
|
146
|
+
return schema;
|
|
147
|
+
const dereferenced = dereferenceSchema(doc, resolved, newSeen);
|
|
148
|
+
if (!dereferenced)
|
|
149
|
+
return schema;
|
|
150
|
+
const refParts = schema.$ref.split("/");
|
|
151
|
+
const typeName = refParts[refParts.length - 1];
|
|
152
|
+
return {
|
|
153
|
+
...dereferenced,
|
|
154
|
+
_originalRef: schema.$ref,
|
|
155
|
+
_originalTypeName: typeName
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
const schemaObj = { ...schema };
|
|
159
|
+
if (schemaObj.properties) {
|
|
160
|
+
const props = schemaObj.properties;
|
|
161
|
+
const newProps = {};
|
|
162
|
+
for (const [key, prop] of Object.entries(props)) {
|
|
163
|
+
newProps[key] = dereferenceSchema(doc, prop, seen) ?? prop;
|
|
164
|
+
}
|
|
165
|
+
schemaObj.properties = newProps;
|
|
166
|
+
}
|
|
167
|
+
if (schemaObj.items) {
|
|
168
|
+
schemaObj.items = dereferenceSchema(doc, schemaObj.items, seen);
|
|
169
|
+
}
|
|
170
|
+
const combinators = ["allOf", "anyOf", "oneOf"];
|
|
171
|
+
for (const comb of combinators) {
|
|
172
|
+
if (Array.isArray(schemaObj[comb])) {
|
|
173
|
+
schemaObj[comb] = schemaObj[comb].map((s) => dereferenceSchema(doc, s, seen) ?? s);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return schemaObj;
|
|
177
|
+
}
|
|
178
|
+
// src/openapi/parser/parameters.ts
|
|
179
|
+
function parseParameters(doc, params) {
|
|
180
|
+
const result = {
|
|
181
|
+
path: [],
|
|
182
|
+
query: [],
|
|
183
|
+
header: [],
|
|
184
|
+
cookie: []
|
|
185
|
+
};
|
|
186
|
+
if (!params)
|
|
187
|
+
return result;
|
|
188
|
+
for (const param of params) {
|
|
189
|
+
let resolved;
|
|
190
|
+
if (isReference(param)) {
|
|
191
|
+
const ref = resolveRef(doc, param.$ref);
|
|
192
|
+
if (!ref)
|
|
193
|
+
continue;
|
|
194
|
+
resolved = ref;
|
|
195
|
+
} else {
|
|
196
|
+
resolved = param;
|
|
197
|
+
}
|
|
198
|
+
const parsed = {
|
|
199
|
+
name: resolved.name,
|
|
200
|
+
in: resolved.in,
|
|
201
|
+
required: resolved.required ?? resolved.in === "path",
|
|
202
|
+
description: resolved.description,
|
|
203
|
+
schema: dereferenceSchema(doc, resolved.schema),
|
|
204
|
+
deprecated: resolved.deprecated ?? false
|
|
205
|
+
};
|
|
206
|
+
result[resolved.in]?.push(parsed);
|
|
207
|
+
}
|
|
208
|
+
return result;
|
|
209
|
+
}
|
|
210
|
+
// src/openapi/parser/operation.ts
|
|
211
|
+
function parseOperation(doc, method, path, operation, pathParams) {
|
|
212
|
+
const allParams = [...pathParams ?? [], ...operation.parameters ?? []];
|
|
213
|
+
const params = parseParameters(doc, allParams);
|
|
214
|
+
let requestBody;
|
|
215
|
+
if (operation.requestBody) {
|
|
216
|
+
const body = isReference(operation.requestBody) ? resolveRef(doc, operation.requestBody.$ref) : operation.requestBody;
|
|
217
|
+
if (body) {
|
|
218
|
+
const contentType = Object.keys(body.content ?? {})[0] ?? "application/json";
|
|
219
|
+
const content = body.content?.[contentType];
|
|
220
|
+
if (content?.schema) {
|
|
221
|
+
requestBody = {
|
|
222
|
+
required: body.required ?? false,
|
|
223
|
+
schema: dereferenceSchema(doc, content.schema) ?? {},
|
|
224
|
+
contentType
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const responses = {};
|
|
230
|
+
for (const [status, response] of Object.entries(operation.responses ?? {})) {
|
|
231
|
+
const resolved = isReference(response) ? resolveRef(doc, response.$ref) : response;
|
|
232
|
+
if (resolved) {
|
|
233
|
+
const contentType = Object.keys(resolved.content ?? {})[0];
|
|
234
|
+
const content = contentType ? resolved.content?.[contentType] : undefined;
|
|
235
|
+
responses[status] = {
|
|
236
|
+
description: resolved.description,
|
|
237
|
+
schema: content?.schema ? dereferenceSchema(doc, content.schema) : undefined,
|
|
238
|
+
contentType
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const contractSpecMeta = operation?.["x-contractspec"];
|
|
243
|
+
return {
|
|
244
|
+
operationId: operation.operationId ?? generateOperationId(method, path),
|
|
245
|
+
method,
|
|
246
|
+
path,
|
|
247
|
+
summary: operation.summary,
|
|
248
|
+
description: operation.description,
|
|
249
|
+
tags: operation.tags ?? [],
|
|
250
|
+
pathParams: params.path,
|
|
251
|
+
queryParams: params.query,
|
|
252
|
+
headerParams: params.header,
|
|
253
|
+
cookieParams: params.cookie,
|
|
254
|
+
requestBody,
|
|
255
|
+
responses,
|
|
256
|
+
deprecated: operation.deprecated ?? false,
|
|
257
|
+
security: operation.security,
|
|
258
|
+
contractSpecMeta
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
// src/openapi/parser/document.ts
|
|
262
|
+
function parseOpenApiDocument(doc, _options = {}) {
|
|
263
|
+
const version = detectVersion(doc);
|
|
264
|
+
const warnings = [];
|
|
265
|
+
const operations = [];
|
|
266
|
+
for (const [path, pathItem] of Object.entries(doc.paths ?? {})) {
|
|
267
|
+
if (!pathItem)
|
|
268
|
+
continue;
|
|
269
|
+
const pathParams = pathItem.parameters;
|
|
270
|
+
for (const method of HTTP_METHODS) {
|
|
271
|
+
const operation = pathItem[method];
|
|
272
|
+
if (operation) {
|
|
273
|
+
try {
|
|
274
|
+
operations.push(parseOperation(doc, method, path, operation, pathParams));
|
|
275
|
+
} catch (error) {
|
|
276
|
+
warnings.push(`Failed to parse ${method.toUpperCase()} ${path}: ${error}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const schemas = {};
|
|
282
|
+
const components = doc.components;
|
|
283
|
+
if (components?.schemas) {
|
|
284
|
+
for (const [name, schema] of Object.entries(components.schemas)) {
|
|
285
|
+
schemas[name] = schema;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const servers = (doc.servers ?? []).map((s) => ({
|
|
289
|
+
url: s.url,
|
|
290
|
+
description: s.description,
|
|
291
|
+
variables: s.variables
|
|
292
|
+
}));
|
|
293
|
+
const events = [];
|
|
294
|
+
if ("webhooks" in doc && doc.webhooks) {
|
|
295
|
+
for (const [name, pathItem] of Object.entries(doc.webhooks)) {
|
|
296
|
+
if (typeof pathItem !== "object" || !pathItem)
|
|
297
|
+
continue;
|
|
298
|
+
const operation = pathItem["post"];
|
|
299
|
+
if (operation && operation.requestBody) {
|
|
300
|
+
if ("$ref" in operation.requestBody) {
|
|
301
|
+
throw new Error(`'$ref' isn't supported`);
|
|
302
|
+
}
|
|
303
|
+
const content = operation.requestBody.content?.["application/json"];
|
|
304
|
+
if (content?.schema) {
|
|
305
|
+
events.push({
|
|
306
|
+
name,
|
|
307
|
+
description: operation.summary || operation.description,
|
|
308
|
+
payload: content.schema
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
document: doc,
|
|
316
|
+
version,
|
|
317
|
+
info: {
|
|
318
|
+
title: doc.info.title,
|
|
319
|
+
version: doc.info.version,
|
|
320
|
+
description: doc.info.description
|
|
321
|
+
},
|
|
322
|
+
operations,
|
|
323
|
+
schemas,
|
|
324
|
+
servers,
|
|
325
|
+
warnings,
|
|
326
|
+
events
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
async function parseOpenApi(source, options = {}) {
|
|
330
|
+
const {
|
|
331
|
+
fetch: fetchFn = globalThis.fetch,
|
|
332
|
+
readFile,
|
|
333
|
+
timeout = 30000
|
|
334
|
+
} = options;
|
|
335
|
+
let content;
|
|
336
|
+
let format;
|
|
337
|
+
if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
338
|
+
const controller = new AbortController;
|
|
339
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
340
|
+
try {
|
|
341
|
+
const response = await fetchFn(source, { signal: controller.signal });
|
|
342
|
+
if (!response.ok) {
|
|
343
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
344
|
+
}
|
|
345
|
+
content = await response.text();
|
|
346
|
+
} finally {
|
|
347
|
+
clearTimeout(timeoutId);
|
|
348
|
+
}
|
|
349
|
+
if (source.endsWith(".yaml") || source.endsWith(".yml")) {
|
|
350
|
+
format = "yaml";
|
|
351
|
+
} else if (source.endsWith(".json")) {
|
|
352
|
+
format = "json";
|
|
353
|
+
} else {
|
|
354
|
+
format = detectFormat(content);
|
|
355
|
+
}
|
|
356
|
+
} else {
|
|
357
|
+
if (!readFile) {
|
|
358
|
+
throw new Error("readFile adapter required for file paths");
|
|
359
|
+
}
|
|
360
|
+
content = await readFile(source);
|
|
361
|
+
if (source.endsWith(".yaml") || source.endsWith(".yml")) {
|
|
362
|
+
format = "yaml";
|
|
363
|
+
} else if (source.endsWith(".json")) {
|
|
364
|
+
format = "json";
|
|
365
|
+
} else {
|
|
366
|
+
format = detectFormat(content);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
const doc = parseOpenApiString(content, format);
|
|
370
|
+
return parseOpenApiDocument(doc, options);
|
|
371
|
+
}
|
|
372
|
+
// src/openapi/exporter/operations.ts
|
|
373
|
+
import { z } from "zod";
|
|
374
|
+
import { compareVersions } from "compare-versions";
|
|
375
|
+
function toOperationId(name, version) {
|
|
376
|
+
return `${name.replace(/\./g, "_")}_v${version.replace(/\./g, "_")}`;
|
|
377
|
+
}
|
|
378
|
+
function toSchemaName(prefix, name, version) {
|
|
379
|
+
return `${prefix}_${toOperationId(name, version)}`;
|
|
380
|
+
}
|
|
381
|
+
function toHttpMethod(kind, override) {
|
|
382
|
+
const method = override ?? (kind === "query" ? "GET" : "POST");
|
|
383
|
+
return method.toLowerCase();
|
|
384
|
+
}
|
|
385
|
+
function defaultRestPath(name, version) {
|
|
386
|
+
return `/${name.replace(/\./g, "/")}/v${version}`;
|
|
387
|
+
}
|
|
388
|
+
function toRestPath(spec) {
|
|
389
|
+
const path = spec.transport?.rest?.path ?? defaultRestPath(spec.meta.key, spec.meta.version);
|
|
390
|
+
return path.startsWith("/") ? path : `/${path}`;
|
|
391
|
+
}
|
|
392
|
+
function schemaModelToJsonSchema(schema) {
|
|
393
|
+
if (!schema)
|
|
394
|
+
return null;
|
|
395
|
+
return z.toJSONSchema(schema.getZod());
|
|
396
|
+
}
|
|
397
|
+
function jsonSchemaForSpec(spec) {
|
|
398
|
+
return {
|
|
399
|
+
input: schemaModelToJsonSchema(spec.io.input),
|
|
400
|
+
output: schemaModelToJsonSchema(spec.io.output),
|
|
401
|
+
meta: {
|
|
402
|
+
key: spec.meta.key,
|
|
403
|
+
version: spec.meta.version,
|
|
404
|
+
kind: spec.meta.kind,
|
|
405
|
+
description: spec.meta.description,
|
|
406
|
+
tags: spec.meta.tags ?? [],
|
|
407
|
+
stability: spec.meta.stability ?? "stable"
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
function exportOperations(registry) {
|
|
412
|
+
const specs = Array.from(registry.list().values()).filter((s) => s.meta.kind === "command" || s.meta.kind === "query").sort((a, b) => {
|
|
413
|
+
const byName = a.meta.key.localeCompare(b.meta.key);
|
|
414
|
+
return byName !== 0 ? byName : compareVersions(a.meta.version, b.meta.version);
|
|
415
|
+
});
|
|
416
|
+
const paths = {};
|
|
417
|
+
const schemas = {};
|
|
418
|
+
for (const spec of specs) {
|
|
419
|
+
const schema = jsonSchemaForSpec(spec);
|
|
420
|
+
const method = toHttpMethod(spec.meta.kind, spec.transport?.rest?.method);
|
|
421
|
+
const path = toRestPath(spec);
|
|
422
|
+
const operationId = toOperationId(spec.meta.key, spec.meta.version);
|
|
423
|
+
const pathItem = paths[path] ??= {};
|
|
424
|
+
const op = {
|
|
425
|
+
operationId,
|
|
426
|
+
summary: spec.meta.description ?? spec.meta.key,
|
|
427
|
+
description: spec.meta.description,
|
|
428
|
+
tags: spec.meta.tags ?? [],
|
|
429
|
+
"x-contractspec": {
|
|
430
|
+
name: spec.meta.key,
|
|
431
|
+
version: spec.meta.version,
|
|
432
|
+
kind: spec.meta.kind
|
|
433
|
+
},
|
|
434
|
+
responses: {}
|
|
435
|
+
};
|
|
436
|
+
if (schema.input) {
|
|
437
|
+
const inputName = toSchemaName("Input", spec.meta.key, spec.meta.version);
|
|
438
|
+
schemas[inputName] = schema.input;
|
|
439
|
+
op["requestBody"] = {
|
|
440
|
+
required: true,
|
|
441
|
+
content: {
|
|
442
|
+
"application/json": {
|
|
443
|
+
schema: { $ref: `#/components/schemas/${inputName}` }
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
const responses = {};
|
|
449
|
+
if (schema.output) {
|
|
450
|
+
const outputName = toSchemaName("Output", spec.meta.key, spec.meta.version);
|
|
451
|
+
schemas[outputName] = schema.output;
|
|
452
|
+
responses["200"] = {
|
|
453
|
+
description: "OK",
|
|
454
|
+
content: {
|
|
455
|
+
"application/json": {
|
|
456
|
+
schema: { $ref: `#/components/schemas/${outputName}` }
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
} else {
|
|
461
|
+
responses["200"] = { description: "OK" };
|
|
462
|
+
}
|
|
463
|
+
op["responses"] = responses;
|
|
464
|
+
pathItem[method] = op;
|
|
465
|
+
}
|
|
466
|
+
return { paths, schemas };
|
|
467
|
+
}
|
|
468
|
+
function generateOperationsRegistry(registry) {
|
|
469
|
+
const specs = Array.from(registry.list().values());
|
|
470
|
+
const imports = new Set;
|
|
471
|
+
const registrations = [];
|
|
472
|
+
for (const spec of specs) {
|
|
473
|
+
const specVarName = spec.meta.key.replace(/\./g, "_") + `_v${spec.meta.version.replace(/\./g, "_")}`;
|
|
474
|
+
imports.add(`import { ${specVarName} } from './${spec.meta.key.split(".")[0]}';`);
|
|
475
|
+
registrations.push(` .register(${specVarName})`);
|
|
476
|
+
}
|
|
477
|
+
const code = `/**
|
|
478
|
+
* Auto-generated operations registry.
|
|
479
|
+
* DO NOT EDIT - This file is generated by ContractSpec exporter.
|
|
480
|
+
*/
|
|
481
|
+
import { OperationSpecRegistry } from '@contractspec/lib.contracts';
|
|
482
|
+
|
|
483
|
+
${Array.from(imports).join(`
|
|
484
|
+
`)}
|
|
485
|
+
|
|
486
|
+
export const operationsRegistry = new OperationSpecRegistry()
|
|
487
|
+
${registrations.join(`
|
|
488
|
+
`)};
|
|
489
|
+
`;
|
|
490
|
+
return {
|
|
491
|
+
code,
|
|
492
|
+
fileName: "operations-registry.ts"
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// src/openapi/exporter/events.ts
|
|
497
|
+
import { z as z2 } from "zod";
|
|
498
|
+
function exportEvents(events) {
|
|
499
|
+
return events.map((event) => ({
|
|
500
|
+
name: event.meta.key,
|
|
501
|
+
version: event.meta.version,
|
|
502
|
+
description: event.meta.description,
|
|
503
|
+
payload: event.payload ? z2.toJSONSchema(event.payload.getZod()) : null,
|
|
504
|
+
pii: event.pii
|
|
505
|
+
}));
|
|
506
|
+
}
|
|
507
|
+
function generateEventsExports(events) {
|
|
508
|
+
const eventExports = [];
|
|
509
|
+
for (const event of events) {
|
|
510
|
+
const eventVarName = event.meta.key.replace(/\./g, "_") + `_v${event.meta.version}`;
|
|
511
|
+
eventExports.push(`export { ${eventVarName} } from './${event.meta.key.split(".")[0]}';`);
|
|
512
|
+
}
|
|
513
|
+
const code = `/**
|
|
514
|
+
* Auto-generated events exports.
|
|
515
|
+
* DO NOT EDIT - This file is generated by ContractSpec exporter.
|
|
516
|
+
*/
|
|
517
|
+
|
|
518
|
+
${eventExports.join(`
|
|
519
|
+
`)}
|
|
520
|
+
`;
|
|
521
|
+
return {
|
|
522
|
+
code,
|
|
523
|
+
fileName: "events-exports.ts"
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// src/openapi/exporter/features.ts
|
|
528
|
+
function exportFeatures(registry) {
|
|
529
|
+
return registry.list().map((feature) => ({
|
|
530
|
+
key: feature.meta.key,
|
|
531
|
+
description: feature.meta.description,
|
|
532
|
+
owners: feature.meta.owners,
|
|
533
|
+
stability: feature.meta.stability,
|
|
534
|
+
operations: feature.operations,
|
|
535
|
+
events: feature.events,
|
|
536
|
+
presentations: feature.presentations
|
|
537
|
+
}));
|
|
538
|
+
}
|
|
539
|
+
function generateFeaturesRegistry(registry) {
|
|
540
|
+
const features = registry.list();
|
|
541
|
+
const imports = new Set;
|
|
542
|
+
const registrations = [];
|
|
543
|
+
for (const feature of features) {
|
|
544
|
+
const featureVarName = feature.meta.key.replace(/-/g, "_");
|
|
545
|
+
imports.add(`import { ${featureVarName} } from './${feature.meta.key}';`);
|
|
546
|
+
registrations.push(` .register(${featureVarName})`);
|
|
547
|
+
}
|
|
548
|
+
const code = `/**
|
|
549
|
+
* Auto-generated features registry.
|
|
550
|
+
* DO NOT EDIT - This file is generated by ContractSpec exporter.
|
|
551
|
+
*/
|
|
552
|
+
import { FeatureRegistry } from '@contractspec/lib.contracts';
|
|
553
|
+
|
|
554
|
+
${Array.from(imports).join(`
|
|
555
|
+
`)}
|
|
556
|
+
|
|
557
|
+
export const featuresRegistry = new FeatureRegistry()
|
|
558
|
+
${registrations.join(`
|
|
559
|
+
`)};
|
|
560
|
+
`;
|
|
561
|
+
return {
|
|
562
|
+
code,
|
|
563
|
+
fileName: "features-registry.ts"
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// src/openapi/exporter/presentations.ts
|
|
568
|
+
function exportPresentations(registry) {
|
|
569
|
+
return registry.list().map((pres) => ({
|
|
570
|
+
name: pres.meta.key,
|
|
571
|
+
version: pres.meta.version,
|
|
572
|
+
description: pres.meta.description,
|
|
573
|
+
stability: pres.meta.stability,
|
|
574
|
+
sourceType: pres.source.type,
|
|
575
|
+
targets: pres.targets,
|
|
576
|
+
tags: pres.meta.tags
|
|
577
|
+
}));
|
|
578
|
+
}
|
|
579
|
+
function exportPresentationsFromArray(descriptors) {
|
|
580
|
+
return descriptors.map((desc) => ({
|
|
581
|
+
name: desc.meta.key,
|
|
582
|
+
version: desc.meta.version,
|
|
583
|
+
description: desc.meta.description,
|
|
584
|
+
stability: desc.meta.stability,
|
|
585
|
+
sourceType: desc.source.type,
|
|
586
|
+
targets: desc.targets,
|
|
587
|
+
tags: desc.meta.tags
|
|
588
|
+
}));
|
|
589
|
+
}
|
|
590
|
+
function generatePresentationsRegistry(registry) {
|
|
591
|
+
const presentations = registry.list();
|
|
592
|
+
const imports = new Set;
|
|
593
|
+
const registrations = [];
|
|
594
|
+
for (const pres of presentations) {
|
|
595
|
+
const presVarName = pres.meta.key.replace(/\./g, "_") + `_v${pres.meta.version}`;
|
|
596
|
+
imports.add(`import { ${presVarName} } from './${pres.meta.key.split(".")[0]}';`);
|
|
597
|
+
registrations.push(` .register(${presVarName})`);
|
|
598
|
+
}
|
|
599
|
+
const code = `/**
|
|
600
|
+
* Auto-generated presentations registry.
|
|
601
|
+
* DO NOT EDIT - This file is generated by ContractSpec exporter.
|
|
602
|
+
*/
|
|
603
|
+
import { PresentationRegistry } from '@contractspec/lib.contracts';
|
|
604
|
+
|
|
605
|
+
${Array.from(imports).join(`
|
|
606
|
+
`)}
|
|
607
|
+
|
|
608
|
+
export const presentationsRegistry = new PresentationRegistry()
|
|
609
|
+
${registrations.join(`
|
|
610
|
+
`)};
|
|
611
|
+
`;
|
|
612
|
+
return {
|
|
613
|
+
code,
|
|
614
|
+
fileName: "presentations-registry.ts"
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// src/openapi/exporter/forms.ts
|
|
619
|
+
import { z as z3 } from "zod";
|
|
620
|
+
function exportForms(registry) {
|
|
621
|
+
return registry.list().map((form) => ({
|
|
622
|
+
key: form.meta.key,
|
|
623
|
+
version: form.meta.version,
|
|
624
|
+
description: form.meta.description,
|
|
625
|
+
stability: form.meta.stability,
|
|
626
|
+
owners: form.meta.owners,
|
|
627
|
+
fields: form.fields,
|
|
628
|
+
model: form.model ? z3.toJSONSchema(form.model.getZod()) : null,
|
|
629
|
+
actions: form.actions
|
|
630
|
+
}));
|
|
631
|
+
}
|
|
632
|
+
function generateFormsRegistry(registry) {
|
|
633
|
+
const forms = registry.list();
|
|
634
|
+
const imports = new Set;
|
|
635
|
+
const registrations = [];
|
|
636
|
+
for (const form of forms) {
|
|
637
|
+
const formVarName = form.meta.key.replace(/-/g, "_") + `_v${form.meta.version}`;
|
|
638
|
+
imports.add(`import { ${formVarName} } from './${form.meta.key}';`);
|
|
639
|
+
registrations.push(` .register(${formVarName})`);
|
|
640
|
+
}
|
|
641
|
+
const code = `/**
|
|
642
|
+
* Auto-generated forms registry.
|
|
643
|
+
* DO NOT EDIT - This file is generated by ContractSpec exporter.
|
|
644
|
+
*/
|
|
645
|
+
import { FormRegistry } from '@contractspec/lib.contracts';
|
|
646
|
+
|
|
647
|
+
${Array.from(imports).join(`
|
|
648
|
+
`)}
|
|
649
|
+
|
|
650
|
+
export const formsRegistry = new FormRegistry()
|
|
651
|
+
${registrations.join(`
|
|
652
|
+
`)};
|
|
653
|
+
`;
|
|
654
|
+
return {
|
|
655
|
+
code,
|
|
656
|
+
fileName: "forms-registry.ts"
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// src/openapi/exporter/data-views.ts
|
|
661
|
+
function exportDataViews(registry) {
|
|
662
|
+
return registry.list().map((dv) => ({
|
|
663
|
+
name: dv.meta.key,
|
|
664
|
+
version: dv.meta.version,
|
|
665
|
+
description: dv.meta.description,
|
|
666
|
+
stability: dv.meta.stability,
|
|
667
|
+
entity: dv.meta.entity,
|
|
668
|
+
kind: dv.view.kind,
|
|
669
|
+
source: dv.source,
|
|
670
|
+
fields: dv.view.fields
|
|
671
|
+
}));
|
|
672
|
+
}
|
|
673
|
+
function generateDataViewsRegistry(registry) {
|
|
674
|
+
const dataViews = registry.list();
|
|
675
|
+
const imports = new Set;
|
|
676
|
+
const registrations = [];
|
|
677
|
+
for (const dv of dataViews) {
|
|
678
|
+
const dvVarName = dv.meta.key.replace(/\./g, "_") + `_v${dv.meta.version}`;
|
|
679
|
+
imports.add(`import { ${dvVarName} } from './${dv.meta.key.split(".")[0]}';`);
|
|
680
|
+
registrations.push(` .register(${dvVarName})`);
|
|
681
|
+
}
|
|
682
|
+
const code = `/**
|
|
683
|
+
* Auto-generated data views registry.
|
|
684
|
+
* DO NOT EDIT - This file is generated by ContractSpec exporter.
|
|
685
|
+
*/
|
|
686
|
+
import { DataViewRegistry } from '@contractspec/lib.contracts';
|
|
687
|
+
|
|
688
|
+
${Array.from(imports).join(`
|
|
689
|
+
`)}
|
|
690
|
+
|
|
691
|
+
export const dataViewsRegistry = new DataViewRegistry()
|
|
692
|
+
${registrations.join(`
|
|
693
|
+
`)};
|
|
694
|
+
`;
|
|
695
|
+
return {
|
|
696
|
+
code,
|
|
697
|
+
fileName: "dataviews-registry.ts"
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// src/openapi/exporter/workflows.ts
|
|
702
|
+
function exportWorkflows(registry) {
|
|
703
|
+
return registry.list().map((wf) => ({
|
|
704
|
+
name: wf.meta.key,
|
|
705
|
+
version: wf.meta.version,
|
|
706
|
+
description: wf.meta.description,
|
|
707
|
+
stability: wf.meta.stability,
|
|
708
|
+
owners: wf.meta.owners,
|
|
709
|
+
steps: wf.definition.steps.map((s) => ({
|
|
710
|
+
id: s.id,
|
|
711
|
+
type: s.type,
|
|
712
|
+
label: s.label
|
|
713
|
+
})),
|
|
714
|
+
transitions: wf.definition.transitions.map((t) => ({
|
|
715
|
+
from: t.from,
|
|
716
|
+
to: t.to,
|
|
717
|
+
label: t.label
|
|
718
|
+
}))
|
|
719
|
+
}));
|
|
720
|
+
}
|
|
721
|
+
function generateWorkflowsRegistry(registry) {
|
|
722
|
+
const workflows = registry.list();
|
|
723
|
+
const imports = new Set;
|
|
724
|
+
const registrations = [];
|
|
725
|
+
for (const wf of workflows) {
|
|
726
|
+
const wfVarName = wf.meta.key.replace(/\./g, "_") + `_v${wf.meta.version}`;
|
|
727
|
+
imports.add(`import { ${wfVarName} } from './${wf.meta.key.split(".")[0]}';`);
|
|
728
|
+
registrations.push(` .register(${wfVarName})`);
|
|
729
|
+
}
|
|
730
|
+
const code = `/**
|
|
731
|
+
* Auto-generated workflows registry.
|
|
732
|
+
* DO NOT EDIT - This file is generated by ContractSpec exporter.
|
|
733
|
+
*/
|
|
734
|
+
import { WorkflowRegistry } from '@contractspec/lib.contracts';
|
|
735
|
+
|
|
736
|
+
${Array.from(imports).join(`
|
|
737
|
+
`)}
|
|
738
|
+
|
|
739
|
+
export const workflowsRegistry = new WorkflowRegistry()
|
|
740
|
+
${registrations.join(`
|
|
741
|
+
`)};
|
|
742
|
+
`;
|
|
743
|
+
return {
|
|
744
|
+
code,
|
|
745
|
+
fileName: "workflows-registry.ts"
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// src/openapi/exporter/registries.ts
|
|
750
|
+
function generateRegistryIndex(options = {}) {
|
|
751
|
+
const {
|
|
752
|
+
operations = true,
|
|
753
|
+
events = true,
|
|
754
|
+
features = true,
|
|
755
|
+
presentations = true,
|
|
756
|
+
forms = true,
|
|
757
|
+
dataViews = true,
|
|
758
|
+
workflows = true
|
|
759
|
+
} = options;
|
|
760
|
+
const exports = [];
|
|
761
|
+
if (operations) {
|
|
762
|
+
exports.push("export * from './operations-registry';");
|
|
763
|
+
}
|
|
764
|
+
if (events) {
|
|
765
|
+
exports.push("export * from './events-exports';");
|
|
766
|
+
}
|
|
767
|
+
if (features) {
|
|
768
|
+
exports.push("export * from './features-registry';");
|
|
769
|
+
}
|
|
770
|
+
if (presentations) {
|
|
771
|
+
exports.push("export * from './presentations-registry';");
|
|
772
|
+
}
|
|
773
|
+
if (forms) {
|
|
774
|
+
exports.push("export * from './forms-registry';");
|
|
775
|
+
}
|
|
776
|
+
if (dataViews) {
|
|
777
|
+
exports.push("export * from './dataviews-registry';");
|
|
778
|
+
}
|
|
779
|
+
if (workflows) {
|
|
780
|
+
exports.push("export * from './workflows-registry';");
|
|
781
|
+
}
|
|
782
|
+
const code = `/**
|
|
783
|
+
* Auto-generated registry index.
|
|
784
|
+
* DO NOT EDIT - This file is generated by ContractSpec exporter.
|
|
785
|
+
*/
|
|
786
|
+
|
|
787
|
+
${exports.join(`
|
|
788
|
+
`)}
|
|
789
|
+
`;
|
|
790
|
+
return {
|
|
791
|
+
code,
|
|
792
|
+
fileName: "index.ts"
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// src/openapi/exporter.ts
|
|
797
|
+
function openApiForRegistry(registry, options = {}) {
|
|
798
|
+
const { paths, schemas } = exportOperations(registry);
|
|
799
|
+
return {
|
|
800
|
+
openapi: "3.1.0",
|
|
801
|
+
info: {
|
|
802
|
+
title: options.title ?? "ContractSpec API",
|
|
803
|
+
version: options.version ?? "0.0.0",
|
|
804
|
+
...options.description ? { description: options.description } : {}
|
|
805
|
+
},
|
|
806
|
+
...options.servers ? { servers: options.servers } : {},
|
|
807
|
+
paths,
|
|
808
|
+
components: { schemas }
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
function exportContractSpec(registries, options = {}) {
|
|
812
|
+
const {
|
|
813
|
+
operations: includeOps = true,
|
|
814
|
+
events: includeEvents = true,
|
|
815
|
+
features: includeFeatures = true,
|
|
816
|
+
presentations: includePresentations = true,
|
|
817
|
+
forms: includeForms = true,
|
|
818
|
+
dataViews: includeDataViews = true,
|
|
819
|
+
workflows: includeWorkflows = true,
|
|
820
|
+
generateRegistries = true
|
|
821
|
+
} = options;
|
|
822
|
+
let paths = {};
|
|
823
|
+
let schemas = {};
|
|
824
|
+
if (includeOps && registries.operations) {
|
|
825
|
+
const opResult = exportOperations(registries.operations);
|
|
826
|
+
paths = opResult.paths;
|
|
827
|
+
schemas = opResult.schemas;
|
|
828
|
+
}
|
|
829
|
+
const doc = {
|
|
830
|
+
openapi: "3.1.0",
|
|
831
|
+
info: {
|
|
832
|
+
title: options.title ?? "ContractSpec API",
|
|
833
|
+
version: options.version ?? "0.0.0",
|
|
834
|
+
...options.description ? { description: options.description } : {}
|
|
835
|
+
},
|
|
836
|
+
...options.servers ? { servers: options.servers } : {},
|
|
837
|
+
paths,
|
|
838
|
+
components: { schemas }
|
|
839
|
+
};
|
|
840
|
+
if (includeEvents && registries.events?.length) {
|
|
841
|
+
doc["x-contractspec-events"] = exportEvents(registries.events);
|
|
842
|
+
}
|
|
843
|
+
if (includeFeatures && registries.features) {
|
|
844
|
+
doc["x-contractspec-features"] = exportFeatures(registries.features);
|
|
845
|
+
}
|
|
846
|
+
if (includePresentations && registries.presentations) {
|
|
847
|
+
doc["x-contractspec-presentations"] = exportPresentations(registries.presentations);
|
|
848
|
+
}
|
|
849
|
+
if (includeForms && registries.forms) {
|
|
850
|
+
doc["x-contractspec-forms"] = exportForms(registries.forms);
|
|
851
|
+
}
|
|
852
|
+
if (includeDataViews && registries.dataViews) {
|
|
853
|
+
doc["x-contractspec-dataviews"] = exportDataViews(registries.dataViews);
|
|
854
|
+
}
|
|
855
|
+
if (includeWorkflows && registries.workflows) {
|
|
856
|
+
doc["x-contractspec-workflows"] = exportWorkflows(registries.workflows);
|
|
857
|
+
}
|
|
858
|
+
const result = {
|
|
859
|
+
openApi: doc
|
|
860
|
+
};
|
|
861
|
+
if (generateRegistries) {
|
|
862
|
+
result.registries = {};
|
|
863
|
+
if (includeOps && registries.operations) {
|
|
864
|
+
result.registries.operations = generateOperationsRegistry(registries.operations);
|
|
865
|
+
}
|
|
866
|
+
if (includeEvents && registries.events?.length) {
|
|
867
|
+
result.registries.events = generateEventsExports(registries.events);
|
|
868
|
+
}
|
|
869
|
+
if (includeFeatures && registries.features) {
|
|
870
|
+
result.registries.features = generateFeaturesRegistry(registries.features);
|
|
871
|
+
}
|
|
872
|
+
if (includePresentations && registries.presentations) {
|
|
873
|
+
result.registries.presentations = generatePresentationsRegistry(registries.presentations);
|
|
874
|
+
}
|
|
875
|
+
if (includeForms && registries.forms) {
|
|
876
|
+
result.registries.forms = generateFormsRegistry(registries.forms);
|
|
877
|
+
}
|
|
878
|
+
if (includeDataViews && registries.dataViews) {
|
|
879
|
+
result.registries.dataViews = generateDataViewsRegistry(registries.dataViews);
|
|
880
|
+
}
|
|
881
|
+
if (includeWorkflows && registries.workflows) {
|
|
882
|
+
result.registries.workflows = generateWorkflowsRegistry(registries.workflows);
|
|
883
|
+
}
|
|
884
|
+
result.registries.index = generateRegistryIndex({
|
|
885
|
+
operations: includeOps && !!registries.operations,
|
|
886
|
+
events: includeEvents && !!registries.events?.length,
|
|
887
|
+
features: includeFeatures && !!registries.features,
|
|
888
|
+
presentations: includePresentations && !!registries.presentations,
|
|
889
|
+
forms: includeForms && !!registries.forms,
|
|
890
|
+
dataViews: includeDataViews && !!registries.dataViews,
|
|
891
|
+
workflows: includeWorkflows && !!registries.workflows
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
return result;
|
|
895
|
+
}
|
|
896
|
+
function openApiToJson(registry, options = {}) {
|
|
897
|
+
const doc = openApiForRegistry(registry, options);
|
|
898
|
+
return JSON.stringify(doc, null, 2);
|
|
899
|
+
}
|
|
900
|
+
function openApiToYaml(registry, options = {}) {
|
|
901
|
+
const doc = openApiForRegistry(registry, options);
|
|
902
|
+
return jsonToYaml(doc);
|
|
903
|
+
}
|
|
904
|
+
function contractSpecToJson(registries, options = {}) {
|
|
905
|
+
const result = exportContractSpec(registries, options);
|
|
906
|
+
return JSON.stringify(result.openApi, null, 2);
|
|
907
|
+
}
|
|
908
|
+
function contractSpecToYaml(registries, options = {}) {
|
|
909
|
+
const result = exportContractSpec(registries, options);
|
|
910
|
+
return jsonToYaml(result.openApi);
|
|
911
|
+
}
|
|
912
|
+
function jsonToYaml(obj, indent = 0) {
|
|
913
|
+
const spaces = " ".repeat(indent);
|
|
914
|
+
let yaml = "";
|
|
915
|
+
if (Array.isArray(obj)) {
|
|
916
|
+
for (const item of obj) {
|
|
917
|
+
if (typeof item === "object" && item !== null) {
|
|
918
|
+
yaml += `${spaces}-
|
|
919
|
+
${jsonToYaml(item, indent + 1)}`;
|
|
920
|
+
} else {
|
|
921
|
+
yaml += `${spaces}- ${JSON.stringify(item)}
|
|
922
|
+
`;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
} else if (typeof obj === "object" && obj !== null) {
|
|
926
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
927
|
+
if (Array.isArray(value)) {
|
|
928
|
+
yaml += `${spaces}${key}:
|
|
929
|
+
${jsonToYaml(value, indent + 1)}`;
|
|
930
|
+
} else if (typeof value === "object" && value !== null) {
|
|
931
|
+
yaml += `${spaces}${key}:
|
|
932
|
+
${jsonToYaml(value, indent + 1)}`;
|
|
933
|
+
} else {
|
|
934
|
+
yaml += `${spaces}${key}: ${JSON.stringify(value)}
|
|
935
|
+
`;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
return yaml;
|
|
940
|
+
}
|
|
941
|
+
// src/openapi/schema-generators/index.ts
|
|
942
|
+
var JSON_SCHEMA_TO_SCALAR = {
|
|
943
|
+
string: "ScalarTypeEnum.String_unsecure",
|
|
944
|
+
integer: "ScalarTypeEnum.Int_unsecure",
|
|
945
|
+
number: "ScalarTypeEnum.Float_unsecure",
|
|
946
|
+
boolean: "ScalarTypeEnum.Boolean",
|
|
947
|
+
"string:date": "ScalarTypeEnum.Date",
|
|
948
|
+
"string:date-time": "ScalarTypeEnum.DateTime",
|
|
949
|
+
"string:email": "ScalarTypeEnum.EmailAddress",
|
|
950
|
+
"string:uri": "ScalarTypeEnum.URL",
|
|
951
|
+
"string:uuid": "ScalarTypeEnum.ID"
|
|
952
|
+
};
|
|
953
|
+
function isReference2(schema) {
|
|
954
|
+
return typeof schema === "object" && schema !== null && "$ref" in schema;
|
|
955
|
+
}
|
|
956
|
+
function typeNameFromRef(ref) {
|
|
957
|
+
const parts = ref.split("/");
|
|
958
|
+
return parts[parts.length - 1] ?? "Unknown";
|
|
959
|
+
}
|
|
960
|
+
function createSchemaGenerator(format, config) {
|
|
961
|
+
switch (format) {
|
|
962
|
+
case "zod":
|
|
963
|
+
return new ZodSchemaGenerator(config);
|
|
964
|
+
case "json-schema":
|
|
965
|
+
return new JsonSchemaGenerator(config);
|
|
966
|
+
case "graphql":
|
|
967
|
+
return new GraphQLSchemaGenerator(config);
|
|
968
|
+
case "contractspec":
|
|
969
|
+
default:
|
|
970
|
+
return new ContractSpecSchemaGenerator(config);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
class ContractSpecSchemaGenerator {
|
|
975
|
+
format = "contractspec";
|
|
976
|
+
config;
|
|
977
|
+
constructor(config) {
|
|
978
|
+
this.config = config;
|
|
979
|
+
}
|
|
980
|
+
generateModel(schema, name) {
|
|
981
|
+
const model = this.generateContractSpecSchema(schema, name);
|
|
982
|
+
const dependencyImports = this.config ? generateImports(model.fields, this.config, false).split(`
|
|
983
|
+
`).filter(Boolean) : [];
|
|
984
|
+
return {
|
|
985
|
+
code: model.code,
|
|
986
|
+
fileName: toKebabCase(name) + ".ts",
|
|
987
|
+
imports: [...this.getBaseImports(), ...dependencyImports],
|
|
988
|
+
name: model.name
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
generateField(schema, fieldName, required) {
|
|
992
|
+
const field = this.convertField(schema, fieldName, required);
|
|
993
|
+
return {
|
|
994
|
+
code: field.scalarType ? `${field.scalarType}()` : "ScalarTypeEnum.String_unsecure()",
|
|
995
|
+
typeRef: field.type.type,
|
|
996
|
+
isOptional: field.type.optional,
|
|
997
|
+
isArray: field.type.array
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
getBaseImports() {
|
|
1001
|
+
return [
|
|
1002
|
+
"import { defineSchemaModel, ScalarTypeEnum, EnumType } from '@contractspec/lib.schema';"
|
|
1003
|
+
];
|
|
1004
|
+
}
|
|
1005
|
+
generateContractSpecSchema(schema, modelName, indent = 0) {
|
|
1006
|
+
const spaces = " ".repeat(indent);
|
|
1007
|
+
const fields = [];
|
|
1008
|
+
if (isReference2(schema)) {
|
|
1009
|
+
return {
|
|
1010
|
+
name: toPascalCase(typeNameFromRef(schema.$ref)),
|
|
1011
|
+
fields: [],
|
|
1012
|
+
code: `// Reference to ${schema.$ref}`
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
const schemaObj = schema;
|
|
1016
|
+
const description = schemaObj["description"];
|
|
1017
|
+
const properties = schemaObj["properties"];
|
|
1018
|
+
const required = schemaObj["required"] ?? [];
|
|
1019
|
+
const enumValues = schemaObj["enum"];
|
|
1020
|
+
if (enumValues && enumValues.length > 0) {
|
|
1021
|
+
const safeModelName2 = toPascalCase(toValidIdentifier(modelName));
|
|
1022
|
+
const enumCode = [
|
|
1023
|
+
`${spaces}/**`,
|
|
1024
|
+
`${spaces} * Enum type: ${safeModelName2}`,
|
|
1025
|
+
description ? `${spaces} * ${description}` : null,
|
|
1026
|
+
`${spaces} */`,
|
|
1027
|
+
`${spaces}export const ${safeModelName2} = new EnumType('${safeModelName2}', [${enumValues.map((v) => `'${String(v)}'`).join(", ")}]);`
|
|
1028
|
+
].filter((line) => line !== null).join(`
|
|
1029
|
+
`);
|
|
1030
|
+
return {
|
|
1031
|
+
name: safeModelName2,
|
|
1032
|
+
description,
|
|
1033
|
+
fields: [],
|
|
1034
|
+
code: enumCode
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
const schemaType = schemaObj["type"];
|
|
1038
|
+
if (schemaType && !properties && !enumValues) {
|
|
1039
|
+
const safeModelName2 = toPascalCase(toValidIdentifier(modelName));
|
|
1040
|
+
const format = schemaObj["format"];
|
|
1041
|
+
const scalarKey = format ? `${schemaType}:${format}` : schemaType;
|
|
1042
|
+
const scalarType = JSON_SCHEMA_TO_SCALAR[scalarKey] ?? JSON_SCHEMA_TO_SCALAR[schemaType];
|
|
1043
|
+
if (scalarType) {
|
|
1044
|
+
const aliasCode = [
|
|
1045
|
+
`${spaces}/**`,
|
|
1046
|
+
`${spaces} * Type alias: ${safeModelName2}`,
|
|
1047
|
+
description ? `${spaces} * ${description}` : null,
|
|
1048
|
+
`${spaces} * Underlying type: ${scalarType}`,
|
|
1049
|
+
`${spaces} */`,
|
|
1050
|
+
`${spaces}export const ${safeModelName2} = defineSchemaModel({`,
|
|
1051
|
+
`${spaces} name: '${safeModelName2}',`,
|
|
1052
|
+
description ? `${spaces} description: ${JSON.stringify(description)},` : null,
|
|
1053
|
+
`${spaces} fields: {`,
|
|
1054
|
+
`${spaces} value: {`,
|
|
1055
|
+
`${spaces} type: ${scalarType}(),`,
|
|
1056
|
+
`${spaces} isOptional: false,`,
|
|
1057
|
+
`${spaces} },`,
|
|
1058
|
+
`${spaces} },`,
|
|
1059
|
+
`${spaces}});`
|
|
1060
|
+
].filter((line) => line !== null).join(`
|
|
1061
|
+
`);
|
|
1062
|
+
return {
|
|
1063
|
+
name: safeModelName2,
|
|
1064
|
+
description,
|
|
1065
|
+
fields: [],
|
|
1066
|
+
code: aliasCode
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
const additionalProperties = schemaObj["additionalProperties"];
|
|
1071
|
+
if (additionalProperties && !properties) {
|
|
1072
|
+
const safeModelName2 = toPascalCase(toValidIdentifier(modelName));
|
|
1073
|
+
const dictCode = [
|
|
1074
|
+
`${spaces}/**`,
|
|
1075
|
+
`${spaces} * Dictionary/Record type: ${safeModelName2}`,
|
|
1076
|
+
description ? `${spaces} * ${description}` : null,
|
|
1077
|
+
`${spaces} * Use as: Record<string, unknown> - access via record[key]`,
|
|
1078
|
+
`${spaces} */`,
|
|
1079
|
+
`${spaces}export const ${safeModelName2} = ScalarTypeEnum.JSONObject();`
|
|
1080
|
+
].filter((line) => line !== null).join(`
|
|
1081
|
+
`);
|
|
1082
|
+
return {
|
|
1083
|
+
name: safeModelName2,
|
|
1084
|
+
description,
|
|
1085
|
+
fields: [],
|
|
1086
|
+
code: dictCode
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
if (!properties) {
|
|
1090
|
+
const safeModelName2 = toPascalCase(toValidIdentifier(modelName));
|
|
1091
|
+
const emptyModelCode = [
|
|
1092
|
+
`${spaces}export const ${safeModelName2} = defineSchemaModel({`,
|
|
1093
|
+
`${spaces} name: '${safeModelName2}',`,
|
|
1094
|
+
description ? `${spaces} description: ${JSON.stringify(description)},` : null,
|
|
1095
|
+
`${spaces} fields: {},`,
|
|
1096
|
+
`${spaces}});`
|
|
1097
|
+
].filter((line) => line !== null).join(`
|
|
1098
|
+
`);
|
|
1099
|
+
return {
|
|
1100
|
+
name: safeModelName2,
|
|
1101
|
+
description,
|
|
1102
|
+
fields: [],
|
|
1103
|
+
code: emptyModelCode
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
const safeModelName = toPascalCase(toValidIdentifier(modelName));
|
|
1107
|
+
for (const [propName, propSchema] of Object.entries(properties)) {
|
|
1108
|
+
const isRequired = required.includes(propName);
|
|
1109
|
+
fields.push(this.convertField(propSchema, propName, isRequired, safeModelName));
|
|
1110
|
+
}
|
|
1111
|
+
const lines = [];
|
|
1112
|
+
for (const field of fields) {
|
|
1113
|
+
if (field.nestedModel) {
|
|
1114
|
+
lines.push(field.nestedModel.code);
|
|
1115
|
+
lines.push("");
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
lines.push(`${spaces}export const ${safeModelName} = defineSchemaModel({`);
|
|
1119
|
+
lines.push(`${spaces} name: '${safeModelName}',`);
|
|
1120
|
+
if (description) {
|
|
1121
|
+
lines.push(`${spaces} description: ${JSON.stringify(description)},`);
|
|
1122
|
+
}
|
|
1123
|
+
lines.push(`${spaces} fields: {`);
|
|
1124
|
+
for (const field of fields) {
|
|
1125
|
+
const fieldLines = this.generateFieldCodeHelper(field, indent + 2);
|
|
1126
|
+
lines.push(fieldLines);
|
|
1127
|
+
}
|
|
1128
|
+
lines.push(`${spaces} },`);
|
|
1129
|
+
lines.push(`${spaces}});`);
|
|
1130
|
+
return {
|
|
1131
|
+
name: safeModelName,
|
|
1132
|
+
description,
|
|
1133
|
+
fields,
|
|
1134
|
+
code: lines.join(`
|
|
1135
|
+
`),
|
|
1136
|
+
imports: []
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
convertField(schema, fieldName, required, parentName) {
|
|
1140
|
+
const type = jsonSchemaToType(schema, fieldName);
|
|
1141
|
+
const scalarType = getScalarType(schema);
|
|
1142
|
+
let enumValues;
|
|
1143
|
+
let nestedModel;
|
|
1144
|
+
if (!isReference2(schema)) {
|
|
1145
|
+
const schemaObj = schema;
|
|
1146
|
+
const enumArr = schemaObj["enum"];
|
|
1147
|
+
if (enumArr) {
|
|
1148
|
+
enumValues = enumArr.map(String);
|
|
1149
|
+
}
|
|
1150
|
+
if (schemaObj["type"] === "object" && !scalarType && schemaObj["properties"] && !enumValues) {
|
|
1151
|
+
const nestedName = (parentName ? parentName : "") + toPascalCase(fieldName);
|
|
1152
|
+
nestedModel = this.generateContractSpecSchema(schema, nestedName);
|
|
1153
|
+
type.type = nestedModel.name;
|
|
1154
|
+
type.isReference = true;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
return {
|
|
1158
|
+
name: fieldName,
|
|
1159
|
+
type: {
|
|
1160
|
+
...type,
|
|
1161
|
+
optional: !required || type.optional,
|
|
1162
|
+
description: !isReference2(schema) ? schema["description"] : undefined
|
|
1163
|
+
},
|
|
1164
|
+
scalarType,
|
|
1165
|
+
enumValues,
|
|
1166
|
+
nestedModel
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
generateFieldCodeHelper(field, indent) {
|
|
1170
|
+
const spaces = " ".repeat(indent);
|
|
1171
|
+
const lines = [];
|
|
1172
|
+
const isIdentifier = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(field.name);
|
|
1173
|
+
const safeKey = isIdentifier ? field.name : `'${field.name}'`;
|
|
1174
|
+
lines.push(`${spaces}${safeKey}: {`);
|
|
1175
|
+
if (field.enumValues) {
|
|
1176
|
+
const enumName = toPascalCase(field.name) + "Enum";
|
|
1177
|
+
lines.push(`${spaces} type: new EnumType('${enumName}', [${field.enumValues.map((v) => `'${v}'`).join(", ")}]),`);
|
|
1178
|
+
} else if (field.scalarType) {
|
|
1179
|
+
lines.push(`${spaces} type: ${field.scalarType}(),`);
|
|
1180
|
+
} else if (field.nestedModel) {
|
|
1181
|
+
lines.push(`${spaces} type: ${field.nestedModel.name},`);
|
|
1182
|
+
} else if (field.type.primitive) {
|
|
1183
|
+
const fallbackScalar = field.type.type === "number" ? "ScalarTypeEnum.Float_unsecure" : field.type.type === "boolean" ? "ScalarTypeEnum.Boolean_unsecure" : "ScalarTypeEnum.String_unsecure";
|
|
1184
|
+
lines.push(`${spaces} type: ${fallbackScalar}(),`);
|
|
1185
|
+
} else if (field.type.isReference) {
|
|
1186
|
+
lines.push(`${spaces} type: ${field.type.type},`);
|
|
1187
|
+
} else {
|
|
1188
|
+
lines.push(`${spaces} type: ScalarTypeEnum.JSONObject(), // TODO: Define nested model for ${field.type.type}`);
|
|
1189
|
+
}
|
|
1190
|
+
lines.push(`${spaces} isOptional: ${field.type.optional},`);
|
|
1191
|
+
if (field.type.array) {
|
|
1192
|
+
lines.push(`${spaces} isArray: true,`);
|
|
1193
|
+
}
|
|
1194
|
+
lines.push(`${spaces}},`);
|
|
1195
|
+
return lines.join(`
|
|
1196
|
+
`);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
class ZodSchemaGenerator {
|
|
1201
|
+
format = "zod";
|
|
1202
|
+
config;
|
|
1203
|
+
constructor(config) {
|
|
1204
|
+
this.config = config;
|
|
1205
|
+
}
|
|
1206
|
+
generateModel(schema, name, options) {
|
|
1207
|
+
const schemaObj = schema;
|
|
1208
|
+
const properties = schemaObj["properties"];
|
|
1209
|
+
const _required = schemaObj["required"] ?? [];
|
|
1210
|
+
const description = options?.description ?? schemaObj["description"];
|
|
1211
|
+
const lines = [];
|
|
1212
|
+
if (description) {
|
|
1213
|
+
lines.push(`/**`);
|
|
1214
|
+
lines.push(` * ${description}`);
|
|
1215
|
+
lines.push(` */`);
|
|
1216
|
+
}
|
|
1217
|
+
const schemaName = `${name}Schema`;
|
|
1218
|
+
let schemaCode;
|
|
1219
|
+
if (properties) {
|
|
1220
|
+
schemaCode = this.generateZodObject(schemaObj);
|
|
1221
|
+
} else {
|
|
1222
|
+
schemaCode = "z.object({})";
|
|
1223
|
+
}
|
|
1224
|
+
lines.push(`export const ${schemaName} = ${schemaCode};`);
|
|
1225
|
+
lines.push(``);
|
|
1226
|
+
lines.push(`export const ${name} = new ZodSchemaType(${schemaName}, { name: '${name}', description: ${JSON.stringify(description)} });`);
|
|
1227
|
+
lines.push(``);
|
|
1228
|
+
lines.push(`export type ${name} = z.infer<typeof ${schemaName}>;`);
|
|
1229
|
+
return {
|
|
1230
|
+
code: lines.join(`
|
|
1231
|
+
`),
|
|
1232
|
+
fileName: toKebabCase(name) + ".ts",
|
|
1233
|
+
imports: this.getBaseImports(),
|
|
1234
|
+
name
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
generateField(schema, _fieldName, required) {
|
|
1238
|
+
const schemaObj = schema;
|
|
1239
|
+
const type = schemaObj["type"];
|
|
1240
|
+
const format = schemaObj["format"];
|
|
1241
|
+
const nullable = schemaObj["nullable"];
|
|
1242
|
+
let zodType;
|
|
1243
|
+
if (type === "object" && schemaObj["properties"]) {
|
|
1244
|
+
zodType = this.generateZodObject(schemaObj);
|
|
1245
|
+
} else {
|
|
1246
|
+
zodType = this.mapTypeToZod(type, format);
|
|
1247
|
+
if (schemaObj["enum"]) {
|
|
1248
|
+
const enumValues = schemaObj["enum"];
|
|
1249
|
+
zodType = `z.enum([${enumValues.map((v) => `'${v}'`).join(", ")}])`;
|
|
1250
|
+
}
|
|
1251
|
+
if (type === "array") {
|
|
1252
|
+
const items = schemaObj["items"];
|
|
1253
|
+
if (items) {
|
|
1254
|
+
const itemField = this.generateField(items, "item", true);
|
|
1255
|
+
zodType = `z.array(${itemField.code.replace(".optional()", "")})`;
|
|
1256
|
+
} else {
|
|
1257
|
+
zodType = "z.array(z.unknown())";
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
if (type === "string") {
|
|
1262
|
+
if (schemaObj["minLength"] !== undefined)
|
|
1263
|
+
zodType += `.min(${schemaObj["minLength"]})`;
|
|
1264
|
+
if (schemaObj["maxLength"] !== undefined)
|
|
1265
|
+
zodType += `.max(${schemaObj["maxLength"]})`;
|
|
1266
|
+
if (schemaObj["pattern"] !== undefined)
|
|
1267
|
+
zodType += `.regex(/${schemaObj["pattern"]}/)`;
|
|
1268
|
+
} else if (type === "integer" || type === "number") {
|
|
1269
|
+
if (schemaObj["minimum"] !== undefined) {
|
|
1270
|
+
zodType += schemaObj["exclusiveMinimum"] === true ? `.gt(${schemaObj["minimum"]})` : `.min(${schemaObj["minimum"]})`;
|
|
1271
|
+
} else if (typeof schemaObj["exclusiveMinimum"] === "number") {
|
|
1272
|
+
zodType += `.gt(${schemaObj["exclusiveMinimum"]})`;
|
|
1273
|
+
}
|
|
1274
|
+
if (schemaObj["maximum"] !== undefined) {
|
|
1275
|
+
zodType += schemaObj["exclusiveMaximum"] === true ? `.lt(${schemaObj["maximum"]})` : `.max(${schemaObj["maximum"]})`;
|
|
1276
|
+
} else if (typeof schemaObj["exclusiveMaximum"] === "number") {
|
|
1277
|
+
zodType += `.lt(${schemaObj["exclusiveMaximum"]})`;
|
|
1278
|
+
}
|
|
1279
|
+
if (schemaObj["multipleOf"] !== undefined) {
|
|
1280
|
+
zodType += `.step(${schemaObj["multipleOf"]})`;
|
|
1281
|
+
}
|
|
1282
|
+
} else if (type === "array") {
|
|
1283
|
+
if (schemaObj["minItems"] !== undefined)
|
|
1284
|
+
zodType += `.min(${schemaObj["minItems"]})`;
|
|
1285
|
+
if (schemaObj["maxItems"] !== undefined)
|
|
1286
|
+
zodType += `.max(${schemaObj["maxItems"]})`;
|
|
1287
|
+
}
|
|
1288
|
+
if (schemaObj["default"] !== undefined) {
|
|
1289
|
+
zodType += `.default(${JSON.stringify(schemaObj["default"])})`;
|
|
1290
|
+
}
|
|
1291
|
+
if (!required || nullable) {
|
|
1292
|
+
zodType = `${zodType}.optional()`;
|
|
1293
|
+
}
|
|
1294
|
+
return {
|
|
1295
|
+
code: zodType,
|
|
1296
|
+
typeRef: type ?? "unknown",
|
|
1297
|
+
isOptional: !required || Boolean(nullable),
|
|
1298
|
+
isArray: type === "array"
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
getBaseImports() {
|
|
1302
|
+
return [
|
|
1303
|
+
"import * as z from 'zod';",
|
|
1304
|
+
"import { ZodSchemaType } from '@contractspec/lib.schema';"
|
|
1305
|
+
];
|
|
1306
|
+
}
|
|
1307
|
+
generateZodObject(schemaObj) {
|
|
1308
|
+
const required = schemaObj["required"] ?? [];
|
|
1309
|
+
const properties = schemaObj["properties"];
|
|
1310
|
+
const lines = ["z.object({"];
|
|
1311
|
+
for (const [propName, propSchema] of Object.entries(properties)) {
|
|
1312
|
+
const isRequired = required.includes(propName);
|
|
1313
|
+
const field = this.generateField(propSchema, propName, isRequired);
|
|
1314
|
+
const safeName = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(propName) ? propName : `'${propName}'`;
|
|
1315
|
+
lines.push(` ${safeName}: ${field.code},`);
|
|
1316
|
+
}
|
|
1317
|
+
lines.push("})");
|
|
1318
|
+
return lines.join(`
|
|
1319
|
+
`);
|
|
1320
|
+
}
|
|
1321
|
+
mapTypeToZod(type, format) {
|
|
1322
|
+
if (format === "date-time")
|
|
1323
|
+
return "z.string().datetime()";
|
|
1324
|
+
if (format === "date")
|
|
1325
|
+
return "z.string().date()";
|
|
1326
|
+
if (format === "email")
|
|
1327
|
+
return "z.string().email()";
|
|
1328
|
+
if (format === "uri" || format === "url")
|
|
1329
|
+
return "z.string().url()";
|
|
1330
|
+
if (format === "uuid")
|
|
1331
|
+
return "z.string().uuid()";
|
|
1332
|
+
switch (type) {
|
|
1333
|
+
case "string":
|
|
1334
|
+
return "z.string()";
|
|
1335
|
+
case "integer":
|
|
1336
|
+
return "z.number().int()";
|
|
1337
|
+
case "number":
|
|
1338
|
+
return "z.number()";
|
|
1339
|
+
case "boolean":
|
|
1340
|
+
return "z.boolean()";
|
|
1341
|
+
case "object":
|
|
1342
|
+
return "z.record(z.string(), z.unknown())";
|
|
1343
|
+
case "null":
|
|
1344
|
+
return "z.null()";
|
|
1345
|
+
default:
|
|
1346
|
+
return "z.unknown()";
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
class JsonSchemaGenerator {
|
|
1352
|
+
format = "json-schema";
|
|
1353
|
+
config;
|
|
1354
|
+
constructor(config) {
|
|
1355
|
+
this.config = config;
|
|
1356
|
+
}
|
|
1357
|
+
generateModel(schema, name, options) {
|
|
1358
|
+
const schemaObj = schema;
|
|
1359
|
+
const description = options?.description ?? schemaObj["description"];
|
|
1360
|
+
const jsonSchema = {
|
|
1361
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
1362
|
+
title: name,
|
|
1363
|
+
...schemaObj
|
|
1364
|
+
};
|
|
1365
|
+
if (description) {
|
|
1366
|
+
jsonSchema["description"] = description;
|
|
1367
|
+
}
|
|
1368
|
+
const lines = [];
|
|
1369
|
+
lines.push(`/**`);
|
|
1370
|
+
lines.push(` * JSON Schema: ${name}`);
|
|
1371
|
+
if (description) {
|
|
1372
|
+
lines.push(` * ${description}`);
|
|
1373
|
+
}
|
|
1374
|
+
lines.push(` */`);
|
|
1375
|
+
const schemaName = `${name}Schema`;
|
|
1376
|
+
lines.push(`export const ${schemaName} = ${JSON.stringify(jsonSchema, null, 2)} as const;`);
|
|
1377
|
+
lines.push(``);
|
|
1378
|
+
lines.push(`export const ${name} = new JsonSchemaType(${schemaName});`);
|
|
1379
|
+
lines.push(``);
|
|
1380
|
+
lines.push(`export type ${name} = unknown; // JSON Schema type inference not fully supported`);
|
|
1381
|
+
return {
|
|
1382
|
+
code: lines.join(`
|
|
1383
|
+
`),
|
|
1384
|
+
fileName: toKebabCase(name) + ".ts",
|
|
1385
|
+
imports: this.getBaseImports(),
|
|
1386
|
+
name
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1389
|
+
generateField(schema, _fieldName, required) {
|
|
1390
|
+
const schemaObj = schema;
|
|
1391
|
+
const type = schemaObj["type"];
|
|
1392
|
+
const nullable = schemaObj["nullable"];
|
|
1393
|
+
return {
|
|
1394
|
+
code: JSON.stringify(schemaObj),
|
|
1395
|
+
typeRef: type ?? "unknown",
|
|
1396
|
+
isOptional: !required || Boolean(nullable),
|
|
1397
|
+
isArray: type === "array"
|
|
1398
|
+
};
|
|
1399
|
+
}
|
|
1400
|
+
getBaseImports() {
|
|
1401
|
+
return ["import { JsonSchemaType } from '@contractspec/lib.schema';"];
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
class GraphQLSchemaGenerator {
|
|
1406
|
+
format = "graphql";
|
|
1407
|
+
config;
|
|
1408
|
+
constructor(config) {
|
|
1409
|
+
this.config = config;
|
|
1410
|
+
}
|
|
1411
|
+
generateModel(schema, name, options) {
|
|
1412
|
+
const schemaObj = schema;
|
|
1413
|
+
const properties = schemaObj["properties"];
|
|
1414
|
+
const required = schemaObj["required"] ?? [];
|
|
1415
|
+
const description = options?.description ?? schemaObj["description"];
|
|
1416
|
+
const lines = [];
|
|
1417
|
+
if (description) {
|
|
1418
|
+
lines.push(`"""${description}"""`);
|
|
1419
|
+
}
|
|
1420
|
+
lines.push(`type ${name} {`);
|
|
1421
|
+
if (properties) {
|
|
1422
|
+
for (const [propName, propSchema] of Object.entries(properties)) {
|
|
1423
|
+
const isRequired = required.includes(propName);
|
|
1424
|
+
const field = this.generateField(propSchema, propName, isRequired);
|
|
1425
|
+
const nullMarker = isRequired ? "!" : "";
|
|
1426
|
+
lines.push(` ${propName}: ${field.typeRef}${nullMarker}`);
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
lines.push(`}`);
|
|
1430
|
+
const sdl = lines.join(`
|
|
1431
|
+
`);
|
|
1432
|
+
const tsLines = [];
|
|
1433
|
+
tsLines.push(`/**`);
|
|
1434
|
+
tsLines.push(` * GraphQL type definition: ${name}`);
|
|
1435
|
+
tsLines.push(` */`);
|
|
1436
|
+
tsLines.push(`export const ${name}TypeDef = \`${sdl}\`;`);
|
|
1437
|
+
tsLines.push(``);
|
|
1438
|
+
tsLines.push(`export const ${name} = new GraphQLSchemaType(${name}TypeDef, '${name}');`);
|
|
1439
|
+
return {
|
|
1440
|
+
code: tsLines.join(`
|
|
1441
|
+
`),
|
|
1442
|
+
fileName: toKebabCase(name) + ".ts",
|
|
1443
|
+
imports: this.getBaseImports(),
|
|
1444
|
+
name
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
generateField(schema, _fieldName, required) {
|
|
1448
|
+
const schemaObj = schema;
|
|
1449
|
+
const type = schemaObj["type"];
|
|
1450
|
+
const format = schemaObj["format"];
|
|
1451
|
+
const nullable = schemaObj["nullable"];
|
|
1452
|
+
const gqlType = this.mapTypeToGraphQL(type, format);
|
|
1453
|
+
return {
|
|
1454
|
+
code: gqlType,
|
|
1455
|
+
typeRef: gqlType,
|
|
1456
|
+
isOptional: !required || Boolean(nullable),
|
|
1457
|
+
isArray: type === "array"
|
|
1458
|
+
};
|
|
1459
|
+
}
|
|
1460
|
+
getBaseImports() {
|
|
1461
|
+
return ["import { GraphQLSchemaType } from '@contractspec/lib.schema';"];
|
|
1462
|
+
}
|
|
1463
|
+
mapTypeToGraphQL(type, format) {
|
|
1464
|
+
if (format === "date-time")
|
|
1465
|
+
return "DateTime";
|
|
1466
|
+
if (format === "date")
|
|
1467
|
+
return "Date";
|
|
1468
|
+
if (format === "email")
|
|
1469
|
+
return "String";
|
|
1470
|
+
if (format === "uri" || format === "url")
|
|
1471
|
+
return "String";
|
|
1472
|
+
if (format === "uuid")
|
|
1473
|
+
return "ID";
|
|
1474
|
+
switch (type) {
|
|
1475
|
+
case "string":
|
|
1476
|
+
return "String";
|
|
1477
|
+
case "integer":
|
|
1478
|
+
return "Int";
|
|
1479
|
+
case "number":
|
|
1480
|
+
return "Float";
|
|
1481
|
+
case "boolean":
|
|
1482
|
+
return "Boolean";
|
|
1483
|
+
case "object":
|
|
1484
|
+
return "JSON";
|
|
1485
|
+
case "array":
|
|
1486
|
+
return "[JSON]";
|
|
1487
|
+
default:
|
|
1488
|
+
return "JSON";
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
// src/openapi/schema-converter.ts
|
|
1494
|
+
var JSON_SCHEMA_TO_SCALAR2 = {
|
|
1495
|
+
string: "ScalarTypeEnum.String_unsecure",
|
|
1496
|
+
integer: "ScalarTypeEnum.Int_unsecure",
|
|
1497
|
+
number: "ScalarTypeEnum.Float_unsecure",
|
|
1498
|
+
boolean: "ScalarTypeEnum.Boolean",
|
|
1499
|
+
"string:date": "ScalarTypeEnum.Date",
|
|
1500
|
+
"string:date-time": "ScalarTypeEnum.DateTime",
|
|
1501
|
+
"string:email": "ScalarTypeEnum.EmailAddress",
|
|
1502
|
+
"string:uri": "ScalarTypeEnum.URL",
|
|
1503
|
+
"string:uuid": "ScalarTypeEnum.ID"
|
|
1504
|
+
};
|
|
1505
|
+
function isReference3(schema) {
|
|
1506
|
+
return typeof schema === "object" && schema !== null && "$ref" in schema;
|
|
1507
|
+
}
|
|
1508
|
+
function typeNameFromRef2(ref) {
|
|
1509
|
+
const parts = ref.split("/");
|
|
1510
|
+
return parts[parts.length - 1] ?? "Unknown";
|
|
1511
|
+
}
|
|
1512
|
+
function jsonSchemaToType(schema, name) {
|
|
1513
|
+
if (isReference3(schema)) {
|
|
1514
|
+
return {
|
|
1515
|
+
type: toPascalCase(typeNameFromRef2(schema.$ref)),
|
|
1516
|
+
optional: false,
|
|
1517
|
+
array: false,
|
|
1518
|
+
primitive: false,
|
|
1519
|
+
isReference: true
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
const schemaObj = schema;
|
|
1523
|
+
const type = schemaObj["type"];
|
|
1524
|
+
const format = schemaObj["format"];
|
|
1525
|
+
const nullable = schemaObj["nullable"];
|
|
1526
|
+
const originalTypeName = schemaObj["_originalTypeName"];
|
|
1527
|
+
if (originalTypeName) {
|
|
1528
|
+
return {
|
|
1529
|
+
type: toPascalCase(originalTypeName),
|
|
1530
|
+
optional: nullable ?? false,
|
|
1531
|
+
array: false,
|
|
1532
|
+
primitive: false,
|
|
1533
|
+
isReference: true
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
if (type === "array") {
|
|
1537
|
+
const items = schemaObj["items"];
|
|
1538
|
+
if (items) {
|
|
1539
|
+
const itemType = jsonSchemaToType(items, name);
|
|
1540
|
+
return {
|
|
1541
|
+
...itemType,
|
|
1542
|
+
array: true,
|
|
1543
|
+
optional: nullable ?? false
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
return {
|
|
1547
|
+
type: "unknown",
|
|
1548
|
+
optional: nullable ?? false,
|
|
1549
|
+
array: true,
|
|
1550
|
+
primitive: false,
|
|
1551
|
+
isReference: true
|
|
1552
|
+
};
|
|
1553
|
+
}
|
|
1554
|
+
if (type === "object" || schemaObj["properties"]) {
|
|
1555
|
+
return {
|
|
1556
|
+
type: name ? toPascalCase(name) : "Record<string, unknown>",
|
|
1557
|
+
optional: nullable ?? false,
|
|
1558
|
+
array: false,
|
|
1559
|
+
primitive: false,
|
|
1560
|
+
isReference: true
|
|
1561
|
+
};
|
|
1562
|
+
}
|
|
1563
|
+
if (schemaObj["enum"]) {
|
|
1564
|
+
return {
|
|
1565
|
+
type: name ? toPascalCase(name) : "string",
|
|
1566
|
+
optional: nullable ?? false,
|
|
1567
|
+
array: false,
|
|
1568
|
+
primitive: false,
|
|
1569
|
+
isReference: true
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1572
|
+
const scalarKey = format ? `${type}:${format}` : type;
|
|
1573
|
+
if (scalarKey === "string") {
|
|
1574
|
+
return {
|
|
1575
|
+
type: "string",
|
|
1576
|
+
optional: nullable ?? false,
|
|
1577
|
+
array: false,
|
|
1578
|
+
primitive: true
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
if (scalarKey === "integer" || type === "number") {
|
|
1582
|
+
return {
|
|
1583
|
+
type: "number",
|
|
1584
|
+
optional: nullable ?? false,
|
|
1585
|
+
array: false,
|
|
1586
|
+
primitive: true
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
if (scalarKey === "boolean") {
|
|
1590
|
+
return {
|
|
1591
|
+
type: "boolean",
|
|
1592
|
+
optional: nullable ?? false,
|
|
1593
|
+
array: false,
|
|
1594
|
+
primitive: true
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
return {
|
|
1598
|
+
type: "unknown",
|
|
1599
|
+
optional: nullable ?? false,
|
|
1600
|
+
array: false,
|
|
1601
|
+
primitive: false,
|
|
1602
|
+
isReference: true
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1605
|
+
function getScalarType(schema) {
|
|
1606
|
+
if (isReference3(schema)) {
|
|
1607
|
+
return;
|
|
1608
|
+
}
|
|
1609
|
+
const schemaObj = schema;
|
|
1610
|
+
const type = schemaObj["type"];
|
|
1611
|
+
const format = schemaObj["format"];
|
|
1612
|
+
if (!type)
|
|
1613
|
+
return;
|
|
1614
|
+
if (type === "array") {
|
|
1615
|
+
const items = schemaObj["items"];
|
|
1616
|
+
if (items) {
|
|
1617
|
+
return getScalarType(items);
|
|
1618
|
+
}
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1621
|
+
const key = format ? `${type}:${format}` : type;
|
|
1622
|
+
return JSON_SCHEMA_TO_SCALAR2[key] ?? JSON_SCHEMA_TO_SCALAR2[type] ?? undefined;
|
|
1623
|
+
}
|
|
1624
|
+
function generateSchemaModelCode(schema, modelName, schemaFormat = "contractspec", config) {
|
|
1625
|
+
const generator = createSchemaGenerator(schemaFormat, config);
|
|
1626
|
+
const result = generator.generateModel(schema, modelName, {
|
|
1627
|
+
description: schema["description"]
|
|
1628
|
+
});
|
|
1629
|
+
return {
|
|
1630
|
+
name: result.name,
|
|
1631
|
+
description: schema["description"],
|
|
1632
|
+
fields: [],
|
|
1633
|
+
code: result.code,
|
|
1634
|
+
imports: result.imports
|
|
1635
|
+
};
|
|
1636
|
+
}
|
|
1637
|
+
function generateImports(fields, options, sameDirectory = true) {
|
|
1638
|
+
const imports = new Set;
|
|
1639
|
+
const modelsDir = sameDirectory ? "." : `../${options.conventions.models}`;
|
|
1640
|
+
imports.add("import { defineSchemaModel, ScalarTypeEnum, EnumType } from '@contractspec/lib.schema';");
|
|
1641
|
+
for (const field of fields) {
|
|
1642
|
+
if (field.type.isReference && !field.type.primitive && !field.enumValues && !field.scalarType && !field.nestedModel) {
|
|
1643
|
+
const modelName = field.type.type;
|
|
1644
|
+
const kebabName = modelName.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
1645
|
+
imports.add(`import { ${modelName} } from '${modelsDir}/${kebabName}';`);
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
return Array.from(imports).join(`
|
|
1649
|
+
`);
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
// src/openapi/importer/schemas.ts
|
|
1653
|
+
function buildInputSchemas(operation2) {
|
|
1654
|
+
const result = {};
|
|
1655
|
+
if (operation2.pathParams.length > 0) {
|
|
1656
|
+
result.params = {
|
|
1657
|
+
type: "object",
|
|
1658
|
+
properties: operation2.pathParams.reduce((acc, p) => {
|
|
1659
|
+
acc[p.name] = p.schema;
|
|
1660
|
+
return acc;
|
|
1661
|
+
}, {}),
|
|
1662
|
+
required: operation2.pathParams.map((p) => p.name)
|
|
1663
|
+
};
|
|
1664
|
+
}
|
|
1665
|
+
if (operation2.queryParams.length > 0) {
|
|
1666
|
+
result.query = {
|
|
1667
|
+
type: "object",
|
|
1668
|
+
properties: operation2.queryParams.reduce((acc, p) => {
|
|
1669
|
+
acc[p.name] = p.schema;
|
|
1670
|
+
return acc;
|
|
1671
|
+
}, {}),
|
|
1672
|
+
required: operation2.queryParams.filter((p) => p.required).map((p) => p.name)
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
const excludedHeaders = [
|
|
1676
|
+
"authorization",
|
|
1677
|
+
"content-type",
|
|
1678
|
+
"accept",
|
|
1679
|
+
"user-agent"
|
|
1680
|
+
];
|
|
1681
|
+
const actualHeaders = operation2.headerParams.filter((p) => !excludedHeaders.includes(p.name.toLowerCase()));
|
|
1682
|
+
if (actualHeaders.length > 0) {
|
|
1683
|
+
result.headers = {
|
|
1684
|
+
type: "object",
|
|
1685
|
+
properties: actualHeaders.reduce((acc, p) => {
|
|
1686
|
+
acc[p.name] = p.schema;
|
|
1687
|
+
return acc;
|
|
1688
|
+
}, {}),
|
|
1689
|
+
required: actualHeaders.filter((p) => p.required).map((p) => p.name)
|
|
1690
|
+
};
|
|
1691
|
+
}
|
|
1692
|
+
if (operation2.requestBody?.schema) {
|
|
1693
|
+
result.body = operation2.requestBody.schema;
|
|
1694
|
+
}
|
|
1695
|
+
return result;
|
|
1696
|
+
}
|
|
1697
|
+
function getOutputSchema(operation2) {
|
|
1698
|
+
const successCodes = ["200", "201", "202", "204"];
|
|
1699
|
+
for (const code of successCodes) {
|
|
1700
|
+
const response = operation2.responses[code];
|
|
1701
|
+
if (response?.schema) {
|
|
1702
|
+
return response.schema;
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
for (const [code, response] of Object.entries(operation2.responses)) {
|
|
1706
|
+
if (code.startsWith("2") && response.schema) {
|
|
1707
|
+
return response.schema;
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
return null;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
// src/openapi/importer/analyzer.ts
|
|
1714
|
+
var COMMAND_METHODS = ["post", "put", "delete", "patch"];
|
|
1715
|
+
function inferOpKind(method) {
|
|
1716
|
+
return COMMAND_METHODS.includes(method.toLowerCase()) ? "command" : "query";
|
|
1717
|
+
}
|
|
1718
|
+
function inferAuthLevel(operation2, defaultAuth) {
|
|
1719
|
+
if (!operation2.security || operation2.security.length === 0) {
|
|
1720
|
+
return defaultAuth;
|
|
1721
|
+
}
|
|
1722
|
+
for (const sec of operation2.security) {
|
|
1723
|
+
if (Object.keys(sec).length === 0) {
|
|
1724
|
+
return "anonymous";
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
return "user";
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
// src/openapi/importer/generator.ts
|
|
1731
|
+
function generateSpecCode(operation2, contractspecConfig, options = {}, inputModel, outputModel, queryModel = null, paramsModel = null, headersModel = null) {
|
|
1732
|
+
const specKey = toSpecKey(operation2.operationId, options.prefix);
|
|
1733
|
+
const kind = inferOpKind(operation2.method);
|
|
1734
|
+
const auth = inferAuthLevel(operation2, options.defaultAuth ?? "user");
|
|
1735
|
+
const lines = [];
|
|
1736
|
+
lines.push("import { defineCommand, defineQuery } from '@contractspec/lib.contracts';");
|
|
1737
|
+
if (inputModel || outputModel || queryModel || paramsModel || headersModel) {
|
|
1738
|
+
const collectedImports = new Set;
|
|
1739
|
+
const models = [
|
|
1740
|
+
inputModel,
|
|
1741
|
+
outputModel,
|
|
1742
|
+
queryModel,
|
|
1743
|
+
paramsModel,
|
|
1744
|
+
headersModel
|
|
1745
|
+
].filter((m) => !!m);
|
|
1746
|
+
models.forEach((m) => {
|
|
1747
|
+
if (m.imports && m.imports.length > 0) {
|
|
1748
|
+
m.imports.forEach((i) => collectedImports.add(i));
|
|
1749
|
+
}
|
|
1750
|
+
});
|
|
1751
|
+
const legacyModels = models.filter((m) => !m.imports || m.imports.length === 0);
|
|
1752
|
+
const legacyFields = legacyModels.flatMap((m) => m.fields);
|
|
1753
|
+
if (legacyFields.length > 0) {
|
|
1754
|
+
const legacyImportStr = generateImports(legacyFields, contractspecConfig, false);
|
|
1755
|
+
legacyImportStr.split(`
|
|
1756
|
+
`).filter(Boolean).forEach((i) => collectedImports.add(i));
|
|
1757
|
+
}
|
|
1758
|
+
if (collectedImports.size > 0) {
|
|
1759
|
+
lines.push(Array.from(collectedImports).sort().join(`
|
|
1760
|
+
`));
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
lines.push("");
|
|
1764
|
+
const schemaSections = [
|
|
1765
|
+
{ label: "Input schema", model: inputModel },
|
|
1766
|
+
{ label: "Query schema", model: queryModel },
|
|
1767
|
+
{ label: "Path schema", model: paramsModel },
|
|
1768
|
+
{ label: "Header schema", model: headersModel },
|
|
1769
|
+
{ label: "Output schema", model: outputModel }
|
|
1770
|
+
];
|
|
1771
|
+
for (const section of schemaSections) {
|
|
1772
|
+
if (section.model && section.model.code) {
|
|
1773
|
+
lines.push(`// ${section.label}`);
|
|
1774
|
+
lines.push(section.model.code);
|
|
1775
|
+
lines.push("");
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
const defineFunc = kind === "command" ? "defineCommand" : "defineQuery";
|
|
1779
|
+
const safeName = toValidIdentifier(toPascalCase(operation2.operationId));
|
|
1780
|
+
lines.push(`/**`);
|
|
1781
|
+
lines.push(` * ${operation2.summary ?? operation2.operationId}`);
|
|
1782
|
+
if (operation2.description) {
|
|
1783
|
+
lines.push(` *`);
|
|
1784
|
+
lines.push(` * ${operation2.description}`);
|
|
1785
|
+
}
|
|
1786
|
+
lines.push(` *`);
|
|
1787
|
+
lines.push(` * @source OpenAPI: ${operation2.method.toUpperCase()} ${operation2.path}`);
|
|
1788
|
+
lines.push(` */`);
|
|
1789
|
+
lines.push(`export const ${safeName}Spec = ${defineFunc}({`);
|
|
1790
|
+
lines.push(" meta: {");
|
|
1791
|
+
lines.push(` key: '${specKey}',`);
|
|
1792
|
+
lines.push(" version: '1.0.0',");
|
|
1793
|
+
lines.push(` stability: '${options.defaultStability ?? "stable"}',`);
|
|
1794
|
+
lines.push(` owners: [${(options.defaultOwners ?? []).map((o) => `'${o}'`).join(", ")}],`);
|
|
1795
|
+
lines.push(` tags: [${operation2.tags.map((t) => `'${t}'`).join(", ")}],`);
|
|
1796
|
+
lines.push(` description: ${JSON.stringify(operation2.summary ?? operation2.operationId)},`);
|
|
1797
|
+
lines.push(` goal: ${JSON.stringify(operation2.description ?? "Imported from OpenAPI")},`);
|
|
1798
|
+
lines.push(` context: 'Imported from OpenAPI: ${operation2.method.toUpperCase()} ${operation2.path}',`);
|
|
1799
|
+
lines.push(" },");
|
|
1800
|
+
lines.push(" io: {");
|
|
1801
|
+
lines.push(` input: ${inputModel?.name ?? "null"},`);
|
|
1802
|
+
if (queryModel)
|
|
1803
|
+
lines.push(` query: ${queryModel.name},`);
|
|
1804
|
+
if (paramsModel)
|
|
1805
|
+
lines.push(` params: ${paramsModel.name},`);
|
|
1806
|
+
if (headersModel)
|
|
1807
|
+
lines.push(` headers: ${headersModel.name},`);
|
|
1808
|
+
if (outputModel) {
|
|
1809
|
+
lines.push(` output: ${outputModel.name},`);
|
|
1810
|
+
} else {
|
|
1811
|
+
lines.push(" output: null, // TODO: Define output schema");
|
|
1812
|
+
}
|
|
1813
|
+
lines.push(" },");
|
|
1814
|
+
lines.push(" policy: {");
|
|
1815
|
+
lines.push(` auth: '${auth}',`);
|
|
1816
|
+
lines.push(" },");
|
|
1817
|
+
const httpMethod = operation2.method.toUpperCase();
|
|
1818
|
+
const restMethod = httpMethod === "GET" ? "GET" : "POST";
|
|
1819
|
+
lines.push(" transport: {");
|
|
1820
|
+
lines.push(" rest: {");
|
|
1821
|
+
lines.push(` method: '${restMethod}',`);
|
|
1822
|
+
lines.push(` path: '${operation2.path}',`);
|
|
1823
|
+
lines.push(" },");
|
|
1824
|
+
lines.push(" },");
|
|
1825
|
+
lines.push("});");
|
|
1826
|
+
return lines.join(`
|
|
1827
|
+
`);
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
// src/openapi/importer/models.ts
|
|
1831
|
+
function generateModelCode(name, schema, options) {
|
|
1832
|
+
const modelName = toPascalCase(toValidIdentifier(name));
|
|
1833
|
+
const schemaFormat = options.schemaFormat || "contractspec";
|
|
1834
|
+
const model = generateSchemaModelCode(schema, modelName, schemaFormat, options);
|
|
1835
|
+
let imports = "";
|
|
1836
|
+
if (model.imports && model.imports.length > 0) {
|
|
1837
|
+
imports = model.imports.join(`
|
|
1838
|
+
`);
|
|
1839
|
+
} else if (model.fields.length > 0) {
|
|
1840
|
+
imports = generateImports(model.fields, options);
|
|
1841
|
+
}
|
|
1842
|
+
return `
|
|
1843
|
+
${imports}
|
|
1844
|
+
|
|
1845
|
+
${model.code}
|
|
1846
|
+
`.trim();
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
// src/openapi/importer/events.ts
|
|
1850
|
+
function generateEventCode(event, options) {
|
|
1851
|
+
const eventName = toValidIdentifier(event.name);
|
|
1852
|
+
const modelName = toPascalCase(eventName) + "Payload";
|
|
1853
|
+
const schemaFormat = options.schemaFormat || "contractspec";
|
|
1854
|
+
const payloadModel = generateSchemaModelCode(event.payload, modelName, schemaFormat, options);
|
|
1855
|
+
const imports = new Set;
|
|
1856
|
+
imports.add("import { defineEvent, type EventSpec } from '@contractspec/lib.contracts';");
|
|
1857
|
+
if (payloadModel.imports && payloadModel.imports.length > 0) {
|
|
1858
|
+
payloadModel.imports.forEach((i) => imports.add(i));
|
|
1859
|
+
} else if (payloadModel.fields && payloadModel.fields.length > 0) {
|
|
1860
|
+
const modelImports = generateImports(payloadModel.fields, options);
|
|
1861
|
+
modelImports.split(`
|
|
1862
|
+
`).filter(Boolean).forEach((i) => imports.add(i));
|
|
1863
|
+
}
|
|
1864
|
+
if (payloadModel.name !== modelName) {
|
|
1865
|
+
const modelsDir = `../${options.conventions.models}`;
|
|
1866
|
+
const kebabName = payloadModel.name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
1867
|
+
imports.add(`import { ${payloadModel.name} } from '${modelsDir}/${kebabName}';`);
|
|
1868
|
+
}
|
|
1869
|
+
const allImports = Array.from(imports).join(`
|
|
1870
|
+
`);
|
|
1871
|
+
return `
|
|
1872
|
+
${allImports}
|
|
1873
|
+
|
|
1874
|
+
${payloadModel.code}
|
|
1875
|
+
|
|
1876
|
+
export const ${eventName} = defineEvent({
|
|
1877
|
+
meta: {
|
|
1878
|
+
key: '${event.name}',
|
|
1879
|
+
version: '1.0.0',
|
|
1880
|
+
description: ${JSON.stringify(event.description ?? "")},
|
|
1881
|
+
},
|
|
1882
|
+
payload: ${payloadModel.name},
|
|
1883
|
+
});
|
|
1884
|
+
`.trim();
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
// src/openapi/importer/grouping.ts
|
|
1888
|
+
function resolveOperationGroupFolder(operation2, conventions) {
|
|
1889
|
+
const groupingRule = conventions.operationsGrouping;
|
|
1890
|
+
if (!groupingRule || groupingRule.strategy === "none") {
|
|
1891
|
+
return "";
|
|
1892
|
+
}
|
|
1893
|
+
return applyGroupingStrategy(groupingRule, {
|
|
1894
|
+
name: operation2.operationId,
|
|
1895
|
+
tags: operation2.tags,
|
|
1896
|
+
path: operation2.path
|
|
1897
|
+
});
|
|
1898
|
+
}
|
|
1899
|
+
function resolveModelGroupFolder(modelName, conventions, relatedPath, relatedTags) {
|
|
1900
|
+
const groupingRule = conventions.modelsGrouping;
|
|
1901
|
+
if (!groupingRule || groupingRule.strategy === "none") {
|
|
1902
|
+
return "";
|
|
1903
|
+
}
|
|
1904
|
+
return applyGroupingStrategy(groupingRule, {
|
|
1905
|
+
name: modelName,
|
|
1906
|
+
tags: relatedTags ?? [],
|
|
1907
|
+
path: relatedPath
|
|
1908
|
+
});
|
|
1909
|
+
}
|
|
1910
|
+
function resolveEventGroupFolder(eventName, conventions, relatedTags) {
|
|
1911
|
+
const groupingRule = conventions.eventsGrouping;
|
|
1912
|
+
if (!groupingRule || groupingRule.strategy === "none") {
|
|
1913
|
+
return "";
|
|
1914
|
+
}
|
|
1915
|
+
return applyGroupingStrategy(groupingRule, {
|
|
1916
|
+
name: eventName,
|
|
1917
|
+
tags: relatedTags ?? []
|
|
1918
|
+
});
|
|
1919
|
+
}
|
|
1920
|
+
function applyGroupingStrategy(rule, context) {
|
|
1921
|
+
switch (rule.strategy) {
|
|
1922
|
+
case "by-tag":
|
|
1923
|
+
return context.tags?.[0] ?? "untagged";
|
|
1924
|
+
case "by-owner":
|
|
1925
|
+
return context.owners?.[0] ?? "unowned";
|
|
1926
|
+
case "by-domain":
|
|
1927
|
+
return extractDomain(context.name);
|
|
1928
|
+
case "by-url-path-single":
|
|
1929
|
+
return extractUrlPathLevel(context.path, 1);
|
|
1930
|
+
case "by-url-path-multi":
|
|
1931
|
+
return extractUrlPathLevel(context.path, rule.urlPathLevel ?? 2);
|
|
1932
|
+
case "by-feature":
|
|
1933
|
+
return extractDomain(context.name);
|
|
1934
|
+
case "none":
|
|
1935
|
+
default:
|
|
1936
|
+
return "";
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
function extractDomain(name) {
|
|
1940
|
+
if (name.includes(".")) {
|
|
1941
|
+
return name.split(".")[0] ?? "default";
|
|
1942
|
+
}
|
|
1943
|
+
if (name.includes("_")) {
|
|
1944
|
+
return name.split("_")[0] ?? "default";
|
|
1945
|
+
}
|
|
1946
|
+
const match = name.match(/^([a-z]+)/i);
|
|
1947
|
+
return match?.[1]?.toLowerCase() ?? "default";
|
|
1948
|
+
}
|
|
1949
|
+
function extractUrlPathLevel(path, level) {
|
|
1950
|
+
if (!path)
|
|
1951
|
+
return "root";
|
|
1952
|
+
const segments = path.split("/").filter(Boolean);
|
|
1953
|
+
const nonParamSegments = segments.filter((s) => !s.startsWith("{"));
|
|
1954
|
+
if (nonParamSegments.length === 0)
|
|
1955
|
+
return "root";
|
|
1956
|
+
return nonParamSegments.slice(0, level).join("/");
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
// src/openapi/importer/index.ts
|
|
1960
|
+
var importFromOpenApi = (parseResult, contractspecOptions, importOptions = {}) => {
|
|
1961
|
+
const { tags, exclude = [], include } = importOptions;
|
|
1962
|
+
const specs = [];
|
|
1963
|
+
const skipped = [];
|
|
1964
|
+
const errors = [];
|
|
1965
|
+
for (const operation2 of parseResult.operations) {
|
|
1966
|
+
if (tags && tags.length > 0) {
|
|
1967
|
+
const hasMatchingTag = operation2.tags.some((t) => tags.includes(t));
|
|
1968
|
+
if (!hasMatchingTag) {
|
|
1969
|
+
skipped.push({
|
|
1970
|
+
sourceId: operation2.operationId,
|
|
1971
|
+
reason: `No matching tags (has: ${operation2.tags.join(", ")})`
|
|
1972
|
+
});
|
|
1973
|
+
continue;
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
if (include && include.length > 0) {
|
|
1977
|
+
if (!include.includes(operation2.operationId)) {
|
|
1978
|
+
skipped.push({
|
|
1979
|
+
sourceId: operation2.operationId,
|
|
1980
|
+
reason: "Not in include list"
|
|
1981
|
+
});
|
|
1982
|
+
continue;
|
|
1983
|
+
}
|
|
1984
|
+
} else if (exclude.includes(operation2.operationId)) {
|
|
1985
|
+
skipped.push({
|
|
1986
|
+
sourceId: operation2.operationId,
|
|
1987
|
+
reason: "In exclude list"
|
|
1988
|
+
});
|
|
1989
|
+
continue;
|
|
1990
|
+
}
|
|
1991
|
+
if (operation2.deprecated && importOptions.defaultStability !== "deprecated") {
|
|
1992
|
+
skipped.push({
|
|
1993
|
+
sourceId: operation2.operationId,
|
|
1994
|
+
reason: "Deprecated operation"
|
|
1995
|
+
});
|
|
1996
|
+
continue;
|
|
1997
|
+
}
|
|
1998
|
+
try {
|
|
1999
|
+
const inputSchemas = buildInputSchemas(operation2);
|
|
2000
|
+
const schemaFormat = importOptions.schemaFormat || contractspecOptions.schemaFormat || "contractspec";
|
|
2001
|
+
const inputModel = inputSchemas.body ? generateSchemaModelCode(inputSchemas.body, `${operation2.operationId}Input`, schemaFormat, contractspecOptions) : null;
|
|
2002
|
+
const queryModel = inputSchemas.query ? generateSchemaModelCode(inputSchemas.query, `${operation2.operationId}Query`, schemaFormat, contractspecOptions) : null;
|
|
2003
|
+
const paramsModel = inputSchemas.params ? generateSchemaModelCode(inputSchemas.params, `${operation2.operationId}Params`, schemaFormat, contractspecOptions) : null;
|
|
2004
|
+
const headersModel = inputSchemas.headers ? generateSchemaModelCode(inputSchemas.headers, `${operation2.operationId}Headers`, schemaFormat, contractspecOptions) : null;
|
|
2005
|
+
const outputSchema = getOutputSchema(operation2);
|
|
2006
|
+
let outputModel = outputSchema ? generateSchemaModelCode(outputSchema, `${operation2.operationId}Output`, schemaFormat, contractspecOptions) : null;
|
|
2007
|
+
if (outputModel && schemaFormat === "contractspec" && !outputModel.code.includes("defineSchemaModel")) {
|
|
2008
|
+
outputModel = null;
|
|
2009
|
+
}
|
|
2010
|
+
const code = generateSpecCode(operation2, contractspecOptions, importOptions, inputModel, outputModel, queryModel, paramsModel, headersModel);
|
|
2011
|
+
const specName = toSpecKey(operation2.operationId, importOptions.prefix);
|
|
2012
|
+
const fileName = toFileName(specName);
|
|
2013
|
+
const transportHints = {
|
|
2014
|
+
rest: {
|
|
2015
|
+
method: operation2.method.toUpperCase(),
|
|
2016
|
+
path: operation2.path,
|
|
2017
|
+
params: {
|
|
2018
|
+
path: operation2.pathParams.map((p) => p.name),
|
|
2019
|
+
query: operation2.queryParams.map((p) => p.name),
|
|
2020
|
+
header: operation2.headerParams.map((p) => p.name),
|
|
2021
|
+
cookie: operation2.cookieParams.map((p) => p.name)
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
};
|
|
2025
|
+
const source = {
|
|
2026
|
+
type: "openapi",
|
|
2027
|
+
sourceId: operation2.operationId,
|
|
2028
|
+
operationId: operation2.operationId,
|
|
2029
|
+
openApiVersion: parseResult.version,
|
|
2030
|
+
importedAt: new Date
|
|
2031
|
+
};
|
|
2032
|
+
const groupFolder = resolveOperationGroupFolder(operation2, contractspecOptions.conventions);
|
|
2033
|
+
specs.push({
|
|
2034
|
+
code,
|
|
2035
|
+
fileName,
|
|
2036
|
+
groupFolder: groupFolder || undefined,
|
|
2037
|
+
source,
|
|
2038
|
+
transportHints
|
|
2039
|
+
});
|
|
2040
|
+
} catch (error) {
|
|
2041
|
+
errors.push({
|
|
2042
|
+
sourceId: operation2.operationId,
|
|
2043
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
for (const [name, schema] of Object.entries(parseResult.schemas)) {
|
|
2048
|
+
try {
|
|
2049
|
+
const code = generateModelCode(name, schema, {
|
|
2050
|
+
...contractspecOptions,
|
|
2051
|
+
schemaFormat: importOptions.schemaFormat || contractspecOptions.schemaFormat
|
|
2052
|
+
});
|
|
2053
|
+
const fileName = toFileName(toSpecKey(name, importOptions.prefix));
|
|
2054
|
+
const groupFolder = resolveModelGroupFolder(name, contractspecOptions.conventions);
|
|
2055
|
+
specs.push({
|
|
2056
|
+
code,
|
|
2057
|
+
fileName,
|
|
2058
|
+
groupFolder: groupFolder || undefined,
|
|
2059
|
+
source: {
|
|
2060
|
+
type: "openapi",
|
|
2061
|
+
sourceId: name,
|
|
2062
|
+
operationId: name,
|
|
2063
|
+
openApiVersion: parseResult.version,
|
|
2064
|
+
importedAt: new Date
|
|
2065
|
+
},
|
|
2066
|
+
transportHints: {}
|
|
2067
|
+
});
|
|
2068
|
+
} catch (error) {
|
|
2069
|
+
errors.push({
|
|
2070
|
+
sourceId: name,
|
|
2071
|
+
error: error instanceof Error ? "Model conversion failed: " + error.message : String(error)
|
|
2072
|
+
});
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
for (const event of parseResult.events) {
|
|
2076
|
+
try {
|
|
2077
|
+
const code = generateEventCode(event, {
|
|
2078
|
+
...contractspecOptions,
|
|
2079
|
+
schemaFormat: importOptions.schemaFormat || contractspecOptions.schemaFormat
|
|
2080
|
+
});
|
|
2081
|
+
const fileName = toFileName(toSpecKey(event.name, importOptions.prefix));
|
|
2082
|
+
const groupFolder = resolveEventGroupFolder(event.name, contractspecOptions.conventions);
|
|
2083
|
+
specs.push({
|
|
2084
|
+
code,
|
|
2085
|
+
fileName,
|
|
2086
|
+
groupFolder: groupFolder || undefined,
|
|
2087
|
+
source: {
|
|
2088
|
+
type: "openapi",
|
|
2089
|
+
sourceId: event.name,
|
|
2090
|
+
operationId: event.name,
|
|
2091
|
+
openApiVersion: parseResult.version,
|
|
2092
|
+
importedAt: new Date
|
|
2093
|
+
},
|
|
2094
|
+
transportHints: {}
|
|
2095
|
+
});
|
|
2096
|
+
} catch (error) {
|
|
2097
|
+
errors.push({
|
|
2098
|
+
sourceId: event.name,
|
|
2099
|
+
error: error instanceof Error ? "Event conversion failed: " + error.message : String(error)
|
|
2100
|
+
});
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
return {
|
|
2104
|
+
operationSpecs: specs,
|
|
2105
|
+
skipped,
|
|
2106
|
+
errors,
|
|
2107
|
+
summary: {
|
|
2108
|
+
total: parseResult.operations.length + Object.keys(parseResult.schemas).length + parseResult.events.length,
|
|
2109
|
+
imported: specs.length,
|
|
2110
|
+
skipped: skipped.length,
|
|
2111
|
+
errors: errors.length
|
|
2112
|
+
}
|
|
2113
|
+
};
|
|
2114
|
+
};
|
|
2115
|
+
function importOperation(operation2, options = {}, contractspecOptions) {
|
|
2116
|
+
const inputSchemas = buildInputSchemas(operation2);
|
|
2117
|
+
const schemaFormat = options.schemaFormat || contractspecOptions.schemaFormat || "contractspec";
|
|
2118
|
+
const inputModel = inputSchemas.body ? generateSchemaModelCode(inputSchemas.body, `${operation2.operationId}Input`, schemaFormat, contractspecOptions) : null;
|
|
2119
|
+
const queryModel = inputSchemas.query ? generateSchemaModelCode(inputSchemas.query, `${operation2.operationId}Query`, schemaFormat, contractspecOptions) : null;
|
|
2120
|
+
const paramsModel = inputSchemas.params ? generateSchemaModelCode(inputSchemas.params, `${operation2.operationId}Params`, schemaFormat, contractspecOptions) : null;
|
|
2121
|
+
const headersModel = inputSchemas.headers ? generateSchemaModelCode(inputSchemas.headers, `${operation2.operationId}Headers`, schemaFormat, contractspecOptions) : null;
|
|
2122
|
+
const outputSchema = getOutputSchema(operation2);
|
|
2123
|
+
const outputModel = outputSchema ? generateSchemaModelCode(outputSchema, `${operation2.operationId}Output`, schemaFormat, contractspecOptions) : null;
|
|
2124
|
+
return generateSpecCode(operation2, contractspecOptions, options, inputModel, outputModel, queryModel, paramsModel, headersModel);
|
|
2125
|
+
}
|
|
2126
|
+
// src/openapi/differ.ts
|
|
2127
|
+
function compareValues(path, oldValue, newValue, description) {
|
|
2128
|
+
if (deepEqual(oldValue, newValue)) {
|
|
2129
|
+
return null;
|
|
2130
|
+
}
|
|
2131
|
+
let changeType = "modified";
|
|
2132
|
+
if (oldValue === undefined || oldValue === null) {
|
|
2133
|
+
changeType = "added";
|
|
2134
|
+
} else if (newValue === undefined || newValue === null) {
|
|
2135
|
+
changeType = "removed";
|
|
2136
|
+
} else if (typeof oldValue !== typeof newValue) {
|
|
2137
|
+
changeType = "type_changed";
|
|
2138
|
+
}
|
|
2139
|
+
return {
|
|
2140
|
+
path,
|
|
2141
|
+
type: changeType,
|
|
2142
|
+
oldValue,
|
|
2143
|
+
newValue,
|
|
2144
|
+
description
|
|
2145
|
+
};
|
|
2146
|
+
}
|
|
2147
|
+
function diffObjects(path, oldObj, newObj, options) {
|
|
2148
|
+
const changes = [];
|
|
2149
|
+
if (!oldObj && !newObj)
|
|
2150
|
+
return changes;
|
|
2151
|
+
if (!oldObj) {
|
|
2152
|
+
changes.push({
|
|
2153
|
+
path,
|
|
2154
|
+
type: "added",
|
|
2155
|
+
newValue: newObj,
|
|
2156
|
+
description: `Added ${path}`
|
|
2157
|
+
});
|
|
2158
|
+
return changes;
|
|
2159
|
+
}
|
|
2160
|
+
if (!newObj) {
|
|
2161
|
+
changes.push({
|
|
2162
|
+
path,
|
|
2163
|
+
type: "removed",
|
|
2164
|
+
oldValue: oldObj,
|
|
2165
|
+
description: `Removed ${path}`
|
|
2166
|
+
});
|
|
2167
|
+
return changes;
|
|
2168
|
+
}
|
|
2169
|
+
const allKeys = new Set([...Object.keys(oldObj), ...Object.keys(newObj)]);
|
|
2170
|
+
for (const key of allKeys) {
|
|
2171
|
+
const keyPath = path ? `${path}.${key}` : key;
|
|
2172
|
+
if (options.ignorePaths?.some((p) => keyPath.startsWith(p))) {
|
|
2173
|
+
continue;
|
|
2174
|
+
}
|
|
2175
|
+
const oldVal = oldObj[key];
|
|
2176
|
+
const newVal = newObj[key];
|
|
2177
|
+
if (typeof oldVal === "object" && typeof newVal === "object") {
|
|
2178
|
+
changes.push(...diffObjects(keyPath, oldVal, newVal, options));
|
|
2179
|
+
} else {
|
|
2180
|
+
const change = compareValues(keyPath, oldVal, newVal, `Changed ${keyPath}`);
|
|
2181
|
+
if (change) {
|
|
2182
|
+
changes.push(change);
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
return changes;
|
|
2187
|
+
}
|
|
2188
|
+
function diffSpecVsOperation(spec, operation2, options = {}) {
|
|
2189
|
+
const changes = [];
|
|
2190
|
+
if (!options.ignoreDescriptions) {
|
|
2191
|
+
const descChange = compareValues("meta.description", spec.meta.description, operation2.summary ?? operation2.description, "Description changed");
|
|
2192
|
+
if (descChange)
|
|
2193
|
+
changes.push(descChange);
|
|
2194
|
+
}
|
|
2195
|
+
if (!options.ignoreTags) {
|
|
2196
|
+
const oldTags = [...spec.meta.tags ?? []].sort();
|
|
2197
|
+
const newTags = [...operation2.tags].sort();
|
|
2198
|
+
if (!deepEqual(oldTags, newTags)) {
|
|
2199
|
+
changes.push({
|
|
2200
|
+
path: "meta.tags",
|
|
2201
|
+
type: "modified",
|
|
2202
|
+
oldValue: oldTags,
|
|
2203
|
+
newValue: newTags,
|
|
2204
|
+
description: "Tags changed"
|
|
2205
|
+
});
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
if (!options.ignoreTransport) {
|
|
2209
|
+
const specMethod = spec.transport?.rest?.method ?? (spec.meta.kind === "query" ? "GET" : "POST");
|
|
2210
|
+
const opMethod = operation2.method.toUpperCase();
|
|
2211
|
+
if (specMethod !== opMethod) {
|
|
2212
|
+
changes.push({
|
|
2213
|
+
path: "transport.rest.method",
|
|
2214
|
+
type: "modified",
|
|
2215
|
+
oldValue: specMethod,
|
|
2216
|
+
newValue: opMethod,
|
|
2217
|
+
description: "HTTP method changed"
|
|
2218
|
+
});
|
|
2219
|
+
}
|
|
2220
|
+
const specPath = spec.transport?.rest?.path;
|
|
2221
|
+
if (specPath && specPath !== operation2.path) {
|
|
2222
|
+
changes.push({
|
|
2223
|
+
path: "transport.rest.path",
|
|
2224
|
+
type: "modified",
|
|
2225
|
+
oldValue: specPath,
|
|
2226
|
+
newValue: operation2.path,
|
|
2227
|
+
description: "Path changed"
|
|
2228
|
+
});
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
const specDeprecated = spec.meta.stability === "deprecated";
|
|
2232
|
+
if (specDeprecated !== operation2.deprecated) {
|
|
2233
|
+
changes.push({
|
|
2234
|
+
path: "meta.stability",
|
|
2235
|
+
type: "modified",
|
|
2236
|
+
oldValue: spec.meta.stability,
|
|
2237
|
+
newValue: operation2.deprecated ? "deprecated" : "stable",
|
|
2238
|
+
description: "Deprecation status changed"
|
|
2239
|
+
});
|
|
2240
|
+
}
|
|
2241
|
+
return changes;
|
|
2242
|
+
}
|
|
2243
|
+
function diffSpecs(oldSpec, newSpec, options = {}) {
|
|
2244
|
+
const changes = [];
|
|
2245
|
+
const metaChanges = diffObjects("meta", oldSpec.meta, newSpec.meta, {
|
|
2246
|
+
...options,
|
|
2247
|
+
ignorePaths: [
|
|
2248
|
+
...options.ignorePaths ?? [],
|
|
2249
|
+
...options.ignoreDescriptions ? ["meta.description", "meta.goal", "meta.context"] : [],
|
|
2250
|
+
...options.ignoreTags ? ["meta.tags"] : []
|
|
2251
|
+
]
|
|
2252
|
+
});
|
|
2253
|
+
changes.push(...metaChanges);
|
|
2254
|
+
if (!options.ignoreTransport) {
|
|
2255
|
+
const transportChanges = diffObjects("transport", oldSpec.transport, newSpec.transport, options);
|
|
2256
|
+
changes.push(...transportChanges);
|
|
2257
|
+
}
|
|
2258
|
+
const policyChanges = diffObjects("policy", oldSpec.policy, newSpec.policy, options);
|
|
2259
|
+
changes.push(...policyChanges);
|
|
2260
|
+
return changes;
|
|
2261
|
+
}
|
|
2262
|
+
function createSpecDiff(operationId, existing, incoming, options = {}) {
|
|
2263
|
+
let changes = [];
|
|
2264
|
+
let isEquivalent = false;
|
|
2265
|
+
if (existing && incoming.operationSpec) {
|
|
2266
|
+
changes = diffSpecs(existing, incoming.operationSpec, options);
|
|
2267
|
+
isEquivalent = changes.length === 0;
|
|
2268
|
+
} else if (existing && !incoming.operationSpec) {
|
|
2269
|
+
changes = [
|
|
2270
|
+
{
|
|
2271
|
+
path: "",
|
|
2272
|
+
type: "modified",
|
|
2273
|
+
oldValue: existing,
|
|
2274
|
+
newValue: incoming.code,
|
|
2275
|
+
description: "Spec code imported from OpenAPI (runtime comparison not available)"
|
|
2276
|
+
}
|
|
2277
|
+
];
|
|
2278
|
+
} else {
|
|
2279
|
+
changes = [
|
|
2280
|
+
{
|
|
2281
|
+
path: "",
|
|
2282
|
+
type: "added",
|
|
2283
|
+
newValue: incoming.operationSpec ?? incoming.code,
|
|
2284
|
+
description: "New spec imported from OpenAPI"
|
|
2285
|
+
}
|
|
2286
|
+
];
|
|
2287
|
+
}
|
|
2288
|
+
return {
|
|
2289
|
+
operationId,
|
|
2290
|
+
existing,
|
|
2291
|
+
incoming,
|
|
2292
|
+
changes,
|
|
2293
|
+
isEquivalent
|
|
2294
|
+
};
|
|
2295
|
+
}
|
|
2296
|
+
function diffAll(existingSpecs, importedSpecs, options = {}) {
|
|
2297
|
+
const diffs = [];
|
|
2298
|
+
const matchedExisting = new Set;
|
|
2299
|
+
for (const imported of importedSpecs) {
|
|
2300
|
+
const operationId = imported.source.sourceId;
|
|
2301
|
+
let existing;
|
|
2302
|
+
for (const [key, spec] of existingSpecs) {
|
|
2303
|
+
const specName = spec.meta.key;
|
|
2304
|
+
if (key === operationId || specName.includes(operationId)) {
|
|
2305
|
+
existing = spec;
|
|
2306
|
+
matchedExisting.add(key);
|
|
2307
|
+
break;
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
diffs.push(createSpecDiff(operationId, existing, imported, options));
|
|
2311
|
+
}
|
|
2312
|
+
for (const [key, spec] of existingSpecs) {
|
|
2313
|
+
if (!matchedExisting.has(key)) {
|
|
2314
|
+
diffs.push({
|
|
2315
|
+
operationId: key,
|
|
2316
|
+
existing: spec,
|
|
2317
|
+
incoming: undefined,
|
|
2318
|
+
changes: [
|
|
2319
|
+
{
|
|
2320
|
+
path: "",
|
|
2321
|
+
type: "removed",
|
|
2322
|
+
oldValue: spec,
|
|
2323
|
+
description: "Spec no longer exists in OpenAPI source"
|
|
2324
|
+
}
|
|
2325
|
+
],
|
|
2326
|
+
isEquivalent: false
|
|
2327
|
+
});
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
return diffs;
|
|
2331
|
+
}
|
|
2332
|
+
function formatDiffChanges(changes) {
|
|
2333
|
+
if (changes.length === 0) {
|
|
2334
|
+
return "No changes detected";
|
|
2335
|
+
}
|
|
2336
|
+
const lines = [];
|
|
2337
|
+
for (const change of changes) {
|
|
2338
|
+
const prefix = {
|
|
2339
|
+
added: "+",
|
|
2340
|
+
removed: "-",
|
|
2341
|
+
modified: "~",
|
|
2342
|
+
type_changed: "!",
|
|
2343
|
+
required_changed: "?"
|
|
2344
|
+
}[change.type];
|
|
2345
|
+
lines.push(`${prefix} ${change.path}: ${change.description}`);
|
|
2346
|
+
if (change.type === "modified" || change.type === "type_changed") {
|
|
2347
|
+
lines.push(` old: ${JSON.stringify(change.oldValue)}`);
|
|
2348
|
+
lines.push(` new: ${JSON.stringify(change.newValue)}`);
|
|
2349
|
+
} else if (change.type === "added") {
|
|
2350
|
+
lines.push(` value: ${JSON.stringify(change.newValue)}`);
|
|
2351
|
+
} else if (change.type === "removed") {
|
|
2352
|
+
lines.push(` was: ${JSON.stringify(change.oldValue)}`);
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
return lines.join(`
|
|
2356
|
+
`);
|
|
2357
|
+
}
|
|
2358
|
+
export {
|
|
2359
|
+
toValidIdentifier,
|
|
2360
|
+
toSpecKey,
|
|
2361
|
+
toSnakeCase,
|
|
2362
|
+
toSchemaName,
|
|
2363
|
+
toRestPath,
|
|
2364
|
+
toPascalCase,
|
|
2365
|
+
toOperationId,
|
|
2366
|
+
toKebabCase,
|
|
2367
|
+
toHttpMethod,
|
|
2368
|
+
toFileName,
|
|
2369
|
+
toCamelCase,
|
|
2370
|
+
schemaModelToJsonSchema,
|
|
2371
|
+
parseOpenApiString,
|
|
2372
|
+
parseOpenApiDocument,
|
|
2373
|
+
parseOpenApi,
|
|
2374
|
+
openApiToYaml,
|
|
2375
|
+
openApiToJson,
|
|
2376
|
+
openApiForRegistry,
|
|
2377
|
+
normalizePath,
|
|
2378
|
+
jsonSchemaToType,
|
|
2379
|
+
jsonSchemaForSpec,
|
|
2380
|
+
importOperation,
|
|
2381
|
+
importFromOpenApi,
|
|
2382
|
+
getScalarType,
|
|
2383
|
+
getByPath,
|
|
2384
|
+
generateWorkflowsRegistry,
|
|
2385
|
+
generateSchemaModelCode,
|
|
2386
|
+
generateRegistryIndex,
|
|
2387
|
+
generatePresentationsRegistry,
|
|
2388
|
+
generateOperationsRegistry,
|
|
2389
|
+
generateImports,
|
|
2390
|
+
generateFormsRegistry,
|
|
2391
|
+
generateFeaturesRegistry,
|
|
2392
|
+
generateEventsExports,
|
|
2393
|
+
generateDataViewsRegistry,
|
|
2394
|
+
formatDiffChanges,
|
|
2395
|
+
extractPathParams,
|
|
2396
|
+
exportWorkflows,
|
|
2397
|
+
exportPresentationsFromArray,
|
|
2398
|
+
exportPresentations,
|
|
2399
|
+
exportOperations,
|
|
2400
|
+
exportForms,
|
|
2401
|
+
exportFeatures,
|
|
2402
|
+
exportEvents,
|
|
2403
|
+
exportDataViews,
|
|
2404
|
+
exportContractSpec,
|
|
2405
|
+
diffSpecs,
|
|
2406
|
+
diffSpecVsOperation,
|
|
2407
|
+
diffAll,
|
|
2408
|
+
detectVersion,
|
|
2409
|
+
detectFormat,
|
|
2410
|
+
defaultRestPath,
|
|
2411
|
+
deepEqual,
|
|
2412
|
+
createSpecDiff,
|
|
2413
|
+
contractSpecToYaml,
|
|
2414
|
+
contractSpecToJson
|
|
2415
|
+
};
|