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