specshield 3.2.7 → 3.2.8
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "specshield",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.8",
|
|
4
4
|
"description": "CLI for OpenAPI breaking change detection and bi-directional contract verification — with can-i-deploy gating, GitHub PR checks, and a first-run setup wizard.",
|
|
5
5
|
"main": "src/cli.js",
|
|
6
6
|
"bin": {
|
|
@@ -21,6 +21,10 @@ const BREAKING_TYPES = new Set([
|
|
|
21
21
|
'REQUEST_TYPE_CHANGED',
|
|
22
22
|
'RESPONSE_TYPE_CHANGED',
|
|
23
23
|
'SCHEMA_REMOVED',
|
|
24
|
+
// Dropping a union variant, or changing the discriminator, breaks consumers
|
|
25
|
+
// that relied on the removed shape / the old discriminator.
|
|
26
|
+
'SCHEMA_VARIANT_REMOVED',
|
|
27
|
+
'SCHEMA_DISCRIMINATOR_CHANGED',
|
|
24
28
|
// Constraint tightening: previously-valid values become invalid → breaking.
|
|
25
29
|
'CONSTRAINT_TIGHTENED',
|
|
26
30
|
// Pattern changes are treated as breaking (semantic safety: we can't
|
|
@@ -36,6 +40,8 @@ const ADDITION_TYPES = new Set([
|
|
|
36
40
|
'RESPONSE_FIELD_ADDED',
|
|
37
41
|
'RESPONSE_ADDED',
|
|
38
42
|
'SCHEMA_ADDED',
|
|
43
|
+
// Adding a union variant is backwards-compatible.
|
|
44
|
+
'SCHEMA_VARIANT_ADDED',
|
|
39
45
|
]);
|
|
40
46
|
|
|
41
47
|
const MODIFICATION_TYPES = new Set([
|
package/src/core/diffEngine.js
CHANGED
|
@@ -323,6 +323,70 @@ function diffSchemaNode(path, method, fieldPrefix, base, target, diffs, isReques
|
|
|
323
323
|
});
|
|
324
324
|
}
|
|
325
325
|
}
|
|
326
|
+
|
|
327
|
+
// additionalProperties (free-form map): compare the value schema so a
|
|
328
|
+
// breaking change inside map values is caught. ".<*>" denotes "any key".
|
|
329
|
+
if (base.additionalProperties || target.additionalProperties) {
|
|
330
|
+
const apField = `${fieldPrefix}.<*>`;
|
|
331
|
+
if (base.additionalProperties && target.additionalProperties) {
|
|
332
|
+
diffSchemaNode(path, method, apField, base.additionalProperties, target.additionalProperties, diffs, isRequest);
|
|
333
|
+
} else if (base.additionalProperties && !target.additionalProperties) {
|
|
334
|
+
diffs.push({
|
|
335
|
+
type: isRequest ? 'REQUEST_FIELD_REMOVED' : 'RESPONSE_FIELD_REMOVED',
|
|
336
|
+
path,
|
|
337
|
+
method,
|
|
338
|
+
field: apField,
|
|
339
|
+
description: `Map value schema (additionalProperties) removed from "${fieldPrefix}" in ${method.toUpperCase()} ${path}`,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Discriminator rename/removal — breaks consumers keyed on the old name.
|
|
345
|
+
if (base.discriminator && base.discriminator !== target.discriminator) {
|
|
346
|
+
diffs.push({
|
|
347
|
+
type: 'SCHEMA_DISCRIMINATOR_CHANGED',
|
|
348
|
+
path,
|
|
349
|
+
method,
|
|
350
|
+
field: `${fieldPrefix}.discriminator`,
|
|
351
|
+
oldValue: base.discriminator,
|
|
352
|
+
newValue: target.discriminator || null,
|
|
353
|
+
description: `Discriminator at "${fieldPrefix}" changed from "${base.discriminator}" to "${target.discriminator || '(none)'}" in ${method.toUpperCase()} ${path}`,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// oneOf / anyOf variant comparison (positional; field-level changes inside a
|
|
358
|
+
// matched variant are caught by recursion). Mirrors the backend engine.
|
|
359
|
+
diffVariants(path, method, `${fieldPrefix}.oneOf`, base.oneOf, target.oneOf, diffs, isRequest);
|
|
360
|
+
diffVariants(path, method, `${fieldPrefix}.anyOf`, base.anyOf, target.anyOf, diffs, isRequest);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function diffVariants(path, method, fieldPrefix, baseVariants, targetVariants, diffs, isRequest) {
|
|
364
|
+
const b = Array.isArray(baseVariants) ? baseVariants : [];
|
|
365
|
+
const t = Array.isArray(targetVariants) ? targetVariants : [];
|
|
366
|
+
if (b.length === 0 && t.length === 0) return;
|
|
367
|
+
|
|
368
|
+
const matched = Math.min(b.length, t.length);
|
|
369
|
+
for (let i = 0; i < matched; i++) {
|
|
370
|
+
diffSchemaNode(path, method, `${fieldPrefix}[${i}]`, b[i], t[i], diffs, isRequest);
|
|
371
|
+
}
|
|
372
|
+
for (let i = t.length; i < b.length; i++) {
|
|
373
|
+
diffs.push({
|
|
374
|
+
type: 'SCHEMA_VARIANT_REMOVED',
|
|
375
|
+
path,
|
|
376
|
+
method,
|
|
377
|
+
field: `${fieldPrefix}[${i}]`,
|
|
378
|
+
description: `A schema variant was removed from "${fieldPrefix}" in ${method.toUpperCase()} ${path}`,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
for (let i = b.length; i < t.length; i++) {
|
|
382
|
+
diffs.push({
|
|
383
|
+
type: 'SCHEMA_VARIANT_ADDED',
|
|
384
|
+
path,
|
|
385
|
+
method,
|
|
386
|
+
field: `${fieldPrefix}[${i}]`,
|
|
387
|
+
description: `A schema variant was added to "${fieldPrefix}" in ${method.toUpperCase()} ${path}`,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
326
390
|
}
|
|
327
391
|
|
|
328
392
|
// ─── Constraints (min/max, length, pattern) ─────────────────────────────────
|
|
@@ -102,20 +102,30 @@ function resolveSchema(schema, schemas, depth = 0) {
|
|
|
102
102
|
return resolveSchema(resolved, schemas, depth + 1);
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
//
|
|
105
|
+
// allOf = composition → merge into one combined object.
|
|
106
106
|
if (schema.allOf) {
|
|
107
107
|
return mergeSchemas(schema.allOf, schemas, depth);
|
|
108
108
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
// oneOf / anyOf are NOT merged — they're kept as separate variants below so
|
|
110
|
+
// the diff engine can compare variants individually (added/removed variants
|
|
111
|
+
// + per-variant field changes). This matches the backend's variant-aware
|
|
112
|
+
// model; merging would lose variant identity and over-mark `required`.
|
|
113
|
+
|
|
114
|
+
// OpenAPI 3.1 expresses nullability as a type array: `type: [x, "null"]`.
|
|
115
|
+
// Normalize that to a single base type + nullable flag so it compares cleanly
|
|
116
|
+
// against 3.0's `nullable: true` form and doesn't surface a bogus type change.
|
|
117
|
+
let resolvedType = schema.type;
|
|
118
|
+
let resolvedNullable = Boolean(schema.nullable);
|
|
119
|
+
if (Array.isArray(resolvedType)) {
|
|
120
|
+
if (resolvedType.includes('null')) resolvedNullable = true;
|
|
121
|
+
resolvedType = resolvedType.find((t) => t !== 'null') || 'object';
|
|
112
122
|
}
|
|
113
123
|
|
|
114
124
|
const node = {
|
|
115
|
-
type:
|
|
125
|
+
type: resolvedType || 'object',
|
|
116
126
|
format: schema.format || null,
|
|
117
127
|
enum: schema.enum || null,
|
|
118
|
-
nullable:
|
|
128
|
+
nullable: resolvedNullable,
|
|
119
129
|
required: Array.isArray(schema.required) ? schema.required : [],
|
|
120
130
|
properties: {},
|
|
121
131
|
items: null,
|
|
@@ -143,6 +153,23 @@ function resolveSchema(schema, schemas, depth = 0) {
|
|
|
143
153
|
node.items = resolveSchema(schema.items, schemas, depth + 1);
|
|
144
154
|
}
|
|
145
155
|
|
|
156
|
+
// additionalProperties as a schema = a free-form map/dictionary value type
|
|
157
|
+
// (e.g. Map<String, Metric>). Boolean true/false is not a value schema.
|
|
158
|
+
if (schema.additionalProperties && typeof schema.additionalProperties === 'object') {
|
|
159
|
+
node.additionalProperties = resolveSchema(schema.additionalProperties, schemas, depth + 1);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// oneOf / anyOf variants kept separate (variant-aware model).
|
|
163
|
+
if (Array.isArray(schema.oneOf)) {
|
|
164
|
+
node.oneOf = schema.oneOf.map((s) => resolveSchema(s, schemas, depth + 1));
|
|
165
|
+
}
|
|
166
|
+
if (Array.isArray(schema.anyOf)) {
|
|
167
|
+
node.anyOf = schema.anyOf.map((s) => resolveSchema(s, schemas, depth + 1));
|
|
168
|
+
}
|
|
169
|
+
if (schema.discriminator && schema.discriminator.propertyName) {
|
|
170
|
+
node.discriminator = schema.discriminator.propertyName;
|
|
171
|
+
}
|
|
172
|
+
|
|
146
173
|
return node;
|
|
147
174
|
}
|
|
148
175
|
|