@contractual/differs.openapi 0.1.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (C) 2025 Omer Morad
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,24 @@
1
+ /**
2
+ * OpenAPI Differ
3
+ *
4
+ * Compares two OpenAPI specifications and detects breaking changes.
5
+ * Supports OpenAPI 3.0 and 3.1.
6
+ */
7
+ import type { DiffResult } from '@contractual/types';
8
+ /**
9
+ * Diff two OpenAPI specification files
10
+ *
11
+ * @param oldSpecPath - Path to the old/base OpenAPI spec file
12
+ * @param newSpecPath - Path to the new/changed OpenAPI spec file
13
+ * @returns DiffResult with classified changes and suggested bump
14
+ */
15
+ export declare function diffOpenApi(oldSpecPath: string, newSpecPath: string): Promise<DiffResult>;
16
+ /**
17
+ * Diff two resolved OpenAPI spec objects directly
18
+ *
19
+ * @param oldSpec - The old/base OpenAPI spec object (fully resolved)
20
+ * @param newSpec - The new/changed OpenAPI spec object (fully resolved)
21
+ * @returns DiffResult with classified changes and suggested bump
22
+ */
23
+ export declare function diffOpenApiObjects(oldSpec: Record<string, unknown>, newSpec: Record<string, unknown>): DiffResult;
24
+ //# sourceMappingURL=differ.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"differ.d.ts","sourceRoot":"","sources":["../src/differ.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAkB,UAAU,EAAa,MAAM,oBAAoB,CAAC;AAKhF;;;;;;GAMG;AACH,wBAAsB,WAAW,CAAC,WAAW,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAK/F;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/B,UAAU,CAGZ"}
package/dist/differ.js ADDED
@@ -0,0 +1,47 @@
1
+ /**
2
+ * OpenAPI Differ
3
+ *
4
+ * Compares two OpenAPI specifications and detects breaking changes.
5
+ * Supports OpenAPI 3.0 and 3.1.
6
+ */
7
+ import { assembleResult, classify, classifyPropertyAdded } from '@contractual/differs.core';
8
+ import { resolveOpenApiSpec } from './resolver.js';
9
+ import { diffStructural } from './structural.js';
10
+ /**
11
+ * Diff two OpenAPI specification files
12
+ *
13
+ * @param oldSpecPath - Path to the old/base OpenAPI spec file
14
+ * @param newSpecPath - Path to the new/changed OpenAPI spec file
15
+ * @returns DiffResult with classified changes and suggested bump
16
+ */
17
+ export async function diffOpenApi(oldSpecPath, newSpecPath) {
18
+ const oldSpec = await resolveOpenApiSpec(oldSpecPath);
19
+ const newSpec = await resolveOpenApiSpec(newSpecPath);
20
+ return diffOpenApiObjects(oldSpec, newSpec);
21
+ }
22
+ /**
23
+ * Diff two resolved OpenAPI spec objects directly
24
+ *
25
+ * @param oldSpec - The old/base OpenAPI spec object (fully resolved)
26
+ * @param newSpec - The new/changed OpenAPI spec object (fully resolved)
27
+ * @returns DiffResult with classified changes and suggested bump
28
+ */
29
+ export function diffOpenApiObjects(oldSpec, newSpec) {
30
+ const rawChanges = diffStructural(oldSpec, newSpec);
31
+ return assembleResult(rawChanges, newSpec, { classifyFn: classifyOpenApiChange });
32
+ }
33
+ function classifyOpenApiChange(change, newSpec) {
34
+ if (change.schemaDirection === 'output') {
35
+ if (change.type === 'required-removed') {
36
+ return 'breaking';
37
+ }
38
+ if (change.type === 'required-added') {
39
+ return 'non-breaking';
40
+ }
41
+ }
42
+ if (change.type === 'property-added') {
43
+ return classifyPropertyAdded(change, newSpec);
44
+ }
45
+ return classify(change);
46
+ }
47
+ //# sourceMappingURL=differ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"differ.js","sourceRoot":"","sources":["../src/differ.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAC5F,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEjD;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,WAAmB,EAAE,WAAmB;IACxE,MAAM,OAAO,GAAG,MAAM,kBAAkB,CAAC,WAAW,CAAC,CAAC;IACtD,MAAM,OAAO,GAAG,MAAM,kBAAkB,CAAC,WAAW,CAAC,CAAC;IAEtD,OAAO,kBAAkB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;AAC9C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAChC,OAAgC,EAChC,OAAgC;IAEhC,MAAM,UAAU,GAAG,cAAc,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IACpD,OAAO,cAAc,CAAC,UAAU,EAAE,OAAO,EAAE,EAAE,UAAU,EAAE,qBAAqB,EAAE,CAAC,CAAC;AACpF,CAAC;AAED,SAAS,qBAAqB,CAAC,MAAiB,EAAE,OAAgB;IAChE,IAAI,MAAM,CAAC,eAAe,KAAK,QAAQ,EAAE,CAAC;QACxC,IAAI,MAAM,CAAC,IAAI,KAAK,kBAAkB,EAAE,CAAC;YACvC,OAAO,UAAU,CAAC;QACpB,CAAC;QAED,IAAI,MAAM,CAAC,IAAI,KAAK,gBAAgB,EAAE,CAAC;YACrC,OAAO,cAAc,CAAC;QACxB,CAAC;IACH,CAAC;IAED,IAAI,MAAM,CAAC,IAAI,KAAK,gBAAgB,EAAE,CAAC;QACrC,OAAO,qBAAqB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChD,CAAC;IAED,OAAO,QAAQ,CAAC,MAAM,CAAC,CAAC;AAC1B,CAAC"}
@@ -0,0 +1,4 @@
1
+ export { diffOpenApi, diffOpenApiObjects } from './differ.js';
2
+ export { resolveOpenApiSpec } from './resolver.js';
3
+ export { diffStructural } from './structural.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAC9D,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { diffOpenApi, diffOpenApiObjects } from './differ.js';
2
+ export { resolveOpenApiSpec } from './resolver.js';
3
+ export { diffStructural } from './structural.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAC9D,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * OpenAPI spec resolver using @redocly/openapi-core
3
+ *
4
+ * Handles both OpenAPI 3.0 and 3.1 specifications.
5
+ * Uses Redocly to bundle external refs into a single document,
6
+ * then uses the core ref-resolver to dereference internal $refs
7
+ * (which handles circular references safely).
8
+ */
9
+ /**
10
+ * Resolve an OpenAPI spec file to a fully dereferenced object
11
+ *
12
+ * @param specPath - Absolute or relative path to the OpenAPI spec file
13
+ * @returns The fully resolved OpenAPI document as a plain object
14
+ */
15
+ export declare function resolveOpenApiSpec(specPath: string): Promise<Record<string, unknown>>;
16
+ //# sourceMappingURL=resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolver.d.ts","sourceRoot":"","sources":["../src/resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH;;;;;GAKG;AACH,wBAAsB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAwB3F"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * OpenAPI spec resolver using @redocly/openapi-core
3
+ *
4
+ * Handles both OpenAPI 3.0 and 3.1 specifications.
5
+ * Uses Redocly to bundle external refs into a single document,
6
+ * then uses the core ref-resolver to dereference internal $refs
7
+ * (which handles circular references safely).
8
+ */
9
+ import { resolveRefs } from '@contractual/differs.core';
10
+ /**
11
+ * Resolve an OpenAPI spec file to a fully dereferenced object
12
+ *
13
+ * @param specPath - Absolute or relative path to the OpenAPI spec file
14
+ * @returns The fully resolved OpenAPI document as a plain object
15
+ */
16
+ export async function resolveOpenApiSpec(specPath) {
17
+ let bundle;
18
+ let loadConfig;
19
+ try {
20
+ const redocly = await import('@redocly/openapi-core');
21
+ bundle = redocly.bundle;
22
+ loadConfig = redocly.loadConfig;
23
+ }
24
+ catch {
25
+ throw new Error('OpenAPI diffing requires @redocly/openapi-core. ' +
26
+ 'Install it with: npm install @redocly/openapi-core');
27
+ }
28
+ // Bundle only (no dereference) — resolves external refs into one document
29
+ // but keeps internal $refs intact to avoid circular object references
30
+ const config = await loadConfig();
31
+ const result = await bundle({ ref: specPath, config });
32
+ const bundled = result.bundle.parsed;
33
+ // Use our own ref-resolver which handles circular refs safely
34
+ const resolved = resolveRefs(bundled);
35
+ return resolved.schema;
36
+ }
37
+ //# sourceMappingURL=resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolver.js","sourceRoot":"","sources":["../src/resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAExD;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,QAAgB;IACvD,IAAI,MAAqD,CAAC;IAC1D,IAAI,UAA6D,CAAC;IAElE,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,uBAAuB,CAAC,CAAC;QACtD,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;QACxB,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;IAClC,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CACb,kDAAkD;YAChD,oDAAoD,CACvD,CAAC;IACJ,CAAC;IAED,0EAA0E;IAC1E,sEAAsE;IACtE,MAAM,MAAM,GAAG,MAAM,UAAU,EAAE,CAAC;IAClC,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;IACvD,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,MAAiC,CAAC;IAEhE,8DAA8D;IAC9D,MAAM,QAAQ,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC;IACtC,OAAO,QAAQ,CAAC,MAAiC,CAAC;AACpD,CAAC"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * OpenAPI structural differ
3
+ *
4
+ * Compares the structural layer of two OpenAPI specs:
5
+ * paths, operations, parameters, request bodies, responses.
6
+ * Schema-level diffing is delegated to @contractual/differs.core walker.
7
+ */
8
+ import type { RawChange } from '@contractual/types';
9
+ /**
10
+ * Diff the structural layer of two resolved OpenAPI specs
11
+ */
12
+ export declare function diffStructural(oldSpec: Record<string, unknown>, newSpec: Record<string, unknown>): RawChange[];
13
+ //# sourceMappingURL=structural.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"structural.d.ts","sourceRoot":"","sources":["../src/structural.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,SAAS,EAA+B,MAAM,oBAAoB,CAAC;AAYjF;;GAEG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/B,SAAS,EAAE,CAyDb"}
@@ -0,0 +1,266 @@
1
+ /**
2
+ * OpenAPI structural differ
3
+ *
4
+ * Compares the structural layer of two OpenAPI specs:
5
+ * paths, operations, parameters, request bodies, responses.
6
+ * Schema-level diffing is delegated to @contractual/differs.core walker.
7
+ */
8
+ import { walk, escapeJsonPointer } from '@contractual/differs.core';
9
+ const HTTP_METHODS = ['get', 'put', 'post', 'delete', 'patch', 'options', 'head', 'trace'];
10
+ function withSchemaDirection(changes, schemaDirection) {
11
+ return changes.map((change) => ({ ...change, schemaDirection }));
12
+ }
13
+ /**
14
+ * Diff the structural layer of two resolved OpenAPI specs
15
+ */
16
+ export function diffStructural(oldSpec, newSpec) {
17
+ const changes = [];
18
+ // Diff top-level info metadata
19
+ const oldInfo = (oldSpec.info ?? {});
20
+ const newInfo = (newSpec.info ?? {});
21
+ if (oldInfo.description !== newInfo.description) {
22
+ changes.push({
23
+ path: '/info/description',
24
+ type: 'description-changed',
25
+ oldValue: oldInfo.description,
26
+ newValue: newInfo.description,
27
+ });
28
+ }
29
+ if (oldInfo.title !== newInfo.title) {
30
+ changes.push({
31
+ path: '/info/title',
32
+ type: 'title-changed',
33
+ oldValue: oldInfo.title,
34
+ newValue: newInfo.title,
35
+ });
36
+ }
37
+ const oldPaths = (oldSpec.paths ?? {});
38
+ const newPaths = (newSpec.paths ?? {});
39
+ // Diff paths
40
+ for (const path of Object.keys(oldPaths)) {
41
+ const escapedPath = escapeJsonPointer(path);
42
+ if (!(path in newPaths)) {
43
+ changes.push({
44
+ path: `/paths/${escapedPath}`,
45
+ type: 'path-removed',
46
+ oldValue: path,
47
+ });
48
+ continue;
49
+ }
50
+ // Diff operations within matching paths
51
+ changes.push(...diffOperations(oldPaths[path], newPaths[path], path));
52
+ }
53
+ for (const path of Object.keys(newPaths)) {
54
+ if (!(path in oldPaths)) {
55
+ const escapedPath = escapeJsonPointer(path);
56
+ changes.push({
57
+ path: `/paths/${escapedPath}`,
58
+ type: 'path-added',
59
+ newValue: path,
60
+ });
61
+ }
62
+ }
63
+ return changes;
64
+ }
65
+ /**
66
+ * Diff operations (HTTP methods) within a path
67
+ */
68
+ function diffOperations(oldPathItem, newPathItem, apiPath) {
69
+ const changes = [];
70
+ const escapedPath = escapeJsonPointer(apiPath);
71
+ for (const method of HTTP_METHODS) {
72
+ const oldOp = oldPathItem[method];
73
+ const newOp = newPathItem[method];
74
+ if (oldOp && !newOp) {
75
+ changes.push({
76
+ path: `/paths/${escapedPath}/${method}`,
77
+ type: 'operation-removed',
78
+ oldValue: `${method.toUpperCase()} ${apiPath}`,
79
+ });
80
+ }
81
+ else if (!oldOp && newOp) {
82
+ changes.push({
83
+ path: `/paths/${escapedPath}/${method}`,
84
+ type: 'operation-added',
85
+ newValue: `${method.toUpperCase()} ${apiPath}`,
86
+ });
87
+ }
88
+ else if (oldOp && newOp) {
89
+ // Both exist — diff metadata, parameters, request body, responses
90
+ const basePath = `/paths/${escapedPath}/${method}`;
91
+ changes.push(...diffOperationMetadata(oldOp, newOp, basePath));
92
+ changes.push(...diffParameters(oldOp, newOp, basePath));
93
+ changes.push(...diffRequestBody(oldOp, newOp, basePath));
94
+ changes.push(...diffResponses(oldOp, newOp, basePath));
95
+ }
96
+ }
97
+ return changes;
98
+ }
99
+ /**
100
+ * Diff operation-level metadata (description, summary, tags, deprecated)
101
+ */
102
+ function diffOperationMetadata(oldOp, newOp, basePath) {
103
+ const changes = [];
104
+ // Description changed (patch)
105
+ if (oldOp.description !== newOp.description) {
106
+ changes.push({
107
+ path: `${basePath}/description`,
108
+ type: 'description-changed',
109
+ oldValue: oldOp.description,
110
+ newValue: newOp.description,
111
+ });
112
+ }
113
+ // Summary changed (patch — treated as title)
114
+ if (oldOp.summary !== newOp.summary) {
115
+ changes.push({
116
+ path: `${basePath}/summary`,
117
+ type: 'title-changed',
118
+ oldValue: oldOp.summary,
119
+ newValue: newOp.summary,
120
+ });
121
+ }
122
+ // Deprecated changed (patch)
123
+ if (oldOp.deprecated !== newOp.deprecated) {
124
+ changes.push({
125
+ path: `${basePath}/deprecated`,
126
+ type: 'deprecated-changed',
127
+ oldValue: oldOp.deprecated,
128
+ newValue: newOp.deprecated,
129
+ });
130
+ }
131
+ return changes;
132
+ }
133
+ /**
134
+ * Diff parameters between two operations
135
+ */
136
+ function diffParameters(oldOp, newOp, basePath) {
137
+ const changes = [];
138
+ const oldParams = (oldOp.parameters ?? []);
139
+ const newParams = (newOp.parameters ?? []);
140
+ // Index by "in+name" for matching
141
+ const oldByKey = new Map(oldParams.map((p) => [`${p.in}:${p.name}`, p]));
142
+ const newByKey = new Map(newParams.map((p) => [`${p.in}:${p.name}`, p]));
143
+ for (const [key, oldParam] of oldByKey) {
144
+ if (!newByKey.has(key)) {
145
+ changes.push({
146
+ path: `${basePath}/parameters/${oldParam.name}`,
147
+ type: 'parameter-removed',
148
+ oldValue: key,
149
+ });
150
+ }
151
+ else {
152
+ const newParam = newByKey.get(key);
153
+ // Check required change
154
+ if (oldParam.required !== newParam.required) {
155
+ changes.push({
156
+ path: `${basePath}/parameters/${oldParam.name}/required`,
157
+ type: 'parameter-required-changed',
158
+ oldValue: oldParam.required,
159
+ newValue: newParam.required,
160
+ });
161
+ }
162
+ // Diff parameter schemas using core walker
163
+ if (oldParam.schema && newParam.schema) {
164
+ const schemaChanges = walk(oldParam.schema, newParam.schema, `${basePath}/parameters/${oldParam.name}/schema`);
165
+ changes.push(...withSchemaDirection(schemaChanges, 'input'));
166
+ }
167
+ }
168
+ }
169
+ for (const [key, newParam] of newByKey) {
170
+ if (!oldByKey.has(key)) {
171
+ // Required parameter added = breaking, optional = non-breaking
172
+ // Store required flag in newValue so classifiers can check
173
+ changes.push({
174
+ path: `${basePath}/parameters/${newParam.name}`,
175
+ type: newParam.required
176
+ ? 'parameter-required-added'
177
+ : 'parameter-added',
178
+ newValue: key,
179
+ });
180
+ }
181
+ }
182
+ return changes;
183
+ }
184
+ /**
185
+ * Extract the primary schema from a content map (prefers application/json)
186
+ */
187
+ function extractSchema(content) {
188
+ if (!content)
189
+ return undefined;
190
+ // Prefer application/json, then first available
191
+ const jsonContent = content['application/json'];
192
+ if (jsonContent?.schema)
193
+ return jsonContent.schema;
194
+ for (const mediaType of Object.values(content)) {
195
+ const mt = mediaType;
196
+ if (mt?.schema)
197
+ return mt.schema;
198
+ }
199
+ return undefined;
200
+ }
201
+ /**
202
+ * Diff request bodies between two operations
203
+ */
204
+ function diffRequestBody(oldOp, newOp, basePath) {
205
+ const changes = [];
206
+ const oldBody = oldOp.requestBody;
207
+ const newBody = newOp.requestBody;
208
+ if (oldBody && !newBody) {
209
+ changes.push({
210
+ path: `${basePath}/requestBody`,
211
+ type: 'request-body-removed',
212
+ });
213
+ }
214
+ else if (!oldBody && newBody) {
215
+ changes.push({
216
+ path: `${basePath}/requestBody`,
217
+ type: 'request-body-added',
218
+ });
219
+ }
220
+ else if (oldBody && newBody) {
221
+ const oldSchema = extractSchema(oldBody.content);
222
+ const newSchema = extractSchema(newBody.content);
223
+ if (oldSchema && newSchema) {
224
+ const schemaChanges = walk(oldSchema, newSchema, `${basePath}/requestBody/content/schema`);
225
+ changes.push(...withSchemaDirection(schemaChanges, 'input'));
226
+ }
227
+ }
228
+ return changes;
229
+ }
230
+ /**
231
+ * Diff responses between two operations
232
+ */
233
+ function diffResponses(oldOp, newOp, basePath) {
234
+ const changes = [];
235
+ const oldResponses = (oldOp.responses ?? {});
236
+ const newResponses = (newOp.responses ?? {});
237
+ for (const statusCode of Object.keys(oldResponses)) {
238
+ if (!(statusCode in newResponses)) {
239
+ changes.push({
240
+ path: `${basePath}/responses/${statusCode}`,
241
+ type: 'response-removed',
242
+ oldValue: statusCode,
243
+ });
244
+ }
245
+ else {
246
+ // Diff response schemas
247
+ const oldSchema = extractSchema(oldResponses[statusCode].content);
248
+ const newSchema = extractSchema(newResponses[statusCode].content);
249
+ if (oldSchema && newSchema) {
250
+ const schemaChanges = walk(oldSchema, newSchema, `${basePath}/responses/${statusCode}/content/schema`);
251
+ changes.push(...withSchemaDirection(schemaChanges, 'output'));
252
+ }
253
+ }
254
+ }
255
+ for (const statusCode of Object.keys(newResponses)) {
256
+ if (!(statusCode in oldResponses)) {
257
+ changes.push({
258
+ path: `${basePath}/responses/${statusCode}`,
259
+ type: 'response-added',
260
+ newValue: statusCode,
261
+ });
262
+ }
263
+ }
264
+ return changes;
265
+ }
266
+ //# sourceMappingURL=structural.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"structural.js","sourceRoot":"","sources":["../src/structural.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@contractual/differs.openapi",
3
+ "private": false,
4
+ "version": "0.1.0-beta.2",
5
+ "description": "OpenAPI 3.0/3.1 breaking change detection for Contractual",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js"
14
+ }
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/codotech/contractual.git",
19
+ "directory": "packages/differs.openapi"
20
+ },
21
+ "engines": {
22
+ "node": ">=22.12.0"
23
+ },
24
+ "scripts": {
25
+ "prebuild": "rimraf dist",
26
+ "build": "tsc -p tsconfig.build.json"
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "README.md"
31
+ ],
32
+ "dependencies": {
33
+ "@contractual/differs.core": "0.1.0-beta.1",
34
+ "@contractual/types": "0.1.0-beta.1",
35
+ "@redocly/openapi-core": "^2.72.1"
36
+ },
37
+ "devDependencies": {
38
+ "rimraf": "^6.1.3",
39
+ "vitest": "^4.1.12"
40
+ },
41
+ "publishConfig": {
42
+ "access": "public",
43
+ "provenance": true
44
+ },
45
+ "gitHead": "9d335ac31283134a3b9cae1c55a7d17c7b3e0c0a"
46
+ }