@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.
@@ -1,55 +1,59 @@
1
- // src/openapi/parser/utils.ts
2
- import { parse as parseYaml } from "yaml";
3
- var HTTP_METHODS = [
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 detectFormat(content) {
20
- const trimmed = content.trim();
21
- if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
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 detectVersion(doc) {
27
- const version = doc.openapi;
28
- if (version.startsWith("3.1")) {
29
- return "3.1";
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 "3.0";
20
+ return result;
32
21
  }
33
- function generateOperationId(method, path) {
34
- const pathParts = path.split("/").filter(Boolean).map((part) => {
35
- if (part.startsWith("{") && part.endsWith("}")) {
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
- // src/openapi/parser/resolvers.ts
43
- function isReference(obj) {
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 resolveRef(doc, ref) {
47
- if (!ref.startsWith("#/")) {
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
- const path = ref.slice(2).split("/");
51
- let current = doc;
52
- for (const part of path) {
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 dereferenceSchema(doc, schema, seen = new Set) {
62
- if (!schema)
63
- return;
64
- if (isReference(schema)) {
65
- if (seen.has(schema.$ref)) {
66
- return schema;
67
- }
68
- const newSeen = new Set(seen);
69
- newSeen.add(schema.$ref);
70
- const resolved = resolveRef(doc, schema.$ref);
71
- if (!resolved)
72
- return schema;
73
- const dereferenced = dereferenceSchema(doc, resolved, newSeen);
74
- if (!dereferenced)
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
- const schemaObj = { ...schema };
85
- if (schemaObj.properties) {
86
- const props = schemaObj.properties;
87
- const newProps = {};
88
- for (const [key, prop] of Object.entries(props)) {
89
- newProps[key] = dereferenceSchema(doc, prop, seen) ?? prop;
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
- if (schemaObj.items) {
94
- schemaObj.items = dereferenceSchema(doc, schemaObj.items, seen);
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
- const combinators = ["allOf", "anyOf", "oneOf"];
97
- for (const comb of combinators) {
98
- if (Array.isArray(schemaObj[comb])) {
99
- schemaObj[comb] = schemaObj[comb].map((s) => dereferenceSchema(doc, s, seen) ?? s);
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
- return schemaObj;
103
- }
104
- // src/openapi/parser/parameters.ts
105
- function parseParameters(doc, params) {
106
- const result = {
107
- path: [],
108
- query: [],
109
- header: [],
110
- cookie: []
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
- resolved = param;
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 result;
135
+ return changes;
135
136
  }
136
- // src/openapi/parser/operation.ts
137
- function parseOperation(doc, method, path, operation, pathParams) {
138
- const allParams = [...pathParams ?? [], ...operation.parameters ?? []];
139
- const params = parseParameters(doc, allParams);
140
- let requestBody;
141
- if (operation.requestBody) {
142
- const body = isReference(operation.requestBody) ? resolveRef(doc, operation.requestBody.$ref) : operation.requestBody;
143
- if (body) {
144
- const contentType = Object.keys(body.content ?? {})[0] ?? "application/json";
145
- const content = body.content?.[contentType];
146
- if (content?.schema) {
147
- requestBody = {
148
- required: body.required ?? false,
149
- schema: dereferenceSchema(doc, content.schema) ?? {},
150
- contentType
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
- const responses = {};
156
- for (const [status, response] of Object.entries(operation.responses ?? {})) {
157
- const resolved = isReference(response) ? resolveRef(doc, response.$ref) : response;
158
- if (resolved) {
159
- const contentType = Object.keys(resolved.content ?? {})[0];
160
- const content = contentType ? resolved.content?.[contentType] : undefined;
161
- responses[status] = {
162
- description: resolved.description,
163
- schema: content?.schema ? dereferenceSchema(doc, content.schema) : undefined,
164
- contentType
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 contractSpecMeta = operation?.["x-contractspec"];
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: operation.operationId ?? generateOperationId(method, path),
171
- method,
172
- path,
173
- summary: operation.summary,
174
- description: operation.description,
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
- // src/openapi/parser/document.ts
188
- function parseOpenApiDocument(doc, _options = {}) {
189
- const version = detectVersion(doc);
190
- const warnings = [];
191
- const operations = [];
192
- for (const [path, pathItem] of Object.entries(doc.paths ?? {})) {
193
- if (!pathItem)
194
- continue;
195
- const pathParams = pathItem.parameters;
196
- for (const method of HTTP_METHODS) {
197
- const operation = pathItem[method];
198
- if (operation) {
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 schemas = {};
208
- const components = doc.components;
209
- if (components?.schemas) {
210
- for (const [name, schema] of Object.entries(components.schemas)) {
211
- schemas[name] = schema;
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
- const servers = (doc.servers ?? []).map((s) => ({
215
- url: s.url,
216
- description: s.description,
217
- variables: s.variables
218
- }));
219
- const events = [];
220
- if ("webhooks" in doc && doc.webhooks) {
221
- for (const [name, pathItem] of Object.entries(doc.webhooks)) {
222
- if (typeof pathItem !== "object" || !pathItem)
223
- continue;
224
- const operation = pathItem["post"];
225
- if (operation && operation.requestBody) {
226
- if ("$ref" in operation.requestBody) {
227
- throw new Error(`'$ref' isn't supported`);
228
- }
229
- const content = operation.requestBody.content?.["application/json"];
230
- if (content?.schema) {
231
- events.push({
232
- name,
233
- description: operation.summary || operation.description,
234
- payload: content.schema
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
- document: doc,
242
- version,
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
- async function parseOpenApi(source, options = {}) {
256
- const {
257
- fetch: fetchFn = globalThis.fetch,
258
- readFile,
259
- timeout = 30000
260
- } = options;
261
- let content;
262
- let format;
263
- if (source.startsWith("http://") || source.startsWith("https://")) {
264
- const controller = new AbortController;
265
- const timeoutId = setTimeout(() => controller.abort(), timeout);
266
- try {
267
- const response = await fetchFn(source, { signal: controller.signal });
268
- if (!response.ok) {
269
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
270
- }
271
- content = await response.text();
272
- } finally {
273
- clearTimeout(timeoutId);
274
- }
275
- if (source.endsWith(".yaml") || source.endsWith(".yml")) {
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 doc = parseOpenApiString(content, format);
296
- return parseOpenApiDocument(doc, options);
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 z.toJSONSchema(schema.getZod());
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
- const code = `/**
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 generateFeaturesRegistry(registry) {
466
- const features = registry.list();
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 feature of features) {
470
- const featureVarName = feature.meta.key.replace(/-/g, "_");
471
- imports.add(`import { ${featureVarName} } from './${feature.meta.key}';`);
472
- registrations.push(` .register(${featureVarName})`);
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 features registry.
567
+ * Auto-generated operations registry.
476
568
  * DO NOT EDIT - This file is generated by ContractSpec exporter.
477
569
  */
478
- import { FeatureRegistry } from '@contractspec/lib.contracts-spec';
570
+ import { OperationSpecRegistry } from '@contractspec/lib.contracts-spec';
479
571
 
480
572
  ${Array.from(imports).join(`
481
573
  `)}
482
574
 
483
- export const featuresRegistry = new FeatureRegistry()
575
+ export const operationsRegistry = new OperationSpecRegistry()
484
576
  ${registrations.join(`
485
577
  `)};
486
578
  `;
487
579
  return {
488
580
  code,
489
- fileName: "features-registry.ts"
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/forms.ts
545
- import { z as z3 } from "zod";
546
- function exportForms(registry) {
547
- return registry.list().map((form) => ({
548
- key: form.meta.key,
549
- version: form.meta.version,
550
- description: form.meta.description,
551
- stability: form.meta.stability,
552
- owners: form.meta.owners,
553
- fields: form.fields,
554
- model: form.model ? z3.toJSONSchema(form.model.getZod()) : null,
555
- actions: form.actions
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
- const code = `/**
568
- * Auto-generated forms registry.
569
- * DO NOT EDIT - This file is generated by ContractSpec exporter.
570
- */
571
- import { FormRegistry } from '@contractspec/lib.contracts-spec';
572
-
573
- ${Array.from(imports).join(`
574
- `)}
575
-
576
- export const formsRegistry = new FormRegistry()
577
- ${registrations.join(`
578
- `)};
579
- `;
580
- return {
581
- code,
582
- fileName: "forms-registry.ts"
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 data views registry.
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
- ${Array.from(imports).join(`
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: "dataviews-registry.ts"
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 current;
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 isReference2(schema) {
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 (isReference2(schema)) {
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 (!isReference2(schema)) {
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: !isReference2(schema) ? schema["description"] : undefined
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 isReference3(schema) {
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 (isReference3(schema)) {
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 (isReference3(schema)) {
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/schemas.ts
1653
- function buildInputSchemas(operation2) {
1654
- const result = {};
1655
- if (operation2.pathParams.length > 0) {
1656
- result.params = {
1657
- type: "object",
1658
- properties: operation2.pathParams.reduce((acc, p) => {
1659
- acc[p.name] = p.schema;
1660
- return acc;
1661
- }, {}),
1662
- required: operation2.pathParams.map((p) => p.name)
1663
- };
1664
- }
1665
- if (operation2.queryParams.length > 0) {
1666
- result.query = {
1667
- type: "object",
1668
- properties: operation2.queryParams.reduce((acc, p) => {
1669
- acc[p.name] = p.schema;
1670
- return acc;
1671
- }, {}),
1672
- required: operation2.queryParams.filter((p) => p.required).map((p) => p.name)
1673
- };
1674
- }
1675
- const excludedHeaders = [
1676
- "authorization",
1677
- "content-type",
1678
- "accept",
1679
- "user-agent"
1680
- ];
1681
- const actualHeaders = operation2.headerParams.filter((p) => !excludedHeaders.includes(p.name.toLowerCase()));
1682
- if (actualHeaders.length > 0) {
1683
- result.headers = {
1684
- type: "object",
1685
- properties: actualHeaders.reduce((acc, p) => {
1686
- acc[p.name] = p.schema;
1687
- return acc;
1688
- }, {}),
1689
- required: actualHeaders.filter((p) => p.required).map((p) => p.name)
1690
- };
1691
- }
1692
- if (operation2.requestBody?.schema) {
1693
- result.body = operation2.requestBody.schema;
1694
- }
1695
- return result;
1696
- }
1697
- function getOutputSchema(operation2) {
1698
- const successCodes = ["200", "201", "202", "204"];
1699
- for (const code of successCodes) {
1700
- const response = operation2.responses[code];
1701
- if (response?.schema) {
1702
- return response.schema;
1703
- }
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
- for (const [code, response] of Object.entries(operation2.responses)) {
1706
- if (code.startsWith("2") && response.schema) {
1707
- return response.schema;
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
- return null;
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(operation2, defaultAuth) {
1719
- if (!operation2.security || operation2.security.length === 0) {
1630
+ function inferAuthLevel(operation, defaultAuth) {
1631
+ if (!operation.security || operation.security.length === 0) {
1720
1632
  return defaultAuth;
1721
1633
  }
1722
- for (const sec of operation2.security) {
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(operation2, contractspecConfig, options = {}, inputModel, outputModel, queryModel = null, paramsModel = null, headersModel = null) {
1732
- const specKey = toSpecKey(operation2.operationId, options.prefix);
1733
- const kind = inferOpKind(operation2.method);
1734
- const auth = inferAuthLevel(operation2, options.defaultAuth ?? "user");
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(operation2.operationId));
1691
+ const safeName = toValidIdentifier(toPascalCase(operation.operationId));
1780
1692
  lines.push(`/**`);
1781
- lines.push(` * ${operation2.summary ?? operation2.operationId}`);
1782
- if (operation2.description) {
1693
+ lines.push(` * ${operation.summary ?? operation.operationId}`);
1694
+ if (operation.description) {
1783
1695
  lines.push(` *`);
1784
- lines.push(` * ${operation2.description}`);
1696
+ lines.push(` * ${operation.description}`);
1785
1697
  }
1786
1698
  lines.push(` *`);
1787
- lines.push(` * @source OpenAPI: ${operation2.method.toUpperCase()} ${operation2.path}`);
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: [${operation2.tags.map((t) => `'${t}'`).join(", ")}],`);
1796
- lines.push(` description: ${JSON.stringify(operation2.summary ?? operation2.operationId)},`);
1797
- lines.push(` goal: ${JSON.stringify(operation2.description ?? "Imported from OpenAPI")},`);
1798
- lines.push(` context: 'Imported from OpenAPI: ${operation2.method.toUpperCase()} ${operation2.path}',`);
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 = operation2.method.toUpperCase();
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: '${operation2.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(operation2, conventions) {
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: operation2.operationId,
1895
- tags: operation2.tags,
1896
- path: operation2.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 extractDomain(name) {
1940
- if (name.includes(".")) {
1941
- return name.split(".")[0] ?? "default";
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
- if (name.includes("_")) {
1944
- return name.split("_")[0] ?? "default";
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
- const match = name.match(/^([a-z]+)/i);
1947
- return match?.[1]?.toLowerCase() ?? "default";
1948
- }
1949
- function extractUrlPathLevel(path, level) {
1950
- if (!path)
1951
- return "root";
1952
- const segments = path.split("/").filter(Boolean);
1953
- const nonParamSegments = segments.filter((s) => !s.startsWith("{"));
1954
- if (nonParamSegments.length === 0)
1955
- return "root";
1956
- return nonParamSegments.slice(0, level).join("/");
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 operation2 of parseResult.operations) {
1900
+ for (const operation of parseResult.operations) {
1966
1901
  if (tags && tags.length > 0) {
1967
- const hasMatchingTag = operation2.tags.some((t) => tags.includes(t));
1902
+ const hasMatchingTag = operation.tags.some((t) => tags.includes(t));
1968
1903
  if (!hasMatchingTag) {
1969
1904
  skipped.push({
1970
- sourceId: operation2.operationId,
1971
- reason: `No matching tags (has: ${operation2.tags.join(", ")})`
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(operation2.operationId)) {
1912
+ if (!include.includes(operation.operationId)) {
1978
1913
  skipped.push({
1979
- sourceId: operation2.operationId,
1914
+ sourceId: operation.operationId,
1980
1915
  reason: "Not in include list"
1981
1916
  });
1982
1917
  continue;
1983
1918
  }
1984
- } else if (exclude.includes(operation2.operationId)) {
1919
+ } else if (exclude.includes(operation.operationId)) {
1985
1920
  skipped.push({
1986
- sourceId: operation2.operationId,
1921
+ sourceId: operation.operationId,
1987
1922
  reason: "In exclude list"
1988
1923
  });
1989
1924
  continue;
1990
1925
  }
1991
- if (operation2.deprecated && importOptions.defaultStability !== "deprecated") {
1926
+ if (operation.deprecated && importOptions.defaultStability !== "deprecated") {
1992
1927
  skipped.push({
1993
- sourceId: operation2.operationId,
1928
+ sourceId: operation.operationId,
1994
1929
  reason: "Deprecated operation"
1995
1930
  });
1996
1931
  continue;
1997
1932
  }
1998
1933
  try {
1999
- const inputSchemas = buildInputSchemas(operation2);
1934
+ const inputSchemas = buildInputSchemas(operation);
2000
1935
  const schemaFormat = importOptions.schemaFormat || contractspecOptions.schemaFormat || "contractspec";
2001
- const inputModel = inputSchemas.body ? generateSchemaModelCode(inputSchemas.body, `${operation2.operationId}Input`, schemaFormat, contractspecOptions) : null;
2002
- const queryModel = inputSchemas.query ? generateSchemaModelCode(inputSchemas.query, `${operation2.operationId}Query`, schemaFormat, contractspecOptions) : null;
2003
- const paramsModel = inputSchemas.params ? generateSchemaModelCode(inputSchemas.params, `${operation2.operationId}Params`, schemaFormat, contractspecOptions) : null;
2004
- const headersModel = inputSchemas.headers ? generateSchemaModelCode(inputSchemas.headers, `${operation2.operationId}Headers`, schemaFormat, contractspecOptions) : null;
2005
- const outputSchema = getOutputSchema(operation2);
2006
- let outputModel = outputSchema ? generateSchemaModelCode(outputSchema, `${operation2.operationId}Output`, schemaFormat, contractspecOptions) : null;
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(operation2, contractspecOptions, importOptions, inputModel, outputModel, queryModel, paramsModel, headersModel);
2011
- const specName = toSpecKey(operation2.operationId, importOptions.prefix);
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: operation2.method.toUpperCase(),
2016
- path: operation2.path,
1950
+ method: operation.method.toUpperCase(),
1951
+ path: operation.path,
2017
1952
  params: {
2018
- path: operation2.pathParams.map((p) => p.name),
2019
- query: operation2.queryParams.map((p) => p.name),
2020
- header: operation2.headerParams.map((p) => p.name),
2021
- cookie: operation2.cookieParams.map((p) => p.name)
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: operation2.operationId,
2028
- operationId: operation2.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(operation2, contractspecOptions.conventions);
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: operation2.operationId,
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(operation2, options = {}, contractspecOptions) {
2116
- const inputSchemas = buildInputSchemas(operation2);
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, `${operation2.operationId}Input`, schemaFormat, contractspecOptions) : null;
2119
- const queryModel = inputSchemas.query ? generateSchemaModelCode(inputSchemas.query, `${operation2.operationId}Query`, schemaFormat, contractspecOptions) : null;
2120
- const paramsModel = inputSchemas.params ? generateSchemaModelCode(inputSchemas.params, `${operation2.operationId}Params`, schemaFormat, contractspecOptions) : null;
2121
- const headersModel = inputSchemas.headers ? generateSchemaModelCode(inputSchemas.headers, `${operation2.operationId}Headers`, schemaFormat, contractspecOptions) : null;
2122
- const outputSchema = getOutputSchema(operation2);
2123
- const outputModel = outputSchema ? generateSchemaModelCode(outputSchema, `${operation2.operationId}Output`, schemaFormat, contractspecOptions) : null;
2124
- return generateSpecCode(operation2, contractspecOptions, options, inputModel, outputModel, queryModel, paramsModel, headersModel);
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/differ.ts
2127
- function compareValues(path, oldValue, newValue, description) {
2128
- if (deepEqual(oldValue, newValue)) {
2129
- return null;
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
- let changeType = "modified";
2132
- if (oldValue === undefined || oldValue === null) {
2133
- changeType = "added";
2134
- } else if (newValue === undefined || newValue === null) {
2135
- changeType = "removed";
2136
- } else if (typeof oldValue !== typeof newValue) {
2137
- changeType = "type_changed";
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 diffObjects(path, oldObj, newObj, options) {
2148
- const changes = [];
2149
- if (!oldObj && !newObj)
2150
- return changes;
2151
- if (!oldObj) {
2152
- changes.push({
2153
- path,
2154
- type: "added",
2155
- newValue: newObj,
2156
- description: `Added ${path}`
2157
- });
2158
- return changes;
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
- if (!newObj) {
2161
- changes.push({
2162
- path,
2163
- type: "removed",
2164
- oldValue: oldObj,
2165
- description: `Removed ${path}`
2166
- });
2167
- return changes;
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
- const allKeys = new Set([...Object.keys(oldObj), ...Object.keys(newObj)]);
2170
- for (const key of allKeys) {
2171
- const keyPath = path ? `${path}.${key}` : key;
2172
- if (options.ignorePaths?.some((p) => keyPath.startsWith(p))) {
2173
- continue;
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
- const oldVal = oldObj[key];
2176
- const newVal = newObj[key];
2177
- if (typeof oldVal === "object" && typeof newVal === "object") {
2178
- changes.push(...diffObjects(keyPath, oldVal, newVal, options));
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
- const change = compareValues(keyPath, oldVal, newVal, `Changed ${keyPath}`);
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 changes;
2154
+ return result;
2187
2155
  }
2188
- function diffSpecVsOperation(spec, operation2, options = {}) {
2189
- const changes = [];
2190
- if (!options.ignoreDescriptions) {
2191
- const descChange = compareValues("meta.description", spec.meta.description, operation2.summary ?? operation2.description, "Description changed");
2192
- if (descChange)
2193
- changes.push(descChange);
2194
- }
2195
- if (!options.ignoreTags) {
2196
- const oldTags = [...spec.meta.tags ?? []].sort();
2197
- const newTags = [...operation2.tags].sort();
2198
- if (!deepEqual(oldTags, newTags)) {
2199
- changes.push({
2200
- path: "meta.tags",
2201
- type: "modified",
2202
- oldValue: oldTags,
2203
- newValue: newTags,
2204
- description: "Tags changed"
2205
- });
2206
- }
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
- if (!options.ignoreTransport) {
2209
- const specMethod = spec.transport?.rest?.method ?? (spec.meta.kind === "query" ? "GET" : "POST");
2210
- const opMethod = operation2.method.toUpperCase();
2211
- if (specMethod !== opMethod) {
2212
- changes.push({
2213
- path: "transport.rest.method",
2214
- type: "modified",
2215
- oldValue: specMethod,
2216
- newValue: opMethod,
2217
- description: "HTTP method changed"
2218
- });
2219
- }
2220
- const specPath = spec.transport?.rest?.path;
2221
- if (specPath && specPath !== operation2.path) {
2222
- changes.push({
2223
- path: "transport.rest.path",
2224
- type: "modified",
2225
- oldValue: specPath,
2226
- newValue: operation2.path,
2227
- description: "Path changed"
2228
- });
2229
- }
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
- const specDeprecated = spec.meta.stability === "deprecated";
2232
- if (specDeprecated !== operation2.deprecated) {
2233
- changes.push({
2234
- path: "meta.stability",
2235
- type: "modified",
2236
- oldValue: spec.meta.stability,
2237
- newValue: operation2.deprecated ? "deprecated" : "stable",
2238
- description: "Deprecation status changed"
2239
- });
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 changes;
2187
+ return "3.0";
2242
2188
  }
2243
- function diffSpecs(oldSpec, newSpec, options = {}) {
2244
- const changes = [];
2245
- const metaChanges = diffObjects("meta", oldSpec.meta, newSpec.meta, {
2246
- ...options,
2247
- ignorePaths: [
2248
- ...options.ignorePaths ?? [],
2249
- ...options.ignoreDescriptions ? ["meta.description", "meta.goal", "meta.context"] : [],
2250
- ...options.ignoreTags ? ["meta.tags"] : []
2251
- ]
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
- changes.push(...metaChanges);
2254
- if (!options.ignoreTransport) {
2255
- const transportChanges = diffObjects("transport", oldSpec.transport, newSpec.transport, options);
2256
- changes.push(...transportChanges);
2257
- }
2258
- const policyChanges = diffObjects("policy", oldSpec.policy, newSpec.policy, options);
2259
- changes.push(...policyChanges);
2260
- return changes;
2196
+ return method + pathParts.join("");
2261
2197
  }
2262
- function createSpecDiff(operationId, existing, incoming, options = {}) {
2263
- let changes = [];
2264
- let isEquivalent = false;
2265
- if (existing && incoming.operationSpec) {
2266
- changes = diffSpecs(existing, incoming.operationSpec, options);
2267
- isEquivalent = changes.length === 0;
2268
- } else if (existing && !incoming.operationSpec) {
2269
- changes = [
2270
- {
2271
- path: "",
2272
- type: "modified",
2273
- oldValue: existing,
2274
- newValue: incoming.code,
2275
- description: "Spec code imported from OpenAPI (runtime comparison not available)"
2276
- }
2277
- ];
2278
- } else {
2279
- changes = [
2280
- {
2281
- path: "",
2282
- type: "added",
2283
- newValue: incoming.operationSpec ?? incoming.code,
2284
- description: "New spec imported from OpenAPI"
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
- existing,
2291
- incoming,
2292
- changes,
2293
- isEquivalent
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
- function diffAll(existingSpecs, importedSpecs, options = {}) {
2297
- const diffs = [];
2298
- const matchedExisting = new Set;
2299
- for (const imported of importedSpecs) {
2300
- const operationId = imported.source.sourceId;
2301
- let existing;
2302
- for (const [key, spec] of existingSpecs) {
2303
- const specName = spec.meta.key;
2304
- if (key === operationId || specName.includes(operationId)) {
2305
- existing = spec;
2306
- matchedExisting.add(key);
2307
- break;
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
- for (const [key, spec] of existingSpecs) {
2313
- if (!matchedExisting.has(key)) {
2314
- diffs.push({
2315
- operationId: key,
2316
- existing: spec,
2317
- incoming: undefined,
2318
- changes: [
2319
- {
2320
- path: "",
2321
- type: "removed",
2322
- oldValue: spec,
2323
- description: "Spec no longer exists in OpenAPI source"
2324
- }
2325
- ],
2326
- isEquivalent: false
2327
- });
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
- return diffs;
2331
- }
2332
- function formatDiffChanges(changes) {
2333
- if (changes.length === 0) {
2334
- return "No changes detected";
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
- const lines = [];
2337
- for (const change of changes) {
2338
- const prefix = {
2339
- added: "+",
2340
- removed: "-",
2341
- modified: "~",
2342
- type_changed: "!",
2343
- required_changed: "?"
2344
- }[change.type];
2345
- lines.push(`${prefix} ${change.path}: ${change.description}`);
2346
- if (change.type === "modified" || change.type === "type_changed") {
2347
- lines.push(` old: ${JSON.stringify(change.oldValue)}`);
2348
- lines.push(` new: ${JSON.stringify(change.newValue)}`);
2349
- } else if (change.type === "added") {
2350
- lines.push(` value: ${JSON.stringify(change.newValue)}`);
2351
- } else if (change.type === "removed") {
2352
- lines.push(` was: ${JSON.stringify(change.oldValue)}`);
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
- return lines.join(`
2356
- `);
2359
+ const doc = parseOpenApiString(content, format);
2360
+ return parseOpenApiDocument(doc, options);
2357
2361
  }
2358
2362
  export {
2359
2363
  toSchemaName,