@contractspec/lib.contracts-transformers 3.7.6 → 3.7.7
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/README.md +41 -73
- package/dist/browser/index.js +900 -896
- package/dist/browser/openapi/index.js +965 -961
- package/dist/index.d.ts +1 -1
- package/dist/index.js +900 -896
- package/dist/node/index.js +900 -896
- package/dist/node/openapi/index.js +965 -961
- package/dist/openapi/differ.d.ts +1 -1
- package/dist/openapi/exporter/index.d.ts +7 -7
- package/dist/openapi/exporter/presentations.d.ts +1 -1
- package/dist/openapi/exporter.d.ts +2 -2
- package/dist/openapi/importer/events.d.ts +1 -1
- package/dist/openapi/importer/generator.d.ts +2 -2
- package/dist/openapi/importer/index.d.ts +5 -5
- package/dist/openapi/importer/models.d.ts +1 -1
- package/dist/openapi/index.d.ts +5 -5
- package/dist/openapi/index.js +965 -961
- package/dist/openapi/parser/index.d.ts +4 -4
- package/dist/openapi/parser/operation.d.ts +1 -1
- package/dist/openapi/parser/resolvers.d.ts +1 -1
- package/dist/openapi/parser/utils.d.ts +1 -1
- package/dist/openapi/schema-converter.d.ts +1 -1
- package/package.json +4 -4
package/dist/openapi/index.js
CHANGED
|
@@ -1,56 +1,60 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
// src/
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
"get",
|
|
6
|
-
"post",
|
|
7
|
-
"put",
|
|
8
|
-
"delete",
|
|
9
|
-
"patch",
|
|
10
|
-
"head",
|
|
11
|
-
"options",
|
|
12
|
-
"trace"
|
|
13
|
-
];
|
|
14
|
-
function parseOpenApiString(content, format = "json") {
|
|
15
|
-
if (format === "yaml") {
|
|
16
|
-
return parseYaml(content);
|
|
17
|
-
}
|
|
18
|
-
return JSON.parse(content);
|
|
2
|
+
// src/common/utils.ts
|
|
3
|
+
function toPascalCase(str) {
|
|
4
|
+
return str.replace(/[-_./\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^./, (c) => c.toUpperCase());
|
|
19
5
|
}
|
|
20
|
-
function
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
return "json";
|
|
24
|
-
}
|
|
25
|
-
return "yaml";
|
|
6
|
+
function toCamelCase(str) {
|
|
7
|
+
const pascal = toPascalCase(str);
|
|
8
|
+
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
26
9
|
}
|
|
27
|
-
function
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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;
|
|
31
20
|
}
|
|
32
|
-
return
|
|
21
|
+
return result;
|
|
33
22
|
}
|
|
34
|
-
function
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
return "By" + part.slice(1, -1).charAt(0).toUpperCase() + part.slice(2, -1);
|
|
38
|
-
}
|
|
39
|
-
return part.charAt(0).toUpperCase() + part.slice(1);
|
|
40
|
-
});
|
|
41
|
-
return method + pathParts.join("");
|
|
23
|
+
function toSpecKey(operationId, prefix) {
|
|
24
|
+
const key = toCamelCase(operationId);
|
|
25
|
+
return prefix ? `${prefix}.${key}` : key;
|
|
42
26
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
return typeof obj === "object" && obj !== null && "$ref" in obj;
|
|
27
|
+
function toFileName(specName) {
|
|
28
|
+
return toKebabCase(specName.replace(/\./g, "-")) + ".ts";
|
|
46
29
|
}
|
|
47
|
-
function
|
|
48
|
-
if (
|
|
49
|
-
return;
|
|
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;
|
|
50
51
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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) {
|
|
54
58
|
if (current === null || current === undefined)
|
|
55
59
|
return;
|
|
56
60
|
if (typeof current !== "object")
|
|
@@ -59,246 +63,405 @@ function resolveRef(doc, ref) {
|
|
|
59
63
|
}
|
|
60
64
|
return current;
|
|
61
65
|
}
|
|
62
|
-
function
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
return schema;
|
|
77
|
-
const refParts = schema.$ref.split("/");
|
|
78
|
-
const typeName = refParts[refParts.length - 1];
|
|
79
|
-
return {
|
|
80
|
-
...dereferenced,
|
|
81
|
-
_originalRef: schema.$ref,
|
|
82
|
-
_originalTypeName: typeName
|
|
83
|
-
};
|
|
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
|
+
|
|
76
|
+
// src/openapi/differ.ts
|
|
77
|
+
function compareValues(path, oldValue, newValue, description) {
|
|
78
|
+
if (deepEqual(oldValue, newValue)) {
|
|
79
|
+
return null;
|
|
84
80
|
}
|
|
85
|
-
|
|
86
|
-
if (
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
schemaObj.properties = newProps;
|
|
81
|
+
let changeType = "modified";
|
|
82
|
+
if (oldValue === undefined || oldValue === null) {
|
|
83
|
+
changeType = "added";
|
|
84
|
+
} else if (newValue === undefined || newValue === null) {
|
|
85
|
+
changeType = "removed";
|
|
86
|
+
} else if (typeof oldValue !== typeof newValue) {
|
|
87
|
+
changeType = "type_changed";
|
|
93
88
|
}
|
|
94
|
-
|
|
95
|
-
|
|
89
|
+
return {
|
|
90
|
+
path,
|
|
91
|
+
type: changeType,
|
|
92
|
+
oldValue,
|
|
93
|
+
newValue,
|
|
94
|
+
description
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function diffObjects(path, oldObj, newObj, options) {
|
|
98
|
+
const changes = [];
|
|
99
|
+
if (!oldObj && !newObj)
|
|
100
|
+
return changes;
|
|
101
|
+
if (!oldObj) {
|
|
102
|
+
changes.push({
|
|
103
|
+
path,
|
|
104
|
+
type: "added",
|
|
105
|
+
newValue: newObj,
|
|
106
|
+
description: `Added ${path}`
|
|
107
|
+
});
|
|
108
|
+
return changes;
|
|
96
109
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
110
|
+
if (!newObj) {
|
|
111
|
+
changes.push({
|
|
112
|
+
path,
|
|
113
|
+
type: "removed",
|
|
114
|
+
oldValue: oldObj,
|
|
115
|
+
description: `Removed ${path}`
|
|
116
|
+
});
|
|
117
|
+
return changes;
|
|
102
118
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (!params)
|
|
114
|
-
return result;
|
|
115
|
-
for (const param of params) {
|
|
116
|
-
let resolved;
|
|
117
|
-
if (isReference(param)) {
|
|
118
|
-
const ref = resolveRef(doc, param.$ref);
|
|
119
|
-
if (!ref)
|
|
120
|
-
continue;
|
|
121
|
-
resolved = ref;
|
|
119
|
+
const allKeys = new Set([...Object.keys(oldObj), ...Object.keys(newObj)]);
|
|
120
|
+
for (const key of allKeys) {
|
|
121
|
+
const keyPath = path ? `${path}.${key}` : key;
|
|
122
|
+
if (options.ignorePaths?.some((p) => keyPath.startsWith(p))) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const oldVal = oldObj[key];
|
|
126
|
+
const newVal = newObj[key];
|
|
127
|
+
if (typeof oldVal === "object" && typeof newVal === "object") {
|
|
128
|
+
changes.push(...diffObjects(keyPath, oldVal, newVal, options));
|
|
122
129
|
} else {
|
|
123
|
-
|
|
130
|
+
const change = compareValues(keyPath, oldVal, newVal, `Changed ${keyPath}`);
|
|
131
|
+
if (change) {
|
|
132
|
+
changes.push(change);
|
|
133
|
+
}
|
|
124
134
|
}
|
|
125
|
-
const parsed = {
|
|
126
|
-
name: resolved.name,
|
|
127
|
-
in: resolved.in,
|
|
128
|
-
required: resolved.required ?? resolved.in === "path",
|
|
129
|
-
description: resolved.description,
|
|
130
|
-
schema: dereferenceSchema(doc, resolved.schema),
|
|
131
|
-
deprecated: resolved.deprecated ?? false
|
|
132
|
-
};
|
|
133
|
-
result[resolved.in]?.push(parsed);
|
|
134
135
|
}
|
|
135
|
-
return
|
|
136
|
+
return changes;
|
|
136
137
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
138
|
+
function diffSpecVsOperation(spec, operation, options = {}) {
|
|
139
|
+
const changes = [];
|
|
140
|
+
if (!options.ignoreDescriptions) {
|
|
141
|
+
const descChange = compareValues("meta.description", spec.meta.description, operation.summary ?? operation.description, "Description changed");
|
|
142
|
+
if (descChange)
|
|
143
|
+
changes.push(descChange);
|
|
144
|
+
}
|
|
145
|
+
if (!options.ignoreTags) {
|
|
146
|
+
const oldTags = [...spec.meta.tags ?? []].sort();
|
|
147
|
+
const newTags = [...operation.tags].sort();
|
|
148
|
+
if (!deepEqual(oldTags, newTags)) {
|
|
149
|
+
changes.push({
|
|
150
|
+
path: "meta.tags",
|
|
151
|
+
type: "modified",
|
|
152
|
+
oldValue: oldTags,
|
|
153
|
+
newValue: newTags,
|
|
154
|
+
description: "Tags changed"
|
|
155
|
+
});
|
|
154
156
|
}
|
|
155
157
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
if (
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
};
|
|
158
|
+
if (!options.ignoreTransport) {
|
|
159
|
+
const specMethod = spec.transport?.rest?.method ?? (spec.meta.kind === "query" ? "GET" : "POST");
|
|
160
|
+
const opMethod = operation.method.toUpperCase();
|
|
161
|
+
if (specMethod !== opMethod) {
|
|
162
|
+
changes.push({
|
|
163
|
+
path: "transport.rest.method",
|
|
164
|
+
type: "modified",
|
|
165
|
+
oldValue: specMethod,
|
|
166
|
+
newValue: opMethod,
|
|
167
|
+
description: "HTTP method changed"
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
const specPath = spec.transport?.rest?.path;
|
|
171
|
+
if (specPath && specPath !== operation.path) {
|
|
172
|
+
changes.push({
|
|
173
|
+
path: "transport.rest.path",
|
|
174
|
+
type: "modified",
|
|
175
|
+
oldValue: specPath,
|
|
176
|
+
newValue: operation.path,
|
|
177
|
+
description: "Path changed"
|
|
178
|
+
});
|
|
167
179
|
}
|
|
168
180
|
}
|
|
169
|
-
const
|
|
181
|
+
const specDeprecated = spec.meta.stability === "deprecated";
|
|
182
|
+
if (specDeprecated !== operation.deprecated) {
|
|
183
|
+
changes.push({
|
|
184
|
+
path: "meta.stability",
|
|
185
|
+
type: "modified",
|
|
186
|
+
oldValue: spec.meta.stability,
|
|
187
|
+
newValue: operation.deprecated ? "deprecated" : "stable",
|
|
188
|
+
description: "Deprecation status changed"
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
return changes;
|
|
192
|
+
}
|
|
193
|
+
function diffSpecs(oldSpec, newSpec, options = {}) {
|
|
194
|
+
const changes = [];
|
|
195
|
+
const metaChanges = diffObjects("meta", oldSpec.meta, newSpec.meta, {
|
|
196
|
+
...options,
|
|
197
|
+
ignorePaths: [
|
|
198
|
+
...options.ignorePaths ?? [],
|
|
199
|
+
...options.ignoreDescriptions ? ["meta.description", "meta.goal", "meta.context"] : [],
|
|
200
|
+
...options.ignoreTags ? ["meta.tags"] : []
|
|
201
|
+
]
|
|
202
|
+
});
|
|
203
|
+
changes.push(...metaChanges);
|
|
204
|
+
if (!options.ignoreTransport) {
|
|
205
|
+
const transportChanges = diffObjects("transport", oldSpec.transport, newSpec.transport, options);
|
|
206
|
+
changes.push(...transportChanges);
|
|
207
|
+
}
|
|
208
|
+
const policyChanges = diffObjects("policy", oldSpec.policy, newSpec.policy, options);
|
|
209
|
+
changes.push(...policyChanges);
|
|
210
|
+
return changes;
|
|
211
|
+
}
|
|
212
|
+
function createSpecDiff(operationId, existing, incoming, options = {}) {
|
|
213
|
+
let changes = [];
|
|
214
|
+
let isEquivalent = false;
|
|
215
|
+
if (existing && incoming.operationSpec) {
|
|
216
|
+
changes = diffSpecs(existing, incoming.operationSpec, options);
|
|
217
|
+
isEquivalent = changes.length === 0;
|
|
218
|
+
} else if (existing && !incoming.operationSpec) {
|
|
219
|
+
changes = [
|
|
220
|
+
{
|
|
221
|
+
path: "",
|
|
222
|
+
type: "modified",
|
|
223
|
+
oldValue: existing,
|
|
224
|
+
newValue: incoming.code,
|
|
225
|
+
description: "Spec code imported from OpenAPI (runtime comparison not available)"
|
|
226
|
+
}
|
|
227
|
+
];
|
|
228
|
+
} else {
|
|
229
|
+
changes = [
|
|
230
|
+
{
|
|
231
|
+
path: "",
|
|
232
|
+
type: "added",
|
|
233
|
+
newValue: incoming.operationSpec ?? incoming.code,
|
|
234
|
+
description: "New spec imported from OpenAPI"
|
|
235
|
+
}
|
|
236
|
+
];
|
|
237
|
+
}
|
|
170
238
|
return {
|
|
171
|
-
operationId
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
tags: operation.tags ?? [],
|
|
177
|
-
pathParams: params.path,
|
|
178
|
-
queryParams: params.query,
|
|
179
|
-
headerParams: params.header,
|
|
180
|
-
cookieParams: params.cookie,
|
|
181
|
-
requestBody,
|
|
182
|
-
responses,
|
|
183
|
-
deprecated: operation.deprecated ?? false,
|
|
184
|
-
security: operation.security,
|
|
185
|
-
contractSpecMeta
|
|
239
|
+
operationId,
|
|
240
|
+
existing,
|
|
241
|
+
incoming,
|
|
242
|
+
changes,
|
|
243
|
+
isEquivalent
|
|
186
244
|
};
|
|
187
245
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
try {
|
|
201
|
-
operations.push(parseOperation(doc, method, path, operation, pathParams));
|
|
202
|
-
} catch (error) {
|
|
203
|
-
warnings.push(`Failed to parse ${method.toUpperCase()} ${path}: ${error}`);
|
|
204
|
-
}
|
|
246
|
+
function diffAll(existingSpecs, importedSpecs, options = {}) {
|
|
247
|
+
const diffs = [];
|
|
248
|
+
const matchedExisting = new Set;
|
|
249
|
+
for (const imported of importedSpecs) {
|
|
250
|
+
const operationId = imported.source.sourceId;
|
|
251
|
+
let existing;
|
|
252
|
+
for (const [key, spec] of existingSpecs) {
|
|
253
|
+
const specName = spec.meta.key;
|
|
254
|
+
if (key === operationId || specName.includes(operationId)) {
|
|
255
|
+
existing = spec;
|
|
256
|
+
matchedExisting.add(key);
|
|
257
|
+
break;
|
|
205
258
|
}
|
|
206
259
|
}
|
|
260
|
+
diffs.push(createSpecDiff(operationId, existing, imported, options));
|
|
207
261
|
}
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
262
|
+
for (const [key, spec] of existingSpecs) {
|
|
263
|
+
if (!matchedExisting.has(key)) {
|
|
264
|
+
diffs.push({
|
|
265
|
+
operationId: key,
|
|
266
|
+
existing: spec,
|
|
267
|
+
incoming: undefined,
|
|
268
|
+
changes: [
|
|
269
|
+
{
|
|
270
|
+
path: "",
|
|
271
|
+
type: "removed",
|
|
272
|
+
oldValue: spec,
|
|
273
|
+
description: "Spec no longer exists in OpenAPI source"
|
|
274
|
+
}
|
|
275
|
+
],
|
|
276
|
+
isEquivalent: false
|
|
277
|
+
});
|
|
213
278
|
}
|
|
214
279
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}
|
|
280
|
+
return diffs;
|
|
281
|
+
}
|
|
282
|
+
function formatDiffChanges(changes) {
|
|
283
|
+
if (changes.length === 0) {
|
|
284
|
+
return "No changes detected";
|
|
285
|
+
}
|
|
286
|
+
const lines = [];
|
|
287
|
+
for (const change of changes) {
|
|
288
|
+
const prefix = {
|
|
289
|
+
added: "+",
|
|
290
|
+
removed: "-",
|
|
291
|
+
modified: "~",
|
|
292
|
+
type_changed: "!",
|
|
293
|
+
required_changed: "?"
|
|
294
|
+
}[change.type];
|
|
295
|
+
lines.push(`${prefix} ${change.path}: ${change.description}`);
|
|
296
|
+
if (change.type === "modified" || change.type === "type_changed") {
|
|
297
|
+
lines.push(` old: ${JSON.stringify(change.oldValue)}`);
|
|
298
|
+
lines.push(` new: ${JSON.stringify(change.newValue)}`);
|
|
299
|
+
} else if (change.type === "added") {
|
|
300
|
+
lines.push(` value: ${JSON.stringify(change.newValue)}`);
|
|
301
|
+
} else if (change.type === "removed") {
|
|
302
|
+
lines.push(` was: ${JSON.stringify(change.oldValue)}`);
|
|
239
303
|
}
|
|
240
304
|
}
|
|
305
|
+
return lines.join(`
|
|
306
|
+
`);
|
|
307
|
+
}
|
|
308
|
+
// src/openapi/exporter/data-views.ts
|
|
309
|
+
function exportDataViews(registry) {
|
|
310
|
+
return registry.list().map((dv) => ({
|
|
311
|
+
name: dv.meta.key,
|
|
312
|
+
version: dv.meta.version,
|
|
313
|
+
description: dv.meta.description,
|
|
314
|
+
stability: dv.meta.stability,
|
|
315
|
+
entity: dv.meta.entity,
|
|
316
|
+
kind: dv.view.kind,
|
|
317
|
+
source: dv.source,
|
|
318
|
+
fields: dv.view.fields
|
|
319
|
+
}));
|
|
320
|
+
}
|
|
321
|
+
function generateDataViewsRegistry(registry) {
|
|
322
|
+
const dataViews = registry.list();
|
|
323
|
+
const imports = new Set;
|
|
324
|
+
const registrations = [];
|
|
325
|
+
for (const dv of dataViews) {
|
|
326
|
+
const dvVarName = dv.meta.key.replace(/\./g, "_") + `_v${dv.meta.version}`;
|
|
327
|
+
imports.add(`import { ${dvVarName} } from './${dv.meta.key.split(".")[0]}';`);
|
|
328
|
+
registrations.push(` .register(${dvVarName})`);
|
|
329
|
+
}
|
|
330
|
+
const code = `/**
|
|
331
|
+
* Auto-generated data views registry.
|
|
332
|
+
* DO NOT EDIT - This file is generated by ContractSpec exporter.
|
|
333
|
+
*/
|
|
334
|
+
import { DataViewRegistry } from '@contractspec/lib.contracts-spec/data-views';
|
|
335
|
+
|
|
336
|
+
${Array.from(imports).join(`
|
|
337
|
+
`)}
|
|
338
|
+
|
|
339
|
+
export const dataViewsRegistry = new DataViewRegistry()
|
|
340
|
+
${registrations.join(`
|
|
341
|
+
`)};
|
|
342
|
+
`;
|
|
343
|
+
return {
|
|
344
|
+
code,
|
|
345
|
+
fileName: "dataviews-registry.ts"
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// src/openapi/exporter/events.ts
|
|
350
|
+
import { z } from "zod";
|
|
351
|
+
function exportEvents(events) {
|
|
352
|
+
return events.map((event) => ({
|
|
353
|
+
name: event.meta.key,
|
|
354
|
+
version: event.meta.version,
|
|
355
|
+
description: event.meta.description,
|
|
356
|
+
payload: event.payload ? z.toJSONSchema(event.payload.getZod()) : null,
|
|
357
|
+
pii: event.pii
|
|
358
|
+
}));
|
|
359
|
+
}
|
|
360
|
+
function generateEventsExports(events) {
|
|
361
|
+
const eventExports = [];
|
|
362
|
+
for (const event of events) {
|
|
363
|
+
const eventVarName = event.meta.key.replace(/\./g, "_") + `_v${event.meta.version}`;
|
|
364
|
+
eventExports.push(`export { ${eventVarName} } from './${event.meta.key.split(".")[0]}';`);
|
|
365
|
+
}
|
|
366
|
+
const code = `/**
|
|
367
|
+
* Auto-generated events exports.
|
|
368
|
+
* DO NOT EDIT - This file is generated by ContractSpec exporter.
|
|
369
|
+
*/
|
|
370
|
+
|
|
371
|
+
${eventExports.join(`
|
|
372
|
+
`)}
|
|
373
|
+
`;
|
|
241
374
|
return {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
info: {
|
|
245
|
-
title: doc.info.title,
|
|
246
|
-
version: doc.info.version,
|
|
247
|
-
description: doc.info.description
|
|
248
|
-
},
|
|
249
|
-
operations,
|
|
250
|
-
schemas,
|
|
251
|
-
servers,
|
|
252
|
-
warnings,
|
|
253
|
-
events
|
|
375
|
+
code,
|
|
376
|
+
fileName: "events-exports.ts"
|
|
254
377
|
};
|
|
255
378
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
format = "yaml";
|
|
278
|
-
} else if (source.endsWith(".json")) {
|
|
279
|
-
format = "json";
|
|
280
|
-
} else {
|
|
281
|
-
format = detectFormat(content);
|
|
282
|
-
}
|
|
283
|
-
} else {
|
|
284
|
-
if (!readFile) {
|
|
285
|
-
throw new Error("readFile adapter required for file paths");
|
|
286
|
-
}
|
|
287
|
-
content = await readFile(source);
|
|
288
|
-
if (source.endsWith(".yaml") || source.endsWith(".yml")) {
|
|
289
|
-
format = "yaml";
|
|
290
|
-
} else if (source.endsWith(".json")) {
|
|
291
|
-
format = "json";
|
|
292
|
-
} else {
|
|
293
|
-
format = detectFormat(content);
|
|
294
|
-
}
|
|
379
|
+
|
|
380
|
+
// src/openapi/exporter/features.ts
|
|
381
|
+
function exportFeatures(registry) {
|
|
382
|
+
return registry.list().map((feature) => ({
|
|
383
|
+
key: feature.meta.key,
|
|
384
|
+
description: feature.meta.description,
|
|
385
|
+
owners: feature.meta.owners,
|
|
386
|
+
stability: feature.meta.stability,
|
|
387
|
+
operations: feature.operations,
|
|
388
|
+
events: feature.events,
|
|
389
|
+
presentations: feature.presentations
|
|
390
|
+
}));
|
|
391
|
+
}
|
|
392
|
+
function generateFeaturesRegistry(registry) {
|
|
393
|
+
const features = registry.list();
|
|
394
|
+
const imports = new Set;
|
|
395
|
+
const registrations = [];
|
|
396
|
+
for (const feature of features) {
|
|
397
|
+
const featureVarName = feature.meta.key.replace(/-/g, "_");
|
|
398
|
+
imports.add(`import { ${featureVarName} } from './${feature.meta.key}';`);
|
|
399
|
+
registrations.push(` .register(${featureVarName})`);
|
|
295
400
|
}
|
|
296
|
-
const
|
|
297
|
-
|
|
401
|
+
const code = `/**
|
|
402
|
+
* Auto-generated features registry.
|
|
403
|
+
* DO NOT EDIT - This file is generated by ContractSpec exporter.
|
|
404
|
+
*/
|
|
405
|
+
import { FeatureRegistry } from '@contractspec/lib.contracts-spec';
|
|
406
|
+
|
|
407
|
+
${Array.from(imports).join(`
|
|
408
|
+
`)}
|
|
409
|
+
|
|
410
|
+
export const featuresRegistry = new FeatureRegistry()
|
|
411
|
+
${registrations.join(`
|
|
412
|
+
`)};
|
|
413
|
+
`;
|
|
414
|
+
return {
|
|
415
|
+
code,
|
|
416
|
+
fileName: "features-registry.ts"
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// src/openapi/exporter/forms.ts
|
|
421
|
+
import { z as z2 } from "zod";
|
|
422
|
+
function exportForms(registry) {
|
|
423
|
+
return registry.list().map((form) => ({
|
|
424
|
+
key: form.meta.key,
|
|
425
|
+
version: form.meta.version,
|
|
426
|
+
description: form.meta.description,
|
|
427
|
+
stability: form.meta.stability,
|
|
428
|
+
owners: form.meta.owners,
|
|
429
|
+
fields: form.fields,
|
|
430
|
+
model: form.model ? z2.toJSONSchema(form.model.getZod()) : null,
|
|
431
|
+
actions: form.actions
|
|
432
|
+
}));
|
|
433
|
+
}
|
|
434
|
+
function generateFormsRegistry(registry) {
|
|
435
|
+
const forms = registry.list();
|
|
436
|
+
const imports = new Set;
|
|
437
|
+
const registrations = [];
|
|
438
|
+
for (const form of forms) {
|
|
439
|
+
const formVarName = form.meta.key.replace(/-/g, "_") + `_v${form.meta.version}`;
|
|
440
|
+
imports.add(`import { ${formVarName} } from './${form.meta.key}';`);
|
|
441
|
+
registrations.push(` .register(${formVarName})`);
|
|
442
|
+
}
|
|
443
|
+
const code = `/**
|
|
444
|
+
* Auto-generated forms registry.
|
|
445
|
+
* DO NOT EDIT - This file is generated by ContractSpec exporter.
|
|
446
|
+
*/
|
|
447
|
+
import { FormRegistry } from '@contractspec/lib.contracts-spec';
|
|
448
|
+
|
|
449
|
+
${Array.from(imports).join(`
|
|
450
|
+
`)}
|
|
451
|
+
|
|
452
|
+
export const formsRegistry = new FormRegistry()
|
|
453
|
+
${registrations.join(`
|
|
454
|
+
`)};
|
|
455
|
+
`;
|
|
456
|
+
return {
|
|
457
|
+
code,
|
|
458
|
+
fileName: "forms-registry.ts"
|
|
459
|
+
};
|
|
298
460
|
}
|
|
461
|
+
|
|
299
462
|
// src/openapi/exporter/operations.ts
|
|
300
|
-
import { z } from "zod";
|
|
301
463
|
import { compareVersions } from "compare-versions";
|
|
464
|
+
import { z as z3 } from "zod";
|
|
302
465
|
function toOperationId(name, version) {
|
|
303
466
|
return `${name.replace(/\./g, "_")}_v${version.replace(/\./g, "_")}`;
|
|
304
467
|
}
|
|
@@ -319,7 +482,7 @@ function toRestPath(spec) {
|
|
|
319
482
|
function schemaModelToJsonSchema(schema) {
|
|
320
483
|
if (!schema)
|
|
321
484
|
return null;
|
|
322
|
-
return
|
|
485
|
+
return z3.toJSONSchema(schema.getZod());
|
|
323
486
|
}
|
|
324
487
|
function jsonSchemaForSpec(spec) {
|
|
325
488
|
return {
|
|
@@ -386,108 +549,37 @@ function exportOperations(registry) {
|
|
|
386
549
|
};
|
|
387
550
|
} else {
|
|
388
551
|
responses["200"] = { description: "OK" };
|
|
389
|
-
}
|
|
390
|
-
op["responses"] = responses;
|
|
391
|
-
pathItem[method] = op;
|
|
392
|
-
}
|
|
393
|
-
return { paths, schemas };
|
|
394
|
-
}
|
|
395
|
-
function generateOperationsRegistry(registry) {
|
|
396
|
-
const specs = Array.from(registry.list().values());
|
|
397
|
-
const imports = new Set;
|
|
398
|
-
const registrations = [];
|
|
399
|
-
for (const spec of specs) {
|
|
400
|
-
const specVarName = spec.meta.key.replace(/\./g, "_") + `_v${spec.meta.version.replace(/\./g, "_")}`;
|
|
401
|
-
imports.add(`import { ${specVarName} } from './${spec.meta.key.split(".")[0]}';`);
|
|
402
|
-
registrations.push(` .register(${specVarName})`);
|
|
403
|
-
}
|
|
404
|
-
const code = `/**
|
|
405
|
-
* Auto-generated operations registry.
|
|
406
|
-
* DO NOT EDIT - This file is generated by ContractSpec exporter.
|
|
407
|
-
*/
|
|
408
|
-
import { OperationSpecRegistry } from '@contractspec/lib.contracts-spec';
|
|
409
|
-
|
|
410
|
-
${Array.from(imports).join(`
|
|
411
|
-
`)}
|
|
412
|
-
|
|
413
|
-
export const operationsRegistry = new OperationSpecRegistry()
|
|
414
|
-
${registrations.join(`
|
|
415
|
-
`)};
|
|
416
|
-
`;
|
|
417
|
-
return {
|
|
418
|
-
code,
|
|
419
|
-
fileName: "operations-registry.ts"
|
|
420
|
-
};
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// src/openapi/exporter/events.ts
|
|
424
|
-
import { z as z2 } from "zod";
|
|
425
|
-
function exportEvents(events) {
|
|
426
|
-
return events.map((event) => ({
|
|
427
|
-
name: event.meta.key,
|
|
428
|
-
version: event.meta.version,
|
|
429
|
-
description: event.meta.description,
|
|
430
|
-
payload: event.payload ? z2.toJSONSchema(event.payload.getZod()) : null,
|
|
431
|
-
pii: event.pii
|
|
432
|
-
}));
|
|
433
|
-
}
|
|
434
|
-
function generateEventsExports(events) {
|
|
435
|
-
const eventExports = [];
|
|
436
|
-
for (const event of events) {
|
|
437
|
-
const eventVarName = event.meta.key.replace(/\./g, "_") + `_v${event.meta.version}`;
|
|
438
|
-
eventExports.push(`export { ${eventVarName} } from './${event.meta.key.split(".")[0]}';`);
|
|
552
|
+
}
|
|
553
|
+
op["responses"] = responses;
|
|
554
|
+
pathItem[method] = op;
|
|
439
555
|
}
|
|
440
|
-
|
|
441
|
-
* Auto-generated events exports.
|
|
442
|
-
* DO NOT EDIT - This file is generated by ContractSpec exporter.
|
|
443
|
-
*/
|
|
444
|
-
|
|
445
|
-
${eventExports.join(`
|
|
446
|
-
`)}
|
|
447
|
-
`;
|
|
448
|
-
return {
|
|
449
|
-
code,
|
|
450
|
-
fileName: "events-exports.ts"
|
|
451
|
-
};
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
// src/openapi/exporter/features.ts
|
|
455
|
-
function exportFeatures(registry) {
|
|
456
|
-
return registry.list().map((feature) => ({
|
|
457
|
-
key: feature.meta.key,
|
|
458
|
-
description: feature.meta.description,
|
|
459
|
-
owners: feature.meta.owners,
|
|
460
|
-
stability: feature.meta.stability,
|
|
461
|
-
operations: feature.operations,
|
|
462
|
-
events: feature.events,
|
|
463
|
-
presentations: feature.presentations
|
|
464
|
-
}));
|
|
556
|
+
return { paths, schemas };
|
|
465
557
|
}
|
|
466
|
-
function
|
|
467
|
-
const
|
|
558
|
+
function generateOperationsRegistry(registry) {
|
|
559
|
+
const specs = Array.from(registry.list().values());
|
|
468
560
|
const imports = new Set;
|
|
469
561
|
const registrations = [];
|
|
470
|
-
for (const
|
|
471
|
-
const
|
|
472
|
-
imports.add(`import { ${
|
|
473
|
-
registrations.push(` .register(${
|
|
562
|
+
for (const spec of specs) {
|
|
563
|
+
const specVarName = spec.meta.key.replace(/\./g, "_") + `_v${spec.meta.version.replace(/\./g, "_")}`;
|
|
564
|
+
imports.add(`import { ${specVarName} } from './${spec.meta.key.split(".")[0]}';`);
|
|
565
|
+
registrations.push(` .register(${specVarName})`);
|
|
474
566
|
}
|
|
475
567
|
const code = `/**
|
|
476
|
-
* Auto-generated
|
|
568
|
+
* Auto-generated operations registry.
|
|
477
569
|
* DO NOT EDIT - This file is generated by ContractSpec exporter.
|
|
478
570
|
*/
|
|
479
|
-
import {
|
|
571
|
+
import { OperationSpecRegistry } from '@contractspec/lib.contracts-spec';
|
|
480
572
|
|
|
481
573
|
${Array.from(imports).join(`
|
|
482
574
|
`)}
|
|
483
575
|
|
|
484
|
-
export const
|
|
576
|
+
export const operationsRegistry = new OperationSpecRegistry()
|
|
485
577
|
${registrations.join(`
|
|
486
578
|
`)};
|
|
487
579
|
`;
|
|
488
580
|
return {
|
|
489
581
|
code,
|
|
490
|
-
fileName: "
|
|
582
|
+
fileName: "operations-registry.ts"
|
|
491
583
|
};
|
|
492
584
|
}
|
|
493
585
|
|
|
@@ -542,86 +634,50 @@ ${registrations.join(`
|
|
|
542
634
|
};
|
|
543
635
|
}
|
|
544
636
|
|
|
545
|
-
// src/openapi/exporter/
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
function generateFormsRegistry(registry) {
|
|
560
|
-
const forms = registry.list();
|
|
561
|
-
const imports = new Set;
|
|
562
|
-
const registrations = [];
|
|
563
|
-
for (const form of forms) {
|
|
564
|
-
const formVarName = form.meta.key.replace(/-/g, "_") + `_v${form.meta.version}`;
|
|
565
|
-
imports.add(`import { ${formVarName} } from './${form.meta.key}';`);
|
|
566
|
-
registrations.push(` .register(${formVarName})`);
|
|
637
|
+
// src/openapi/exporter/registries.ts
|
|
638
|
+
function generateRegistryIndex(options = {}) {
|
|
639
|
+
const {
|
|
640
|
+
operations = true,
|
|
641
|
+
events = true,
|
|
642
|
+
features = true,
|
|
643
|
+
presentations = true,
|
|
644
|
+
forms = true,
|
|
645
|
+
dataViews = true,
|
|
646
|
+
workflows = true
|
|
647
|
+
} = options;
|
|
648
|
+
const exports = [];
|
|
649
|
+
if (operations) {
|
|
650
|
+
exports.push("export * from './operations-registry';");
|
|
567
651
|
}
|
|
568
|
-
|
|
569
|
-
*
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
// src/openapi/exporter/data-views.ts
|
|
588
|
-
function exportDataViews(registry) {
|
|
589
|
-
return registry.list().map((dv) => ({
|
|
590
|
-
name: dv.meta.key,
|
|
591
|
-
version: dv.meta.version,
|
|
592
|
-
description: dv.meta.description,
|
|
593
|
-
stability: dv.meta.stability,
|
|
594
|
-
entity: dv.meta.entity,
|
|
595
|
-
kind: dv.view.kind,
|
|
596
|
-
source: dv.source,
|
|
597
|
-
fields: dv.view.fields
|
|
598
|
-
}));
|
|
599
|
-
}
|
|
600
|
-
function generateDataViewsRegistry(registry) {
|
|
601
|
-
const dataViews = registry.list();
|
|
602
|
-
const imports = new Set;
|
|
603
|
-
const registrations = [];
|
|
604
|
-
for (const dv of dataViews) {
|
|
605
|
-
const dvVarName = dv.meta.key.replace(/\./g, "_") + `_v${dv.meta.version}`;
|
|
606
|
-
imports.add(`import { ${dvVarName} } from './${dv.meta.key.split(".")[0]}';`);
|
|
607
|
-
registrations.push(` .register(${dvVarName})`);
|
|
652
|
+
if (events) {
|
|
653
|
+
exports.push("export * from './events-exports';");
|
|
654
|
+
}
|
|
655
|
+
if (features) {
|
|
656
|
+
exports.push("export * from './features-registry';");
|
|
657
|
+
}
|
|
658
|
+
if (presentations) {
|
|
659
|
+
exports.push("export * from './presentations-registry';");
|
|
660
|
+
}
|
|
661
|
+
if (forms) {
|
|
662
|
+
exports.push("export * from './forms-registry';");
|
|
663
|
+
}
|
|
664
|
+
if (dataViews) {
|
|
665
|
+
exports.push("export * from './dataviews-registry';");
|
|
666
|
+
}
|
|
667
|
+
if (workflows) {
|
|
668
|
+
exports.push("export * from './workflows-registry';");
|
|
608
669
|
}
|
|
609
670
|
const code = `/**
|
|
610
|
-
* Auto-generated
|
|
671
|
+
* Auto-generated registry index.
|
|
611
672
|
* DO NOT EDIT - This file is generated by ContractSpec exporter.
|
|
612
673
|
*/
|
|
613
|
-
import { DataViewRegistry } from '@contractspec/lib.contracts-spec/data-views';
|
|
614
674
|
|
|
615
|
-
${
|
|
675
|
+
${exports.join(`
|
|
616
676
|
`)}
|
|
617
|
-
|
|
618
|
-
export const dataViewsRegistry = new DataViewRegistry()
|
|
619
|
-
${registrations.join(`
|
|
620
|
-
`)};
|
|
621
677
|
`;
|
|
622
678
|
return {
|
|
623
679
|
code,
|
|
624
|
-
fileName: "
|
|
680
|
+
fileName: "index.ts"
|
|
625
681
|
};
|
|
626
682
|
}
|
|
627
683
|
|
|
@@ -673,53 +729,6 @@ ${registrations.join(`
|
|
|
673
729
|
};
|
|
674
730
|
}
|
|
675
731
|
|
|
676
|
-
// src/openapi/exporter/registries.ts
|
|
677
|
-
function generateRegistryIndex(options = {}) {
|
|
678
|
-
const {
|
|
679
|
-
operations = true,
|
|
680
|
-
events = true,
|
|
681
|
-
features = true,
|
|
682
|
-
presentations = true,
|
|
683
|
-
forms = true,
|
|
684
|
-
dataViews = true,
|
|
685
|
-
workflows = true
|
|
686
|
-
} = options;
|
|
687
|
-
const exports = [];
|
|
688
|
-
if (operations) {
|
|
689
|
-
exports.push("export * from './operations-registry';");
|
|
690
|
-
}
|
|
691
|
-
if (events) {
|
|
692
|
-
exports.push("export * from './events-exports';");
|
|
693
|
-
}
|
|
694
|
-
if (features) {
|
|
695
|
-
exports.push("export * from './features-registry';");
|
|
696
|
-
}
|
|
697
|
-
if (presentations) {
|
|
698
|
-
exports.push("export * from './presentations-registry';");
|
|
699
|
-
}
|
|
700
|
-
if (forms) {
|
|
701
|
-
exports.push("export * from './forms-registry';");
|
|
702
|
-
}
|
|
703
|
-
if (dataViews) {
|
|
704
|
-
exports.push("export * from './dataviews-registry';");
|
|
705
|
-
}
|
|
706
|
-
if (workflows) {
|
|
707
|
-
exports.push("export * from './workflows-registry';");
|
|
708
|
-
}
|
|
709
|
-
const code = `/**
|
|
710
|
-
* Auto-generated registry index.
|
|
711
|
-
* DO NOT EDIT - This file is generated by ContractSpec exporter.
|
|
712
|
-
*/
|
|
713
|
-
|
|
714
|
-
${exports.join(`
|
|
715
|
-
`)}
|
|
716
|
-
`;
|
|
717
|
-
return {
|
|
718
|
-
code,
|
|
719
|
-
fileName: "index.ts"
|
|
720
|
-
};
|
|
721
|
-
}
|
|
722
|
-
|
|
723
732
|
// src/openapi/exporter.ts
|
|
724
733
|
function openApiForRegistry(registry, options = {}) {
|
|
725
734
|
const { paths, schemas } = exportOperations(registry);
|
|
@@ -855,90 +864,16 @@ ${jsonToYaml(item, indent + 1)}`;
|
|
|
855
864
|
yaml += `${spaces}${key}:
|
|
856
865
|
${jsonToYaml(value, indent + 1)}`;
|
|
857
866
|
} else if (typeof value === "object" && value !== null) {
|
|
858
|
-
yaml += `${spaces}${key}:
|
|
859
|
-
${jsonToYaml(value, indent + 1)}`;
|
|
860
|
-
} else {
|
|
861
|
-
yaml += `${spaces}${key}: ${JSON.stringify(value)}
|
|
862
|
-
`;
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
return yaml;
|
|
867
|
-
}
|
|
868
|
-
// src/common/utils.ts
|
|
869
|
-
function toPascalCase(str) {
|
|
870
|
-
return str.replace(/[-_./\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^./, (c) => c.toUpperCase());
|
|
871
|
-
}
|
|
872
|
-
function toCamelCase(str) {
|
|
873
|
-
const pascal = toPascalCase(str);
|
|
874
|
-
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
875
|
-
}
|
|
876
|
-
function toKebabCase(str) {
|
|
877
|
-
return str.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[\s_./]+/g, "-").toLowerCase();
|
|
878
|
-
}
|
|
879
|
-
function toSnakeCase(str) {
|
|
880
|
-
return str.replace(/([a-z])([A-Z])/g, "$1_$2").replace(/[\s\-./]+/g, "_").toLowerCase();
|
|
881
|
-
}
|
|
882
|
-
function toValidIdentifier(str) {
|
|
883
|
-
let result = str.replace(/[^a-zA-Z0-9_$]/g, "_");
|
|
884
|
-
if (/^[0-9]/.test(result)) {
|
|
885
|
-
result = "_" + result;
|
|
886
|
-
}
|
|
887
|
-
return result;
|
|
888
|
-
}
|
|
889
|
-
function toSpecKey(operationId, prefix) {
|
|
890
|
-
const key = toCamelCase(operationId);
|
|
891
|
-
return prefix ? `${prefix}.${key}` : key;
|
|
892
|
-
}
|
|
893
|
-
function toFileName(specName) {
|
|
894
|
-
return toKebabCase(specName.replace(/\./g, "-")) + ".ts";
|
|
895
|
-
}
|
|
896
|
-
function deepEqual(a, b) {
|
|
897
|
-
if (a === b)
|
|
898
|
-
return true;
|
|
899
|
-
if (a === null || b === null)
|
|
900
|
-
return false;
|
|
901
|
-
if (typeof a !== typeof b)
|
|
902
|
-
return false;
|
|
903
|
-
if (typeof a === "object") {
|
|
904
|
-
const aObj = a;
|
|
905
|
-
const bObj = b;
|
|
906
|
-
const aKeys = Object.keys(aObj);
|
|
907
|
-
const bKeys = Object.keys(bObj);
|
|
908
|
-
if (aKeys.length !== bKeys.length)
|
|
909
|
-
return false;
|
|
910
|
-
for (const key of aKeys) {
|
|
911
|
-
if (!bKeys.includes(key))
|
|
912
|
-
return false;
|
|
913
|
-
if (!deepEqual(aObj[key], bObj[key]))
|
|
914
|
-
return false;
|
|
867
|
+
yaml += `${spaces}${key}:
|
|
868
|
+
${jsonToYaml(value, indent + 1)}`;
|
|
869
|
+
} else {
|
|
870
|
+
yaml += `${spaces}${key}: ${JSON.stringify(value)}
|
|
871
|
+
`;
|
|
872
|
+
}
|
|
915
873
|
}
|
|
916
|
-
return true;
|
|
917
|
-
}
|
|
918
|
-
return false;
|
|
919
|
-
}
|
|
920
|
-
function getByPath(obj, path) {
|
|
921
|
-
const parts = path.split(".").filter(Boolean);
|
|
922
|
-
let current = obj;
|
|
923
|
-
for (const part of parts) {
|
|
924
|
-
if (current === null || current === undefined)
|
|
925
|
-
return;
|
|
926
|
-
if (typeof current !== "object")
|
|
927
|
-
return;
|
|
928
|
-
current = current[part];
|
|
929
874
|
}
|
|
930
|
-
return
|
|
931
|
-
}
|
|
932
|
-
function extractPathParams(path) {
|
|
933
|
-
const matches = path.match(/\{([^}]+)\}/g) || [];
|
|
934
|
-
return matches.map((m) => m.slice(1, -1));
|
|
935
|
-
}
|
|
936
|
-
function normalizePath(path) {
|
|
937
|
-
let normalized = path.replace(/^\/+|\/+$/g, "");
|
|
938
|
-
normalized = normalized.replace(/\/+/g, "/");
|
|
939
|
-
return "/" + normalized;
|
|
875
|
+
return yaml;
|
|
940
876
|
}
|
|
941
|
-
|
|
942
877
|
// src/openapi/schema-generators/index.ts
|
|
943
878
|
var JSON_SCHEMA_TO_SCALAR = {
|
|
944
879
|
string: "ScalarTypeEnum.String_unsecure",
|
|
@@ -951,7 +886,7 @@ var JSON_SCHEMA_TO_SCALAR = {
|
|
|
951
886
|
"string:uri": "ScalarTypeEnum.URL",
|
|
952
887
|
"string:uuid": "ScalarTypeEnum.ID"
|
|
953
888
|
};
|
|
954
|
-
function
|
|
889
|
+
function isReference(schema) {
|
|
955
890
|
return typeof schema === "object" && schema !== null && "$ref" in schema;
|
|
956
891
|
}
|
|
957
892
|
function typeNameFromRef(ref) {
|
|
@@ -1006,7 +941,7 @@ class ContractSpecSchemaGenerator {
|
|
|
1006
941
|
generateContractSpecSchema(schema, modelName, indent = 0) {
|
|
1007
942
|
const spaces = " ".repeat(indent);
|
|
1008
943
|
const fields = [];
|
|
1009
|
-
if (
|
|
944
|
+
if (isReference(schema)) {
|
|
1010
945
|
return {
|
|
1011
946
|
name: toPascalCase(typeNameFromRef(schema.$ref)),
|
|
1012
947
|
fields: [],
|
|
@@ -1142,7 +1077,7 @@ class ContractSpecSchemaGenerator {
|
|
|
1142
1077
|
const scalarType = getScalarType(schema);
|
|
1143
1078
|
let enumValues;
|
|
1144
1079
|
let nestedModel;
|
|
1145
|
-
if (!
|
|
1080
|
+
if (!isReference(schema)) {
|
|
1146
1081
|
const schemaObj = schema;
|
|
1147
1082
|
const enumArr = schemaObj["enum"];
|
|
1148
1083
|
if (enumArr) {
|
|
@@ -1160,7 +1095,7 @@ class ContractSpecSchemaGenerator {
|
|
|
1160
1095
|
type: {
|
|
1161
1096
|
...type,
|
|
1162
1097
|
optional: !required || type.optional,
|
|
1163
|
-
description: !
|
|
1098
|
+
description: !isReference(schema) ? schema["description"] : undefined
|
|
1164
1099
|
},
|
|
1165
1100
|
scalarType,
|
|
1166
1101
|
enumValues,
|
|
@@ -1503,7 +1438,7 @@ var JSON_SCHEMA_TO_SCALAR2 = {
|
|
|
1503
1438
|
"string:uri": "ScalarTypeEnum.URL",
|
|
1504
1439
|
"string:uuid": "ScalarTypeEnum.ID"
|
|
1505
1440
|
};
|
|
1506
|
-
function
|
|
1441
|
+
function isReference2(schema) {
|
|
1507
1442
|
return typeof schema === "object" && schema !== null && "$ref" in schema;
|
|
1508
1443
|
}
|
|
1509
1444
|
function typeNameFromRef2(ref) {
|
|
@@ -1511,7 +1446,7 @@ function typeNameFromRef2(ref) {
|
|
|
1511
1446
|
return parts[parts.length - 1] ?? "Unknown";
|
|
1512
1447
|
}
|
|
1513
1448
|
function jsonSchemaToType(schema, name) {
|
|
1514
|
-
if (
|
|
1449
|
+
if (isReference2(schema)) {
|
|
1515
1450
|
return {
|
|
1516
1451
|
type: toPascalCase(typeNameFromRef2(schema.$ref)),
|
|
1517
1452
|
optional: false,
|
|
@@ -1604,7 +1539,7 @@ function jsonSchemaToType(schema, name) {
|
|
|
1604
1539
|
};
|
|
1605
1540
|
}
|
|
1606
1541
|
function getScalarType(schema) {
|
|
1607
|
-
if (
|
|
1542
|
+
if (isReference2(schema)) {
|
|
1608
1543
|
return;
|
|
1609
1544
|
}
|
|
1610
1545
|
const schemaObj = schema;
|
|
@@ -1650,65 +1585,42 @@ function generateImports(fields, options, sameDirectory = true) {
|
|
|
1650
1585
|
`);
|
|
1651
1586
|
}
|
|
1652
1587
|
|
|
1653
|
-
// src/openapi/importer/
|
|
1654
|
-
function
|
|
1655
|
-
const
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
result.query = {
|
|
1668
|
-
type: "object",
|
|
1669
|
-
properties: operation2.queryParams.reduce((acc, p) => {
|
|
1670
|
-
acc[p.name] = p.schema;
|
|
1671
|
-
return acc;
|
|
1672
|
-
}, {}),
|
|
1673
|
-
required: operation2.queryParams.filter((p) => p.required).map((p) => p.name)
|
|
1674
|
-
};
|
|
1675
|
-
}
|
|
1676
|
-
const excludedHeaders = [
|
|
1677
|
-
"authorization",
|
|
1678
|
-
"content-type",
|
|
1679
|
-
"accept",
|
|
1680
|
-
"user-agent"
|
|
1681
|
-
];
|
|
1682
|
-
const actualHeaders = operation2.headerParams.filter((p) => !excludedHeaders.includes(p.name.toLowerCase()));
|
|
1683
|
-
if (actualHeaders.length > 0) {
|
|
1684
|
-
result.headers = {
|
|
1685
|
-
type: "object",
|
|
1686
|
-
properties: actualHeaders.reduce((acc, p) => {
|
|
1687
|
-
acc[p.name] = p.schema;
|
|
1688
|
-
return acc;
|
|
1689
|
-
}, {}),
|
|
1690
|
-
required: actualHeaders.filter((p) => p.required).map((p) => p.name)
|
|
1691
|
-
};
|
|
1692
|
-
}
|
|
1693
|
-
if (operation2.requestBody?.schema) {
|
|
1694
|
-
result.body = operation2.requestBody.schema;
|
|
1695
|
-
}
|
|
1696
|
-
return result;
|
|
1697
|
-
}
|
|
1698
|
-
function getOutputSchema(operation2) {
|
|
1699
|
-
const successCodes = ["200", "201", "202", "204"];
|
|
1700
|
-
for (const code of successCodes) {
|
|
1701
|
-
const response = operation2.responses[code];
|
|
1702
|
-
if (response?.schema) {
|
|
1703
|
-
return response.schema;
|
|
1704
|
-
}
|
|
1588
|
+
// src/openapi/importer/events.ts
|
|
1589
|
+
function generateEventCode(event, options) {
|
|
1590
|
+
const eventName = toValidIdentifier(event.name);
|
|
1591
|
+
const modelName = toPascalCase(eventName) + "Payload";
|
|
1592
|
+
const schemaFormat = options.schemaFormat || "contractspec";
|
|
1593
|
+
const payloadModel = generateSchemaModelCode(event.payload, modelName, schemaFormat, options);
|
|
1594
|
+
const imports = new Set;
|
|
1595
|
+
imports.add("import { defineEvent, type EventSpec } from '@contractspec/lib.contracts-spec';");
|
|
1596
|
+
if (payloadModel.imports && payloadModel.imports.length > 0) {
|
|
1597
|
+
payloadModel.imports.forEach((i) => imports.add(i));
|
|
1598
|
+
} else if (payloadModel.fields && payloadModel.fields.length > 0) {
|
|
1599
|
+
const modelImports = generateImports(payloadModel.fields, options);
|
|
1600
|
+
modelImports.split(`
|
|
1601
|
+
`).filter(Boolean).forEach((i) => imports.add(i));
|
|
1705
1602
|
}
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
}
|
|
1603
|
+
if (payloadModel.name !== modelName) {
|
|
1604
|
+
const modelsDir = `../${options.conventions.models}`;
|
|
1605
|
+
const kebabName = payloadModel.name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
1606
|
+
imports.add(`import { ${payloadModel.name} } from '${modelsDir}/${kebabName}';`);
|
|
1710
1607
|
}
|
|
1711
|
-
|
|
1608
|
+
const allImports = Array.from(imports).join(`
|
|
1609
|
+
`);
|
|
1610
|
+
return `
|
|
1611
|
+
${allImports}
|
|
1612
|
+
|
|
1613
|
+
${payloadModel.code}
|
|
1614
|
+
|
|
1615
|
+
export const ${eventName} = defineEvent({
|
|
1616
|
+
meta: {
|
|
1617
|
+
key: '${event.name}',
|
|
1618
|
+
version: '1.0.0',
|
|
1619
|
+
description: ${JSON.stringify(event.description ?? "")},
|
|
1620
|
+
},
|
|
1621
|
+
payload: ${payloadModel.name},
|
|
1622
|
+
});
|
|
1623
|
+
`.trim();
|
|
1712
1624
|
}
|
|
1713
1625
|
|
|
1714
1626
|
// src/openapi/importer/analyzer.ts
|
|
@@ -1716,11 +1628,11 @@ var COMMAND_METHODS = ["post", "put", "delete", "patch"];
|
|
|
1716
1628
|
function inferOpKind(method) {
|
|
1717
1629
|
return COMMAND_METHODS.includes(method.toLowerCase()) ? "command" : "query";
|
|
1718
1630
|
}
|
|
1719
|
-
function inferAuthLevel(
|
|
1720
|
-
if (!
|
|
1631
|
+
function inferAuthLevel(operation, defaultAuth) {
|
|
1632
|
+
if (!operation.security || operation.security.length === 0) {
|
|
1721
1633
|
return defaultAuth;
|
|
1722
1634
|
}
|
|
1723
|
-
for (const sec of
|
|
1635
|
+
for (const sec of operation.security) {
|
|
1724
1636
|
if (Object.keys(sec).length === 0) {
|
|
1725
1637
|
return "anonymous";
|
|
1726
1638
|
}
|
|
@@ -1729,10 +1641,10 @@ function inferAuthLevel(operation2, defaultAuth) {
|
|
|
1729
1641
|
}
|
|
1730
1642
|
|
|
1731
1643
|
// src/openapi/importer/generator.ts
|
|
1732
|
-
function generateSpecCode(
|
|
1733
|
-
const specKey = toSpecKey(
|
|
1734
|
-
const kind = inferOpKind(
|
|
1735
|
-
const auth = inferAuthLevel(
|
|
1644
|
+
function generateSpecCode(operation, contractspecConfig, options = {}, inputModel, outputModel, queryModel = null, paramsModel = null, headersModel = null) {
|
|
1645
|
+
const specKey = toSpecKey(operation.operationId, options.prefix);
|
|
1646
|
+
const kind = inferOpKind(operation.method);
|
|
1647
|
+
const auth = inferAuthLevel(operation, options.defaultAuth ?? "user");
|
|
1736
1648
|
const lines = [];
|
|
1737
1649
|
lines.push("import { defineCommand, defineQuery } from '@contractspec/lib.contracts-spec';");
|
|
1738
1650
|
if (inputModel || outputModel || queryModel || paramsModel || headersModel) {
|
|
@@ -1777,15 +1689,15 @@ function generateSpecCode(operation2, contractspecConfig, options = {}, inputMod
|
|
|
1777
1689
|
}
|
|
1778
1690
|
}
|
|
1779
1691
|
const defineFunc = kind === "command" ? "defineCommand" : "defineQuery";
|
|
1780
|
-
const safeName = toValidIdentifier(toPascalCase(
|
|
1692
|
+
const safeName = toValidIdentifier(toPascalCase(operation.operationId));
|
|
1781
1693
|
lines.push(`/**`);
|
|
1782
|
-
lines.push(` * ${
|
|
1783
|
-
if (
|
|
1694
|
+
lines.push(` * ${operation.summary ?? operation.operationId}`);
|
|
1695
|
+
if (operation.description) {
|
|
1784
1696
|
lines.push(` *`);
|
|
1785
|
-
lines.push(` * ${
|
|
1697
|
+
lines.push(` * ${operation.description}`);
|
|
1786
1698
|
}
|
|
1787
1699
|
lines.push(` *`);
|
|
1788
|
-
lines.push(` * @source OpenAPI: ${
|
|
1700
|
+
lines.push(` * @source OpenAPI: ${operation.method.toUpperCase()} ${operation.path}`);
|
|
1789
1701
|
lines.push(` */`);
|
|
1790
1702
|
lines.push(`export const ${safeName}Spec = ${defineFunc}({`);
|
|
1791
1703
|
lines.push(" meta: {");
|
|
@@ -1793,10 +1705,10 @@ function generateSpecCode(operation2, contractspecConfig, options = {}, inputMod
|
|
|
1793
1705
|
lines.push(" version: '1.0.0',");
|
|
1794
1706
|
lines.push(` stability: '${options.defaultStability ?? "stable"}',`);
|
|
1795
1707
|
lines.push(` owners: [${(options.defaultOwners ?? []).map((o) => `'${o}'`).join(", ")}],`);
|
|
1796
|
-
lines.push(` tags: [${
|
|
1797
|
-
lines.push(` description: ${JSON.stringify(
|
|
1798
|
-
lines.push(` goal: ${JSON.stringify(
|
|
1799
|
-
lines.push(` context: 'Imported from OpenAPI: ${
|
|
1708
|
+
lines.push(` tags: [${operation.tags.map((t) => `'${t}'`).join(", ")}],`);
|
|
1709
|
+
lines.push(` description: ${JSON.stringify(operation.summary ?? operation.operationId)},`);
|
|
1710
|
+
lines.push(` goal: ${JSON.stringify(operation.description ?? "Imported from OpenAPI")},`);
|
|
1711
|
+
lines.push(` context: 'Imported from OpenAPI: ${operation.method.toUpperCase()} ${operation.path}',`);
|
|
1800
1712
|
lines.push(" },");
|
|
1801
1713
|
lines.push(" io: {");
|
|
1802
1714
|
lines.push(` input: ${inputModel?.name ?? "null"},`);
|
|
@@ -1815,12 +1727,12 @@ function generateSpecCode(operation2, contractspecConfig, options = {}, inputMod
|
|
|
1815
1727
|
lines.push(" policy: {");
|
|
1816
1728
|
lines.push(` auth: '${auth}',`);
|
|
1817
1729
|
lines.push(" },");
|
|
1818
|
-
const httpMethod =
|
|
1730
|
+
const httpMethod = operation.method.toUpperCase();
|
|
1819
1731
|
const restMethod = httpMethod === "GET" ? "GET" : "POST";
|
|
1820
1732
|
lines.push(" transport: {");
|
|
1821
1733
|
lines.push(" rest: {");
|
|
1822
1734
|
lines.push(` method: '${restMethod}',`);
|
|
1823
|
-
lines.push(` path: '${
|
|
1735
|
+
lines.push(` path: '${operation.path}',`);
|
|
1824
1736
|
lines.push(" },");
|
|
1825
1737
|
lines.push(" },");
|
|
1826
1738
|
lines.push("});");
|
|
@@ -1828,73 +1740,16 @@ function generateSpecCode(operation2, contractspecConfig, options = {}, inputMod
|
|
|
1828
1740
|
`);
|
|
1829
1741
|
}
|
|
1830
1742
|
|
|
1831
|
-
// src/openapi/importer/models.ts
|
|
1832
|
-
function generateModelCode(name, schema, options) {
|
|
1833
|
-
const modelName = toPascalCase(toValidIdentifier(name));
|
|
1834
|
-
const schemaFormat = options.schemaFormat || "contractspec";
|
|
1835
|
-
const model = generateSchemaModelCode(schema, modelName, schemaFormat, options);
|
|
1836
|
-
let imports = "";
|
|
1837
|
-
if (model.imports && model.imports.length > 0) {
|
|
1838
|
-
imports = model.imports.join(`
|
|
1839
|
-
`);
|
|
1840
|
-
} else if (model.fields.length > 0) {
|
|
1841
|
-
imports = generateImports(model.fields, options);
|
|
1842
|
-
}
|
|
1843
|
-
return `
|
|
1844
|
-
${imports}
|
|
1845
|
-
|
|
1846
|
-
${model.code}
|
|
1847
|
-
`.trim();
|
|
1848
|
-
}
|
|
1849
|
-
|
|
1850
|
-
// src/openapi/importer/events.ts
|
|
1851
|
-
function generateEventCode(event, options) {
|
|
1852
|
-
const eventName = toValidIdentifier(event.name);
|
|
1853
|
-
const modelName = toPascalCase(eventName) + "Payload";
|
|
1854
|
-
const schemaFormat = options.schemaFormat || "contractspec";
|
|
1855
|
-
const payloadModel = generateSchemaModelCode(event.payload, modelName, schemaFormat, options);
|
|
1856
|
-
const imports = new Set;
|
|
1857
|
-
imports.add("import { defineEvent, type EventSpec } from '@contractspec/lib.contracts-spec';");
|
|
1858
|
-
if (payloadModel.imports && payloadModel.imports.length > 0) {
|
|
1859
|
-
payloadModel.imports.forEach((i) => imports.add(i));
|
|
1860
|
-
} else if (payloadModel.fields && payloadModel.fields.length > 0) {
|
|
1861
|
-
const modelImports = generateImports(payloadModel.fields, options);
|
|
1862
|
-
modelImports.split(`
|
|
1863
|
-
`).filter(Boolean).forEach((i) => imports.add(i));
|
|
1864
|
-
}
|
|
1865
|
-
if (payloadModel.name !== modelName) {
|
|
1866
|
-
const modelsDir = `../${options.conventions.models}`;
|
|
1867
|
-
const kebabName = payloadModel.name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
1868
|
-
imports.add(`import { ${payloadModel.name} } from '${modelsDir}/${kebabName}';`);
|
|
1869
|
-
}
|
|
1870
|
-
const allImports = Array.from(imports).join(`
|
|
1871
|
-
`);
|
|
1872
|
-
return `
|
|
1873
|
-
${allImports}
|
|
1874
|
-
|
|
1875
|
-
${payloadModel.code}
|
|
1876
|
-
|
|
1877
|
-
export const ${eventName} = defineEvent({
|
|
1878
|
-
meta: {
|
|
1879
|
-
key: '${event.name}',
|
|
1880
|
-
version: '1.0.0',
|
|
1881
|
-
description: ${JSON.stringify(event.description ?? "")},
|
|
1882
|
-
},
|
|
1883
|
-
payload: ${payloadModel.name},
|
|
1884
|
-
});
|
|
1885
|
-
`.trim();
|
|
1886
|
-
}
|
|
1887
|
-
|
|
1888
1743
|
// src/openapi/importer/grouping.ts
|
|
1889
|
-
function resolveOperationGroupFolder(
|
|
1744
|
+
function resolveOperationGroupFolder(operation, conventions) {
|
|
1890
1745
|
const groupingRule = conventions.operationsGrouping;
|
|
1891
1746
|
if (!groupingRule || groupingRule.strategy === "none") {
|
|
1892
1747
|
return "";
|
|
1893
1748
|
}
|
|
1894
1749
|
return applyGroupingStrategy(groupingRule, {
|
|
1895
|
-
name:
|
|
1896
|
-
tags:
|
|
1897
|
-
path:
|
|
1750
|
+
name: operation.operationId,
|
|
1751
|
+
tags: operation.tags,
|
|
1752
|
+
path: operation.path
|
|
1898
1753
|
});
|
|
1899
1754
|
}
|
|
1900
1755
|
function resolveModelGroupFolder(modelName, conventions, relatedPath, relatedTags) {
|
|
@@ -1913,48 +1768,128 @@ function resolveEventGroupFolder(eventName, conventions, relatedTags) {
|
|
|
1913
1768
|
if (!groupingRule || groupingRule.strategy === "none") {
|
|
1914
1769
|
return "";
|
|
1915
1770
|
}
|
|
1916
|
-
return applyGroupingStrategy(groupingRule, {
|
|
1917
|
-
name: eventName,
|
|
1918
|
-
tags: relatedTags ?? []
|
|
1919
|
-
});
|
|
1920
|
-
}
|
|
1921
|
-
function applyGroupingStrategy(rule, context) {
|
|
1922
|
-
switch (rule.strategy) {
|
|
1923
|
-
case "by-tag":
|
|
1924
|
-
return context.tags?.[0] ?? "untagged";
|
|
1925
|
-
case "by-owner":
|
|
1926
|
-
return context.owners?.[0] ?? "unowned";
|
|
1927
|
-
case "by-domain":
|
|
1928
|
-
return extractDomain(context.name);
|
|
1929
|
-
case "by-url-path-single":
|
|
1930
|
-
return extractUrlPathLevel(context.path, 1);
|
|
1931
|
-
case "by-url-path-multi":
|
|
1932
|
-
return extractUrlPathLevel(context.path, rule.urlPathLevel ?? 2);
|
|
1933
|
-
case "by-feature":
|
|
1934
|
-
return extractDomain(context.name);
|
|
1935
|
-
case "none":
|
|
1936
|
-
default:
|
|
1937
|
-
return "";
|
|
1771
|
+
return applyGroupingStrategy(groupingRule, {
|
|
1772
|
+
name: eventName,
|
|
1773
|
+
tags: relatedTags ?? []
|
|
1774
|
+
});
|
|
1775
|
+
}
|
|
1776
|
+
function applyGroupingStrategy(rule, context) {
|
|
1777
|
+
switch (rule.strategy) {
|
|
1778
|
+
case "by-tag":
|
|
1779
|
+
return context.tags?.[0] ?? "untagged";
|
|
1780
|
+
case "by-owner":
|
|
1781
|
+
return context.owners?.[0] ?? "unowned";
|
|
1782
|
+
case "by-domain":
|
|
1783
|
+
return extractDomain(context.name);
|
|
1784
|
+
case "by-url-path-single":
|
|
1785
|
+
return extractUrlPathLevel(context.path, 1);
|
|
1786
|
+
case "by-url-path-multi":
|
|
1787
|
+
return extractUrlPathLevel(context.path, rule.urlPathLevel ?? 2);
|
|
1788
|
+
case "by-feature":
|
|
1789
|
+
return extractDomain(context.name);
|
|
1790
|
+
case "none":
|
|
1791
|
+
default:
|
|
1792
|
+
return "";
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
function extractDomain(name) {
|
|
1796
|
+
if (name.includes(".")) {
|
|
1797
|
+
return name.split(".")[0] ?? "default";
|
|
1798
|
+
}
|
|
1799
|
+
if (name.includes("_")) {
|
|
1800
|
+
return name.split("_")[0] ?? "default";
|
|
1801
|
+
}
|
|
1802
|
+
const match = name.match(/^([a-z]+)/i);
|
|
1803
|
+
return match?.[1]?.toLowerCase() ?? "default";
|
|
1804
|
+
}
|
|
1805
|
+
function extractUrlPathLevel(path, level) {
|
|
1806
|
+
if (!path)
|
|
1807
|
+
return "root";
|
|
1808
|
+
const segments = path.split("/").filter(Boolean);
|
|
1809
|
+
const nonParamSegments = segments.filter((s) => !s.startsWith("{"));
|
|
1810
|
+
if (nonParamSegments.length === 0)
|
|
1811
|
+
return "root";
|
|
1812
|
+
return nonParamSegments.slice(0, level).join("/");
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
// src/openapi/importer/models.ts
|
|
1816
|
+
function generateModelCode(name, schema, options) {
|
|
1817
|
+
const modelName = toPascalCase(toValidIdentifier(name));
|
|
1818
|
+
const schemaFormat = options.schemaFormat || "contractspec";
|
|
1819
|
+
const model = generateSchemaModelCode(schema, modelName, schemaFormat, options);
|
|
1820
|
+
let imports = "";
|
|
1821
|
+
if (model.imports && model.imports.length > 0) {
|
|
1822
|
+
imports = model.imports.join(`
|
|
1823
|
+
`);
|
|
1824
|
+
} else if (model.fields.length > 0) {
|
|
1825
|
+
imports = generateImports(model.fields, options);
|
|
1826
|
+
}
|
|
1827
|
+
return `
|
|
1828
|
+
${imports}
|
|
1829
|
+
|
|
1830
|
+
${model.code}
|
|
1831
|
+
`.trim();
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
// src/openapi/importer/schemas.ts
|
|
1835
|
+
function buildInputSchemas(operation) {
|
|
1836
|
+
const result = {};
|
|
1837
|
+
if (operation.pathParams.length > 0) {
|
|
1838
|
+
result.params = {
|
|
1839
|
+
type: "object",
|
|
1840
|
+
properties: operation.pathParams.reduce((acc, p) => {
|
|
1841
|
+
acc[p.name] = p.schema;
|
|
1842
|
+
return acc;
|
|
1843
|
+
}, {}),
|
|
1844
|
+
required: operation.pathParams.map((p) => p.name)
|
|
1845
|
+
};
|
|
1846
|
+
}
|
|
1847
|
+
if (operation.queryParams.length > 0) {
|
|
1848
|
+
result.query = {
|
|
1849
|
+
type: "object",
|
|
1850
|
+
properties: operation.queryParams.reduce((acc, p) => {
|
|
1851
|
+
acc[p.name] = p.schema;
|
|
1852
|
+
return acc;
|
|
1853
|
+
}, {}),
|
|
1854
|
+
required: operation.queryParams.filter((p) => p.required).map((p) => p.name)
|
|
1855
|
+
};
|
|
1856
|
+
}
|
|
1857
|
+
const excludedHeaders = [
|
|
1858
|
+
"authorization",
|
|
1859
|
+
"content-type",
|
|
1860
|
+
"accept",
|
|
1861
|
+
"user-agent"
|
|
1862
|
+
];
|
|
1863
|
+
const actualHeaders = operation.headerParams.filter((p) => !excludedHeaders.includes(p.name.toLowerCase()));
|
|
1864
|
+
if (actualHeaders.length > 0) {
|
|
1865
|
+
result.headers = {
|
|
1866
|
+
type: "object",
|
|
1867
|
+
properties: actualHeaders.reduce((acc, p) => {
|
|
1868
|
+
acc[p.name] = p.schema;
|
|
1869
|
+
return acc;
|
|
1870
|
+
}, {}),
|
|
1871
|
+
required: actualHeaders.filter((p) => p.required).map((p) => p.name)
|
|
1872
|
+
};
|
|
1873
|
+
}
|
|
1874
|
+
if (operation.requestBody?.schema) {
|
|
1875
|
+
result.body = operation.requestBody.schema;
|
|
1938
1876
|
}
|
|
1877
|
+
return result;
|
|
1939
1878
|
}
|
|
1940
|
-
function
|
|
1941
|
-
|
|
1942
|
-
|
|
1879
|
+
function getOutputSchema(operation) {
|
|
1880
|
+
const successCodes = ["200", "201", "202", "204"];
|
|
1881
|
+
for (const code of successCodes) {
|
|
1882
|
+
const response = operation.responses[code];
|
|
1883
|
+
if (response?.schema) {
|
|
1884
|
+
return response.schema;
|
|
1885
|
+
}
|
|
1943
1886
|
}
|
|
1944
|
-
|
|
1945
|
-
|
|
1887
|
+
for (const [code, response] of Object.entries(operation.responses)) {
|
|
1888
|
+
if (code.startsWith("2") && response.schema) {
|
|
1889
|
+
return response.schema;
|
|
1890
|
+
}
|
|
1946
1891
|
}
|
|
1947
|
-
|
|
1948
|
-
return match?.[1]?.toLowerCase() ?? "default";
|
|
1949
|
-
}
|
|
1950
|
-
function extractUrlPathLevel(path, level) {
|
|
1951
|
-
if (!path)
|
|
1952
|
-
return "root";
|
|
1953
|
-
const segments = path.split("/").filter(Boolean);
|
|
1954
|
-
const nonParamSegments = segments.filter((s) => !s.startsWith("{"));
|
|
1955
|
-
if (nonParamSegments.length === 0)
|
|
1956
|
-
return "root";
|
|
1957
|
-
return nonParamSegments.slice(0, level).join("/");
|
|
1892
|
+
return null;
|
|
1958
1893
|
}
|
|
1959
1894
|
|
|
1960
1895
|
// src/openapi/importer/index.ts
|
|
@@ -1963,74 +1898,74 @@ var importFromOpenApi = (parseResult, contractspecOptions, importOptions = {}) =
|
|
|
1963
1898
|
const specs = [];
|
|
1964
1899
|
const skipped = [];
|
|
1965
1900
|
const errors = [];
|
|
1966
|
-
for (const
|
|
1901
|
+
for (const operation of parseResult.operations) {
|
|
1967
1902
|
if (tags && tags.length > 0) {
|
|
1968
|
-
const hasMatchingTag =
|
|
1903
|
+
const hasMatchingTag = operation.tags.some((t) => tags.includes(t));
|
|
1969
1904
|
if (!hasMatchingTag) {
|
|
1970
1905
|
skipped.push({
|
|
1971
|
-
sourceId:
|
|
1972
|
-
reason: `No matching tags (has: ${
|
|
1906
|
+
sourceId: operation.operationId,
|
|
1907
|
+
reason: `No matching tags (has: ${operation.tags.join(", ")})`
|
|
1973
1908
|
});
|
|
1974
1909
|
continue;
|
|
1975
1910
|
}
|
|
1976
1911
|
}
|
|
1977
1912
|
if (include && include.length > 0) {
|
|
1978
|
-
if (!include.includes(
|
|
1913
|
+
if (!include.includes(operation.operationId)) {
|
|
1979
1914
|
skipped.push({
|
|
1980
|
-
sourceId:
|
|
1915
|
+
sourceId: operation.operationId,
|
|
1981
1916
|
reason: "Not in include list"
|
|
1982
1917
|
});
|
|
1983
1918
|
continue;
|
|
1984
1919
|
}
|
|
1985
|
-
} else if (exclude.includes(
|
|
1920
|
+
} else if (exclude.includes(operation.operationId)) {
|
|
1986
1921
|
skipped.push({
|
|
1987
|
-
sourceId:
|
|
1922
|
+
sourceId: operation.operationId,
|
|
1988
1923
|
reason: "In exclude list"
|
|
1989
1924
|
});
|
|
1990
1925
|
continue;
|
|
1991
1926
|
}
|
|
1992
|
-
if (
|
|
1927
|
+
if (operation.deprecated && importOptions.defaultStability !== "deprecated") {
|
|
1993
1928
|
skipped.push({
|
|
1994
|
-
sourceId:
|
|
1929
|
+
sourceId: operation.operationId,
|
|
1995
1930
|
reason: "Deprecated operation"
|
|
1996
1931
|
});
|
|
1997
1932
|
continue;
|
|
1998
1933
|
}
|
|
1999
1934
|
try {
|
|
2000
|
-
const inputSchemas = buildInputSchemas(
|
|
1935
|
+
const inputSchemas = buildInputSchemas(operation);
|
|
2001
1936
|
const schemaFormat = importOptions.schemaFormat || contractspecOptions.schemaFormat || "contractspec";
|
|
2002
|
-
const inputModel = inputSchemas.body ? generateSchemaModelCode(inputSchemas.body, `${
|
|
2003
|
-
const queryModel = inputSchemas.query ? generateSchemaModelCode(inputSchemas.query, `${
|
|
2004
|
-
const paramsModel = inputSchemas.params ? generateSchemaModelCode(inputSchemas.params, `${
|
|
2005
|
-
const headersModel = inputSchemas.headers ? generateSchemaModelCode(inputSchemas.headers, `${
|
|
2006
|
-
const outputSchema = getOutputSchema(
|
|
2007
|
-
let outputModel = outputSchema ? generateSchemaModelCode(outputSchema, `${
|
|
1937
|
+
const inputModel = inputSchemas.body ? generateSchemaModelCode(inputSchemas.body, `${operation.operationId}Input`, schemaFormat, contractspecOptions) : null;
|
|
1938
|
+
const queryModel = inputSchemas.query ? generateSchemaModelCode(inputSchemas.query, `${operation.operationId}Query`, schemaFormat, contractspecOptions) : null;
|
|
1939
|
+
const paramsModel = inputSchemas.params ? generateSchemaModelCode(inputSchemas.params, `${operation.operationId}Params`, schemaFormat, contractspecOptions) : null;
|
|
1940
|
+
const headersModel = inputSchemas.headers ? generateSchemaModelCode(inputSchemas.headers, `${operation.operationId}Headers`, schemaFormat, contractspecOptions) : null;
|
|
1941
|
+
const outputSchema = getOutputSchema(operation);
|
|
1942
|
+
let outputModel = outputSchema ? generateSchemaModelCode(outputSchema, `${operation.operationId}Output`, schemaFormat, contractspecOptions) : null;
|
|
2008
1943
|
if (outputModel && schemaFormat === "contractspec" && !outputModel.code.includes("defineSchemaModel")) {
|
|
2009
1944
|
outputModel = null;
|
|
2010
1945
|
}
|
|
2011
|
-
const code = generateSpecCode(
|
|
2012
|
-
const specName = toSpecKey(
|
|
1946
|
+
const code = generateSpecCode(operation, contractspecOptions, importOptions, inputModel, outputModel, queryModel, paramsModel, headersModel);
|
|
1947
|
+
const specName = toSpecKey(operation.operationId, importOptions.prefix);
|
|
2013
1948
|
const fileName = toFileName(specName);
|
|
2014
1949
|
const transportHints = {
|
|
2015
1950
|
rest: {
|
|
2016
|
-
method:
|
|
2017
|
-
path:
|
|
1951
|
+
method: operation.method.toUpperCase(),
|
|
1952
|
+
path: operation.path,
|
|
2018
1953
|
params: {
|
|
2019
|
-
path:
|
|
2020
|
-
query:
|
|
2021
|
-
header:
|
|
2022
|
-
cookie:
|
|
1954
|
+
path: operation.pathParams.map((p) => p.name),
|
|
1955
|
+
query: operation.queryParams.map((p) => p.name),
|
|
1956
|
+
header: operation.headerParams.map((p) => p.name),
|
|
1957
|
+
cookie: operation.cookieParams.map((p) => p.name)
|
|
2023
1958
|
}
|
|
2024
1959
|
}
|
|
2025
1960
|
};
|
|
2026
1961
|
const source = {
|
|
2027
1962
|
type: "openapi",
|
|
2028
|
-
sourceId:
|
|
2029
|
-
operationId:
|
|
1963
|
+
sourceId: operation.operationId,
|
|
1964
|
+
operationId: operation.operationId,
|
|
2030
1965
|
openApiVersion: parseResult.version,
|
|
2031
1966
|
importedAt: new Date
|
|
2032
1967
|
};
|
|
2033
|
-
const groupFolder = resolveOperationGroupFolder(
|
|
1968
|
+
const groupFolder = resolveOperationGroupFolder(operation, contractspecOptions.conventions);
|
|
2034
1969
|
specs.push({
|
|
2035
1970
|
code,
|
|
2036
1971
|
fileName,
|
|
@@ -2040,7 +1975,7 @@ var importFromOpenApi = (parseResult, contractspecOptions, importOptions = {}) =
|
|
|
2040
1975
|
});
|
|
2041
1976
|
} catch (error) {
|
|
2042
1977
|
errors.push({
|
|
2043
|
-
sourceId:
|
|
1978
|
+
sourceId: operation.operationId,
|
|
2044
1979
|
error: error instanceof Error ? error.message : String(error)
|
|
2045
1980
|
});
|
|
2046
1981
|
}
|
|
@@ -2113,248 +2048,317 @@ var importFromOpenApi = (parseResult, contractspecOptions, importOptions = {}) =
|
|
|
2113
2048
|
}
|
|
2114
2049
|
};
|
|
2115
2050
|
};
|
|
2116
|
-
function importOperation(
|
|
2117
|
-
const inputSchemas = buildInputSchemas(
|
|
2051
|
+
function importOperation(operation, options = {}, contractspecOptions) {
|
|
2052
|
+
const inputSchemas = buildInputSchemas(operation);
|
|
2118
2053
|
const schemaFormat = options.schemaFormat || contractspecOptions.schemaFormat || "contractspec";
|
|
2119
|
-
const inputModel = inputSchemas.body ? generateSchemaModelCode(inputSchemas.body, `${
|
|
2120
|
-
const queryModel = inputSchemas.query ? generateSchemaModelCode(inputSchemas.query, `${
|
|
2121
|
-
const paramsModel = inputSchemas.params ? generateSchemaModelCode(inputSchemas.params, `${
|
|
2122
|
-
const headersModel = inputSchemas.headers ? generateSchemaModelCode(inputSchemas.headers, `${
|
|
2123
|
-
const outputSchema = getOutputSchema(
|
|
2124
|
-
const outputModel = outputSchema ? generateSchemaModelCode(outputSchema, `${
|
|
2125
|
-
return generateSpecCode(
|
|
2054
|
+
const inputModel = inputSchemas.body ? generateSchemaModelCode(inputSchemas.body, `${operation.operationId}Input`, schemaFormat, contractspecOptions) : null;
|
|
2055
|
+
const queryModel = inputSchemas.query ? generateSchemaModelCode(inputSchemas.query, `${operation.operationId}Query`, schemaFormat, contractspecOptions) : null;
|
|
2056
|
+
const paramsModel = inputSchemas.params ? generateSchemaModelCode(inputSchemas.params, `${operation.operationId}Params`, schemaFormat, contractspecOptions) : null;
|
|
2057
|
+
const headersModel = inputSchemas.headers ? generateSchemaModelCode(inputSchemas.headers, `${operation.operationId}Headers`, schemaFormat, contractspecOptions) : null;
|
|
2058
|
+
const outputSchema = getOutputSchema(operation);
|
|
2059
|
+
const outputModel = outputSchema ? generateSchemaModelCode(outputSchema, `${operation.operationId}Output`, schemaFormat, contractspecOptions) : null;
|
|
2060
|
+
return generateSpecCode(operation, contractspecOptions, options, inputModel, outputModel, queryModel, paramsModel, headersModel);
|
|
2126
2061
|
}
|
|
2127
|
-
// src/openapi/
|
|
2128
|
-
function
|
|
2129
|
-
|
|
2130
|
-
|
|
2062
|
+
// src/openapi/parser/resolvers.ts
|
|
2063
|
+
function isReference3(obj) {
|
|
2064
|
+
return typeof obj === "object" && obj !== null && "$ref" in obj;
|
|
2065
|
+
}
|
|
2066
|
+
function resolveRef(doc, ref) {
|
|
2067
|
+
if (!ref.startsWith("#/")) {
|
|
2068
|
+
return;
|
|
2131
2069
|
}
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2070
|
+
const path = ref.slice(2).split("/");
|
|
2071
|
+
let current = doc;
|
|
2072
|
+
for (const part of path) {
|
|
2073
|
+
if (current === null || current === undefined)
|
|
2074
|
+
return;
|
|
2075
|
+
if (typeof current !== "object")
|
|
2076
|
+
return;
|
|
2077
|
+
current = current[part];
|
|
2139
2078
|
}
|
|
2140
|
-
return
|
|
2141
|
-
path,
|
|
2142
|
-
type: changeType,
|
|
2143
|
-
oldValue,
|
|
2144
|
-
newValue,
|
|
2145
|
-
description
|
|
2146
|
-
};
|
|
2079
|
+
return current;
|
|
2147
2080
|
}
|
|
2148
|
-
function
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2081
|
+
function dereferenceSchema(doc, schema, seen = new Set) {
|
|
2082
|
+
if (!schema)
|
|
2083
|
+
return;
|
|
2084
|
+
if (isReference3(schema)) {
|
|
2085
|
+
if (seen.has(schema.$ref)) {
|
|
2086
|
+
return schema;
|
|
2087
|
+
}
|
|
2088
|
+
const newSeen = new Set(seen);
|
|
2089
|
+
newSeen.add(schema.$ref);
|
|
2090
|
+
const resolved = resolveRef(doc, schema.$ref);
|
|
2091
|
+
if (!resolved)
|
|
2092
|
+
return schema;
|
|
2093
|
+
const dereferenced = dereferenceSchema(doc, resolved, newSeen);
|
|
2094
|
+
if (!dereferenced)
|
|
2095
|
+
return schema;
|
|
2096
|
+
const refParts = schema.$ref.split("/");
|
|
2097
|
+
const typeName = refParts[refParts.length - 1];
|
|
2098
|
+
return {
|
|
2099
|
+
...dereferenced,
|
|
2100
|
+
_originalRef: schema.$ref,
|
|
2101
|
+
_originalTypeName: typeName
|
|
2102
|
+
};
|
|
2160
2103
|
}
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
}
|
|
2168
|
-
|
|
2104
|
+
const schemaObj = { ...schema };
|
|
2105
|
+
if (schemaObj.properties) {
|
|
2106
|
+
const props = schemaObj.properties;
|
|
2107
|
+
const newProps = {};
|
|
2108
|
+
for (const [key, prop] of Object.entries(props)) {
|
|
2109
|
+
newProps[key] = dereferenceSchema(doc, prop, seen) ?? prop;
|
|
2110
|
+
}
|
|
2111
|
+
schemaObj.properties = newProps;
|
|
2169
2112
|
}
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2113
|
+
if (schemaObj.items) {
|
|
2114
|
+
schemaObj.items = dereferenceSchema(doc, schemaObj.items, seen);
|
|
2115
|
+
}
|
|
2116
|
+
const combinators = ["allOf", "anyOf", "oneOf"];
|
|
2117
|
+
for (const comb of combinators) {
|
|
2118
|
+
if (Array.isArray(schemaObj[comb])) {
|
|
2119
|
+
schemaObj[comb] = schemaObj[comb].map((s) => dereferenceSchema(doc, s, seen) ?? s);
|
|
2175
2120
|
}
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2121
|
+
}
|
|
2122
|
+
return schemaObj;
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
// src/openapi/parser/parameters.ts
|
|
2126
|
+
function parseParameters(doc, params) {
|
|
2127
|
+
const result = {
|
|
2128
|
+
path: [],
|
|
2129
|
+
query: [],
|
|
2130
|
+
header: [],
|
|
2131
|
+
cookie: []
|
|
2132
|
+
};
|
|
2133
|
+
if (!params)
|
|
2134
|
+
return result;
|
|
2135
|
+
for (const param of params) {
|
|
2136
|
+
let resolved;
|
|
2137
|
+
if (isReference3(param)) {
|
|
2138
|
+
const ref = resolveRef(doc, param.$ref);
|
|
2139
|
+
if (!ref)
|
|
2140
|
+
continue;
|
|
2141
|
+
resolved = ref;
|
|
2180
2142
|
} else {
|
|
2181
|
-
|
|
2182
|
-
if (change) {
|
|
2183
|
-
changes.push(change);
|
|
2184
|
-
}
|
|
2143
|
+
resolved = param;
|
|
2185
2144
|
}
|
|
2145
|
+
const parsed = {
|
|
2146
|
+
name: resolved.name,
|
|
2147
|
+
in: resolved.in,
|
|
2148
|
+
required: resolved.required ?? resolved.in === "path",
|
|
2149
|
+
description: resolved.description,
|
|
2150
|
+
schema: dereferenceSchema(doc, resolved.schema),
|
|
2151
|
+
deprecated: resolved.deprecated ?? false
|
|
2152
|
+
};
|
|
2153
|
+
result[resolved.in]?.push(parsed);
|
|
2186
2154
|
}
|
|
2187
|
-
return
|
|
2155
|
+
return result;
|
|
2188
2156
|
}
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
description: "Tags changed"
|
|
2206
|
-
});
|
|
2207
|
-
}
|
|
2157
|
+
|
|
2158
|
+
// src/openapi/parser/utils.ts
|
|
2159
|
+
import { parse as parseYaml } from "yaml";
|
|
2160
|
+
var HTTP_METHODS = [
|
|
2161
|
+
"get",
|
|
2162
|
+
"post",
|
|
2163
|
+
"put",
|
|
2164
|
+
"delete",
|
|
2165
|
+
"patch",
|
|
2166
|
+
"head",
|
|
2167
|
+
"options",
|
|
2168
|
+
"trace"
|
|
2169
|
+
];
|
|
2170
|
+
function parseOpenApiString(content, format = "json") {
|
|
2171
|
+
if (format === "yaml") {
|
|
2172
|
+
return parseYaml(content);
|
|
2208
2173
|
}
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
type: "modified",
|
|
2216
|
-
oldValue: specMethod,
|
|
2217
|
-
newValue: opMethod,
|
|
2218
|
-
description: "HTTP method changed"
|
|
2219
|
-
});
|
|
2220
|
-
}
|
|
2221
|
-
const specPath = spec.transport?.rest?.path;
|
|
2222
|
-
if (specPath && specPath !== operation2.path) {
|
|
2223
|
-
changes.push({
|
|
2224
|
-
path: "transport.rest.path",
|
|
2225
|
-
type: "modified",
|
|
2226
|
-
oldValue: specPath,
|
|
2227
|
-
newValue: operation2.path,
|
|
2228
|
-
description: "Path changed"
|
|
2229
|
-
});
|
|
2230
|
-
}
|
|
2174
|
+
return JSON.parse(content);
|
|
2175
|
+
}
|
|
2176
|
+
function detectFormat(content) {
|
|
2177
|
+
const trimmed = content.trim();
|
|
2178
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
2179
|
+
return "json";
|
|
2231
2180
|
}
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
newValue: operation2.deprecated ? "deprecated" : "stable",
|
|
2239
|
-
description: "Deprecation status changed"
|
|
2240
|
-
});
|
|
2181
|
+
return "yaml";
|
|
2182
|
+
}
|
|
2183
|
+
function detectVersion(doc) {
|
|
2184
|
+
const version = doc.openapi;
|
|
2185
|
+
if (version.startsWith("3.1")) {
|
|
2186
|
+
return "3.1";
|
|
2241
2187
|
}
|
|
2242
|
-
return
|
|
2188
|
+
return "3.0";
|
|
2243
2189
|
}
|
|
2244
|
-
function
|
|
2245
|
-
const
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
...options.ignoreDescriptions ? ["meta.description", "meta.goal", "meta.context"] : [],
|
|
2251
|
-
...options.ignoreTags ? ["meta.tags"] : []
|
|
2252
|
-
]
|
|
2190
|
+
function generateOperationId(method, path) {
|
|
2191
|
+
const pathParts = path.split("/").filter(Boolean).map((part) => {
|
|
2192
|
+
if (part.startsWith("{") && part.endsWith("}")) {
|
|
2193
|
+
return "By" + part.slice(1, -1).charAt(0).toUpperCase() + part.slice(2, -1);
|
|
2194
|
+
}
|
|
2195
|
+
return part.charAt(0).toUpperCase() + part.slice(1);
|
|
2253
2196
|
});
|
|
2254
|
-
|
|
2255
|
-
if (!options.ignoreTransport) {
|
|
2256
|
-
const transportChanges = diffObjects("transport", oldSpec.transport, newSpec.transport, options);
|
|
2257
|
-
changes.push(...transportChanges);
|
|
2258
|
-
}
|
|
2259
|
-
const policyChanges = diffObjects("policy", oldSpec.policy, newSpec.policy, options);
|
|
2260
|
-
changes.push(...policyChanges);
|
|
2261
|
-
return changes;
|
|
2197
|
+
return method + pathParts.join("");
|
|
2262
2198
|
}
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
changes = [
|
|
2281
|
-
{
|
|
2282
|
-
path: "",
|
|
2283
|
-
type: "added",
|
|
2284
|
-
newValue: incoming.operationSpec ?? incoming.code,
|
|
2285
|
-
description: "New spec imported from OpenAPI"
|
|
2199
|
+
|
|
2200
|
+
// src/openapi/parser/operation.ts
|
|
2201
|
+
function parseOperation(doc, method, path, operation, pathParams) {
|
|
2202
|
+
const allParams = [...pathParams ?? [], ...operation.parameters ?? []];
|
|
2203
|
+
const params = parseParameters(doc, allParams);
|
|
2204
|
+
let requestBody;
|
|
2205
|
+
if (operation.requestBody) {
|
|
2206
|
+
const body = isReference3(operation.requestBody) ? resolveRef(doc, operation.requestBody.$ref) : operation.requestBody;
|
|
2207
|
+
if (body) {
|
|
2208
|
+
const contentType = Object.keys(body.content ?? {})[0] ?? "application/json";
|
|
2209
|
+
const content = body.content?.[contentType];
|
|
2210
|
+
if (content?.schema) {
|
|
2211
|
+
requestBody = {
|
|
2212
|
+
required: body.required ?? false,
|
|
2213
|
+
schema: dereferenceSchema(doc, content.schema) ?? {},
|
|
2214
|
+
contentType
|
|
2215
|
+
};
|
|
2286
2216
|
}
|
|
2287
|
-
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
const responses = {};
|
|
2220
|
+
for (const [status, response] of Object.entries(operation.responses ?? {})) {
|
|
2221
|
+
const resolved = isReference3(response) ? resolveRef(doc, response.$ref) : response;
|
|
2222
|
+
if (resolved) {
|
|
2223
|
+
const contentType = Object.keys(resolved.content ?? {})[0];
|
|
2224
|
+
const content = contentType ? resolved.content?.[contentType] : undefined;
|
|
2225
|
+
responses[status] = {
|
|
2226
|
+
description: resolved.description,
|
|
2227
|
+
schema: content?.schema ? dereferenceSchema(doc, content.schema) : undefined,
|
|
2228
|
+
contentType
|
|
2229
|
+
};
|
|
2230
|
+
}
|
|
2288
2231
|
}
|
|
2232
|
+
const contractSpecMeta = operation?.["x-contractspec"];
|
|
2289
2233
|
return {
|
|
2290
|
-
operationId,
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2234
|
+
operationId: operation.operationId ?? generateOperationId(method, path),
|
|
2235
|
+
method,
|
|
2236
|
+
path,
|
|
2237
|
+
summary: operation.summary,
|
|
2238
|
+
description: operation.description,
|
|
2239
|
+
tags: operation.tags ?? [],
|
|
2240
|
+
pathParams: params.path,
|
|
2241
|
+
queryParams: params.query,
|
|
2242
|
+
headerParams: params.header,
|
|
2243
|
+
cookieParams: params.cookie,
|
|
2244
|
+
requestBody,
|
|
2245
|
+
responses,
|
|
2246
|
+
deprecated: operation.deprecated ?? false,
|
|
2247
|
+
security: operation.security,
|
|
2248
|
+
contractSpecMeta
|
|
2295
2249
|
};
|
|
2296
2250
|
}
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2251
|
+
|
|
2252
|
+
// src/openapi/parser/document.ts
|
|
2253
|
+
function parseOpenApiDocument(doc, _options = {}) {
|
|
2254
|
+
const version = detectVersion(doc);
|
|
2255
|
+
const warnings = [];
|
|
2256
|
+
const operations = [];
|
|
2257
|
+
for (const [path, pathItem] of Object.entries(doc.paths ?? {})) {
|
|
2258
|
+
if (!pathItem)
|
|
2259
|
+
continue;
|
|
2260
|
+
const pathParams = pathItem.parameters;
|
|
2261
|
+
for (const method of HTTP_METHODS) {
|
|
2262
|
+
const operation = pathItem[method];
|
|
2263
|
+
if (operation) {
|
|
2264
|
+
try {
|
|
2265
|
+
operations.push(parseOperation(doc, method, path, operation, pathParams));
|
|
2266
|
+
} catch (error) {
|
|
2267
|
+
warnings.push(`Failed to parse ${method.toUpperCase()} ${path}: ${error}`);
|
|
2268
|
+
}
|
|
2309
2269
|
}
|
|
2310
2270
|
}
|
|
2311
|
-
diffs.push(createSpecDiff(operationId, existing, imported, options));
|
|
2312
2271
|
}
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
incoming: undefined,
|
|
2319
|
-
changes: [
|
|
2320
|
-
{
|
|
2321
|
-
path: "",
|
|
2322
|
-
type: "removed",
|
|
2323
|
-
oldValue: spec,
|
|
2324
|
-
description: "Spec no longer exists in OpenAPI source"
|
|
2325
|
-
}
|
|
2326
|
-
],
|
|
2327
|
-
isEquivalent: false
|
|
2328
|
-
});
|
|
2272
|
+
const schemas2 = {};
|
|
2273
|
+
const components = doc.components;
|
|
2274
|
+
if (components?.schemas) {
|
|
2275
|
+
for (const [name, schema] of Object.entries(components.schemas)) {
|
|
2276
|
+
schemas2[name] = schema;
|
|
2329
2277
|
}
|
|
2330
2278
|
}
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2279
|
+
const servers = (doc.servers ?? []).map((s) => ({
|
|
2280
|
+
url: s.url,
|
|
2281
|
+
description: s.description,
|
|
2282
|
+
variables: s.variables
|
|
2283
|
+
}));
|
|
2284
|
+
const events2 = [];
|
|
2285
|
+
if ("webhooks" in doc && doc.webhooks) {
|
|
2286
|
+
for (const [name, pathItem] of Object.entries(doc.webhooks)) {
|
|
2287
|
+
if (typeof pathItem !== "object" || !pathItem)
|
|
2288
|
+
continue;
|
|
2289
|
+
const operation = pathItem["post"];
|
|
2290
|
+
if (operation && operation.requestBody) {
|
|
2291
|
+
if ("$ref" in operation.requestBody) {
|
|
2292
|
+
throw new Error(`'$ref' isn't supported`);
|
|
2293
|
+
}
|
|
2294
|
+
const content = operation.requestBody.content?.["application/json"];
|
|
2295
|
+
if (content?.schema) {
|
|
2296
|
+
events2.push({
|
|
2297
|
+
name,
|
|
2298
|
+
description: operation.summary || operation.description,
|
|
2299
|
+
payload: content.schema
|
|
2300
|
+
});
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2336
2304
|
}
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2305
|
+
return {
|
|
2306
|
+
document: doc,
|
|
2307
|
+
version,
|
|
2308
|
+
info: {
|
|
2309
|
+
title: doc.info.title,
|
|
2310
|
+
version: doc.info.version,
|
|
2311
|
+
description: doc.info.description
|
|
2312
|
+
},
|
|
2313
|
+
operations,
|
|
2314
|
+
schemas: schemas2,
|
|
2315
|
+
servers,
|
|
2316
|
+
warnings,
|
|
2317
|
+
events: events2
|
|
2318
|
+
};
|
|
2319
|
+
}
|
|
2320
|
+
async function parseOpenApi(source, options = {}) {
|
|
2321
|
+
const {
|
|
2322
|
+
fetch: fetchFn = globalThis.fetch,
|
|
2323
|
+
readFile,
|
|
2324
|
+
timeout = 30000
|
|
2325
|
+
} = options;
|
|
2326
|
+
let content;
|
|
2327
|
+
let format;
|
|
2328
|
+
if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
2329
|
+
const controller = new AbortController;
|
|
2330
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
2331
|
+
try {
|
|
2332
|
+
const response = await fetchFn(source, { signal: controller.signal });
|
|
2333
|
+
if (!response.ok) {
|
|
2334
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
2335
|
+
}
|
|
2336
|
+
content = await response.text();
|
|
2337
|
+
} finally {
|
|
2338
|
+
clearTimeout(timeoutId);
|
|
2339
|
+
}
|
|
2340
|
+
if (source.endsWith(".yaml") || source.endsWith(".yml")) {
|
|
2341
|
+
format = "yaml";
|
|
2342
|
+
} else if (source.endsWith(".json")) {
|
|
2343
|
+
format = "json";
|
|
2344
|
+
} else {
|
|
2345
|
+
format = detectFormat(content);
|
|
2346
|
+
}
|
|
2347
|
+
} else {
|
|
2348
|
+
if (!readFile) {
|
|
2349
|
+
throw new Error("readFile adapter required for file paths");
|
|
2350
|
+
}
|
|
2351
|
+
content = await readFile(source);
|
|
2352
|
+
if (source.endsWith(".yaml") || source.endsWith(".yml")) {
|
|
2353
|
+
format = "yaml";
|
|
2354
|
+
} else if (source.endsWith(".json")) {
|
|
2355
|
+
format = "json";
|
|
2356
|
+
} else {
|
|
2357
|
+
format = detectFormat(content);
|
|
2354
2358
|
}
|
|
2355
2359
|
}
|
|
2356
|
-
|
|
2357
|
-
|
|
2360
|
+
const doc = parseOpenApiString(content, format);
|
|
2361
|
+
return parseOpenApiDocument(doc, options);
|
|
2358
2362
|
}
|
|
2359
2363
|
export {
|
|
2360
2364
|
toSchemaName,
|