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