@squiz/dx-json-schema-lib 1.87.0 → 1.89.0
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/CHANGELOG.md +14 -0
- package/lib/browser.d.ts +21 -0
- package/lib/browser.js +61 -0
- package/lib/browser.js.map +1 -0
- package/lib/formatted-text/v1/resolveFormattedTextNodes.d.ts +5 -2
- package/lib/formatted-text/v1/resolveFormattedTextNodes.js +12 -6
- package/lib/formatted-text/v1/resolveFormattedTextNodes.js.map +1 -1
- package/lib/formatted-text/v1/resolveFormattedTextNodes.spec.js +58 -0
- package/lib/formatted-text/v1/resolveFormattedTextNodes.spec.js.map +1 -1
- package/lib/index.d.ts +1 -15
- package/lib/index.js +4 -38
- package/lib/index.js.map +1 -1
- package/lib/primitiveTypes/FormattedText.js +36 -3
- package/lib/primitiveTypes/FormattedText.js.map +1 -1
- package/lib/utils/jsonBreakDetector/checks/findArrayConstraintNarrowing.d.ts +6 -0
- package/lib/utils/jsonBreakDetector/checks/findArrayConstraintNarrowing.js +93 -0
- package/lib/utils/jsonBreakDetector/checks/findArrayConstraintNarrowing.js.map +1 -0
- package/lib/utils/jsonBreakDetector/checks/findConstChanges.d.ts +6 -0
- package/lib/utils/jsonBreakDetector/checks/findConstChanges.js +75 -0
- package/lib/utils/jsonBreakDetector/checks/findConstChanges.js.map +1 -0
- package/lib/utils/jsonBreakDetector/checks/findEnumNarrowing.d.ts +6 -0
- package/lib/utils/jsonBreakDetector/checks/findEnumNarrowing.js +90 -0
- package/lib/utils/jsonBreakDetector/checks/findEnumNarrowing.js.map +1 -0
- package/lib/utils/jsonBreakDetector/checks/findFieldTypeChanges.d.ts +8 -0
- package/lib/utils/jsonBreakDetector/checks/findFieldTypeChanges.js +86 -0
- package/lib/utils/jsonBreakDetector/checks/findFieldTypeChanges.js.map +1 -0
- package/lib/utils/jsonBreakDetector/checks/findNewRequiredFields.d.ts +6 -0
- package/lib/utils/jsonBreakDetector/checks/findNewRequiredFields.js +68 -0
- package/lib/utils/jsonBreakDetector/checks/findNewRequiredFields.js.map +1 -0
- package/lib/utils/jsonBreakDetector/checks/findNumericConstraintNarrowing.d.ts +6 -0
- package/lib/utils/jsonBreakDetector/checks/findNumericConstraintNarrowing.js +179 -0
- package/lib/utils/jsonBreakDetector/checks/findNumericConstraintNarrowing.js.map +1 -0
- package/lib/utils/jsonBreakDetector/checks/findStringConstraintNarrowing.d.ts +6 -0
- package/lib/utils/jsonBreakDetector/checks/findStringConstraintNarrowing.js +106 -0
- package/lib/utils/jsonBreakDetector/checks/findStringConstraintNarrowing.js.map +1 -0
- package/lib/utils/jsonBreakDetector/compareSubschemas.d.ts +9 -0
- package/lib/utils/jsonBreakDetector/compareSubschemas.js +49 -0
- package/lib/utils/jsonBreakDetector/compareSubschemas.js.map +1 -0
- package/lib/utils/jsonBreakDetector/dereference/dereference.d.ts +6 -0
- package/lib/utils/jsonBreakDetector/dereference/dereference.js +52 -0
- package/lib/utils/jsonBreakDetector/dereference/dereference.js.map +1 -0
- package/lib/utils/jsonBreakDetector/dereference/dereference.spec.d.ts +1 -0
- package/lib/utils/jsonBreakDetector/dereference/dereference.spec.js +225 -0
- package/lib/utils/jsonBreakDetector/dereference/dereference.spec.js.map +1 -0
- package/lib/utils/jsonBreakDetector/jsonBreakDetector.d.ts +8 -0
- package/lib/utils/jsonBreakDetector/jsonBreakDetector.js +35 -0
- package/lib/utils/jsonBreakDetector/jsonBreakDetector.js.map +1 -0
- package/lib/utils/jsonBreakDetector/jsonBreakDetector.spec.d.ts +1 -0
- package/lib/utils/jsonBreakDetector/jsonBreakDetector.spec.js +2761 -0
- package/lib/utils/jsonBreakDetector/jsonBreakDetector.spec.js.map +1 -0
- package/lib/utils/jsonBreakDetector/types.d.ts +25 -0
- package/lib/utils/jsonBreakDetector/types.js +28 -0
- package/lib/utils/jsonBreakDetector/types.js.map +1 -0
- package/package.json +10 -1
- package/src/browser.ts +27 -0
- package/src/formatted-text/v1/resolveFormattedTextNodes.spec.ts +70 -0
- package/src/formatted-text/v1/resolveFormattedTextNodes.ts +19 -4
- package/src/index.ts +4 -20
- package/src/primitiveTypes/FormattedText.ts +1 -1
- package/src/utils/jsonBreakDetector/checks/findArrayConstraintNarrowing.ts +121 -0
- package/src/utils/jsonBreakDetector/checks/findConstChanges.ts +97 -0
- package/src/utils/jsonBreakDetector/checks/findEnumNarrowing.ts +124 -0
- package/src/utils/jsonBreakDetector/checks/findFieldTypeChanges.ts +105 -0
- package/src/utils/jsonBreakDetector/checks/findNewRequiredFields.ts +97 -0
- package/src/utils/jsonBreakDetector/checks/findNumericConstraintNarrowing.ts +243 -0
- package/src/utils/jsonBreakDetector/checks/findStringConstraintNarrowing.ts +139 -0
- package/src/utils/jsonBreakDetector/compareSubschemas.ts +67 -0
- package/src/utils/jsonBreakDetector/dereference/dereference.spec.ts +245 -0
- package/src/utils/jsonBreakDetector/dereference/dereference.ts +65 -0
- package/src/utils/jsonBreakDetector/jsonBreakDetector.spec.ts +3041 -0
- package/src/utils/jsonBreakDetector/jsonBreakDetector.ts +36 -0
- package/src/utils/jsonBreakDetector/types.ts +29 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { BREAK_MESSAGES, JsonBreak, pushBreak } from '../types';
|
|
2
|
+
import { JSONSchema } from '@squiz/json-schema-library';
|
|
3
|
+
|
|
4
|
+
/** Recursively detects narrowed string constraints at each schema level, accumulating dotted paths. */
|
|
5
|
+
export function findStringConstraintNarrowing(
|
|
6
|
+
beforeSchema: JSONSchema,
|
|
7
|
+
afterSchema: JSONSchema,
|
|
8
|
+
path: string,
|
|
9
|
+
breaks: JsonBreak[],
|
|
10
|
+
): void {
|
|
11
|
+
const beforeProperties = beforeSchema.properties ?? {};
|
|
12
|
+
const afterProperties = afterSchema.properties ?? {};
|
|
13
|
+
|
|
14
|
+
// Recurse into latest nested object properties.
|
|
15
|
+
for (const [key, afterPropertySchema] of Object.entries(afterProperties)) {
|
|
16
|
+
// Skip if the property is not an object.
|
|
17
|
+
if (!afterPropertySchema || typeof afterPropertySchema !== 'object') {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const beforePropertySchema = beforeProperties[key];
|
|
22
|
+
const childPath = path ? `${path}.${key}` : key;
|
|
23
|
+
|
|
24
|
+
// Only compare string constraints on fields that existed in the before schema. New fields are skipped.
|
|
25
|
+
if (beforePropertySchema && typeof beforePropertySchema === 'object') {
|
|
26
|
+
checkStringConstraintNarrowing(beforePropertySchema, afterPropertySchema, childPath, breaks);
|
|
27
|
+
findStringConstraintNarrowing(beforePropertySchema, afterPropertySchema, childPath, breaks);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const beforeItems = beforeSchema.items;
|
|
32
|
+
const afterItems = afterSchema.items;
|
|
33
|
+
|
|
34
|
+
// Recurse into latest nested array items.
|
|
35
|
+
if (afterItems && typeof afterItems === 'object' && !Array.isArray(afterItems)) {
|
|
36
|
+
const itemsPath = path ? `${path}[]` : '[]';
|
|
37
|
+
const beforeItemSchema =
|
|
38
|
+
beforeItems && typeof beforeItems === 'object' && !Array.isArray(beforeItems) ? beforeItems : undefined;
|
|
39
|
+
|
|
40
|
+
// Only compare string constraints on items that existed in the before schema. New items are skipped.
|
|
41
|
+
if (beforeItemSchema) {
|
|
42
|
+
checkStringConstraintNarrowing(beforeItemSchema, afterItems, itemsPath, breaks);
|
|
43
|
+
findStringConstraintNarrowing(beforeItemSchema, afterItems, itemsPath, breaks);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Returns true when a schema represents a string field. */
|
|
49
|
+
function isStringFieldSchema(schema: JSONSchema): boolean {
|
|
50
|
+
const type = schema.type;
|
|
51
|
+
if (type === 'string') {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Type can be defined as a union of strings e.g. ['string', 'number'], so check if 'string' is included.
|
|
56
|
+
if (Array.isArray(type) && type.includes('string')) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
schema.minLength !== undefined ||
|
|
62
|
+
schema.maxLength !== undefined ||
|
|
63
|
+
schema.format !== undefined ||
|
|
64
|
+
schema.pattern !== undefined
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Returns true when afterSchema requires a longer string than beforeSchema. */
|
|
69
|
+
function hasMinLengthIncreased(beforeSchema: JSONSchema, afterSchema: JSONSchema): boolean {
|
|
70
|
+
const beforeMinLength = beforeSchema.minLength ?? 0;
|
|
71
|
+
const afterMinLength = afterSchema.minLength ?? 0;
|
|
72
|
+
return afterMinLength > beforeMinLength;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Returns true when afterSchema allows a shorter string than beforeSchema. */
|
|
76
|
+
function hasMaxLengthLowered(beforeSchema: JSONSchema, afterSchema: JSONSchema): boolean {
|
|
77
|
+
const beforeMaxLength = beforeSchema.maxLength;
|
|
78
|
+
const afterMaxLength = afterSchema.maxLength;
|
|
79
|
+
|
|
80
|
+
if (afterMaxLength === undefined) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (beforeMaxLength === undefined) {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return afterMaxLength < beforeMaxLength;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Returns true when afterSchema adds or changes a format constraint. */
|
|
92
|
+
function hasFormatChanged(beforeSchema: JSONSchema, afterSchema: JSONSchema): boolean {
|
|
93
|
+
const afterFormat = afterSchema.format;
|
|
94
|
+
|
|
95
|
+
if (afterFormat === undefined) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return beforeSchema.format !== afterFormat;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Returns true when afterSchema adds or changes a pattern constraint. */
|
|
103
|
+
function hasPatternChanged(beforeSchema: JSONSchema, afterSchema: JSONSchema): boolean {
|
|
104
|
+
const afterPattern = afterSchema.pattern;
|
|
105
|
+
|
|
106
|
+
if (afterPattern === undefined) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return beforeSchema.pattern !== afterPattern;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Checks string constraint keywords on a single schema for narrowing changes. */
|
|
114
|
+
export function checkStringConstraintNarrowing(
|
|
115
|
+
beforeSchema: JSONSchema,
|
|
116
|
+
afterSchema: JSONSchema,
|
|
117
|
+
path: string,
|
|
118
|
+
breaks: JsonBreak[],
|
|
119
|
+
): void {
|
|
120
|
+
if (!isStringFieldSchema(beforeSchema) && !isStringFieldSchema(afterSchema)) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (hasMinLengthIncreased(beforeSchema, afterSchema)) {
|
|
125
|
+
pushBreak(breaks, path, BREAK_MESSAGES.minLength);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (hasMaxLengthLowered(beforeSchema, afterSchema)) {
|
|
129
|
+
pushBreak(breaks, path, BREAK_MESSAGES.maxLength);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (hasFormatChanged(beforeSchema, afterSchema)) {
|
|
133
|
+
pushBreak(breaks, path, BREAK_MESSAGES.format);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (hasPatternChanged(beforeSchema, afterSchema)) {
|
|
137
|
+
pushBreak(breaks, path, BREAK_MESSAGES.pattern);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { JSONSchema } from '@squiz/json-schema-library';
|
|
2
|
+
|
|
3
|
+
import { findArrayConstraintNarrowing, checkArrayConstraintNarrowing } from './checks/findArrayConstraintNarrowing';
|
|
4
|
+
import { findConstChanges, checkConstNarrowingAtNode } from './checks/findConstChanges';
|
|
5
|
+
import { findFieldTypeChanges, hasTypeChange } from './checks/findFieldTypeChanges';
|
|
6
|
+
import { findNewRequiredFields } from './checks/findNewRequiredFields';
|
|
7
|
+
import {
|
|
8
|
+
checkNumericConstraintNarrowing,
|
|
9
|
+
findNumericConstraintNarrowing,
|
|
10
|
+
} from './checks/findNumericConstraintNarrowing';
|
|
11
|
+
import { findEnumNarrowing, checkEnumNarrowingAtNode } from './checks/findEnumNarrowing';
|
|
12
|
+
import { checkStringConstraintNarrowing, findStringConstraintNarrowing } from './checks/findStringConstraintNarrowing';
|
|
13
|
+
import { BREAK_MESSAGES, JsonBreak, pushBreak } from './types';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Compares two schemas at the same path using every jsonBreakDetector check,
|
|
17
|
+
* including leaf-level constraints on the current node.
|
|
18
|
+
*/
|
|
19
|
+
export function compareSubschemas(
|
|
20
|
+
beforeSchema: JSONSchema,
|
|
21
|
+
afterSchema: JSONSchema,
|
|
22
|
+
path: string,
|
|
23
|
+
breaks: JsonBreak[],
|
|
24
|
+
): void {
|
|
25
|
+
// Only applicable to nested schemas (from `contains` subschemas), not the root schema.
|
|
26
|
+
if (path !== '') {
|
|
27
|
+
if (hasTypeChange(beforeSchema, afterSchema)) {
|
|
28
|
+
pushBreak(breaks, path, BREAK_MESSAGES.type);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
checkEnumNarrowingAtNode(beforeSchema, afterSchema, path, breaks);
|
|
32
|
+
checkConstNarrowingAtNode(beforeSchema, afterSchema, path, breaks);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
checkArrayConstraintNarrowing(beforeSchema, afterSchema, path, breaks);
|
|
36
|
+
checkNumericConstraintNarrowing(beforeSchema, afterSchema, path, breaks);
|
|
37
|
+
checkStringConstraintNarrowing(beforeSchema, afterSchema, path, breaks);
|
|
38
|
+
|
|
39
|
+
// Find breaking changes at children properties and items
|
|
40
|
+
findNewRequiredFields(beforeSchema, afterSchema, path, breaks);
|
|
41
|
+
findFieldTypeChanges(beforeSchema, afterSchema, path, breaks);
|
|
42
|
+
findEnumNarrowing(beforeSchema, afterSchema, path, breaks);
|
|
43
|
+
findConstChanges(beforeSchema, afterSchema, path, breaks);
|
|
44
|
+
findArrayConstraintNarrowing(beforeSchema, afterSchema, path, breaks);
|
|
45
|
+
findNumericConstraintNarrowing(beforeSchema, afterSchema, path, breaks);
|
|
46
|
+
findStringConstraintNarrowing(beforeSchema, afterSchema, path, breaks);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Compares array `contains` subschemas using the full jsonBreakDetector check suite. */
|
|
50
|
+
export function compareContainsSubschemas(
|
|
51
|
+
beforeArraySchema: JSONSchema,
|
|
52
|
+
afterArraySchema: JSONSchema,
|
|
53
|
+
arrayPath: string,
|
|
54
|
+
breaks: JsonBreak[],
|
|
55
|
+
): void {
|
|
56
|
+
const afterContains = afterArraySchema.contains;
|
|
57
|
+
|
|
58
|
+
if (!afterContains || typeof afterContains !== 'object') {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const beforeContains = beforeArraySchema.contains;
|
|
63
|
+
const containsPath = arrayPath ? `${arrayPath}.{contains}` : '{contains}';
|
|
64
|
+
const beforeSubschema = beforeContains && typeof beforeContains === 'object' ? (beforeContains as JSONSchema) : {};
|
|
65
|
+
|
|
66
|
+
compareSubschemas(beforeSubschema, afterContains as JSONSchema, containsPath, breaks);
|
|
67
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { dereferenceLocal } from './dereference';
|
|
2
|
+
|
|
3
|
+
describe('dereferenceLocal', () => {
|
|
4
|
+
describe('JSON Schema Draft 7', () => {
|
|
5
|
+
it('dereferences a top-level $ref to a definition', () => {
|
|
6
|
+
const schema = {
|
|
7
|
+
$schema: 'http://json-schema.org/draft-07/schema#',
|
|
8
|
+
$ref: '#/definitions/Person',
|
|
9
|
+
definitions: {
|
|
10
|
+
Person: {
|
|
11
|
+
type: 'object',
|
|
12
|
+
properties: {
|
|
13
|
+
name: { type: 'string' },
|
|
14
|
+
},
|
|
15
|
+
required: ['name'],
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
expect(dereferenceLocal(schema)).toEqual({
|
|
21
|
+
$schema: 'http://json-schema.org/draft-07/schema#',
|
|
22
|
+
definitions: {
|
|
23
|
+
Person: {
|
|
24
|
+
type: 'object',
|
|
25
|
+
properties: {
|
|
26
|
+
name: { type: 'string' },
|
|
27
|
+
},
|
|
28
|
+
required: ['name'],
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
type: 'object',
|
|
32
|
+
properties: {
|
|
33
|
+
name: { type: 'string' },
|
|
34
|
+
},
|
|
35
|
+
required: ['name'],
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('dereferences nested $refs in properties', () => {
|
|
40
|
+
const schema = {
|
|
41
|
+
$schema: 'http://json-schema.org/draft-07/schema#',
|
|
42
|
+
type: 'object',
|
|
43
|
+
properties: {
|
|
44
|
+
address: { $ref: '#/definitions/Address' },
|
|
45
|
+
},
|
|
46
|
+
definitions: {
|
|
47
|
+
Address: {
|
|
48
|
+
type: 'object',
|
|
49
|
+
properties: {
|
|
50
|
+
city: { $ref: '#/definitions/City' },
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
City: {
|
|
54
|
+
type: 'string',
|
|
55
|
+
minLength: 1,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
expect(dereferenceLocal(schema)).toEqual({
|
|
61
|
+
$schema: 'http://json-schema.org/draft-07/schema#',
|
|
62
|
+
type: 'object',
|
|
63
|
+
properties: {
|
|
64
|
+
address: {
|
|
65
|
+
type: 'object',
|
|
66
|
+
properties: {
|
|
67
|
+
city: {
|
|
68
|
+
type: 'string',
|
|
69
|
+
minLength: 1,
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
definitions: {
|
|
75
|
+
Address: {
|
|
76
|
+
type: 'object',
|
|
77
|
+
properties: {
|
|
78
|
+
city: { $ref: '#/definitions/City' },
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
City: {
|
|
82
|
+
type: 'string',
|
|
83
|
+
minLength: 1,
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('dereferences $refs inside combinators', () => {
|
|
90
|
+
const schema = {
|
|
91
|
+
$schema: 'http://json-schema.org/draft-07/schema#',
|
|
92
|
+
oneOf: [{ $ref: '#/definitions/StringValue' }, { $ref: '#/definitions/NumberValue' }],
|
|
93
|
+
definitions: {
|
|
94
|
+
StringValue: { type: 'string' },
|
|
95
|
+
NumberValue: { type: 'number' },
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
expect(dereferenceLocal(schema)).toEqual({
|
|
100
|
+
$schema: 'http://json-schema.org/draft-07/schema#',
|
|
101
|
+
oneOf: [{ type: 'string' }, { type: 'number' }],
|
|
102
|
+
definitions: {
|
|
103
|
+
StringValue: { type: 'string' },
|
|
104
|
+
NumberValue: { type: 'number' },
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('dereferences chained $refs', () => {
|
|
110
|
+
const schema = {
|
|
111
|
+
$schema: 'http://json-schema.org/draft-07/schema#',
|
|
112
|
+
$ref: '#/definitions/Outer',
|
|
113
|
+
definitions: {
|
|
114
|
+
Outer: { $ref: '#/definitions/Inner' },
|
|
115
|
+
Inner: {
|
|
116
|
+
type: 'boolean',
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
expect(dereferenceLocal(schema)).toEqual({
|
|
122
|
+
$schema: 'http://json-schema.org/draft-07/schema#',
|
|
123
|
+
definitions: {
|
|
124
|
+
Outer: { $ref: '#/definitions/Inner' },
|
|
125
|
+
Inner: {
|
|
126
|
+
type: 'boolean',
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
type: 'boolean',
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('merges sibling properties alongside a $ref', () => {
|
|
134
|
+
const schema = {
|
|
135
|
+
$schema: 'http://json-schema.org/draft-07/schema#',
|
|
136
|
+
properties: {
|
|
137
|
+
title: {
|
|
138
|
+
$ref: '#/definitions/NonEmptyString',
|
|
139
|
+
description: 'A human-readable title',
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
definitions: {
|
|
143
|
+
NonEmptyString: {
|
|
144
|
+
type: 'string',
|
|
145
|
+
minLength: 1,
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
expect(dereferenceLocal(schema)).toEqual({
|
|
151
|
+
$schema: 'http://json-schema.org/draft-07/schema#',
|
|
152
|
+
properties: {
|
|
153
|
+
title: {
|
|
154
|
+
type: 'string',
|
|
155
|
+
minLength: 1,
|
|
156
|
+
description: 'A human-readable title',
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
definitions: {
|
|
160
|
+
NonEmptyString: {
|
|
161
|
+
type: 'string',
|
|
162
|
+
minLength: 1,
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('resolves JSON pointer escaped property names', () => {
|
|
169
|
+
const schema = {
|
|
170
|
+
$schema: 'http://json-schema.org/draft-07/schema#',
|
|
171
|
+
properties: {
|
|
172
|
+
'foo/bar': { $ref: '#/definitions/foo~1bar' },
|
|
173
|
+
},
|
|
174
|
+
definitions: {
|
|
175
|
+
'foo/bar': {
|
|
176
|
+
type: 'string',
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
expect(dereferenceLocal(schema)).toEqual({
|
|
182
|
+
$schema: 'http://json-schema.org/draft-07/schema#',
|
|
183
|
+
properties: {
|
|
184
|
+
'foo/bar': {
|
|
185
|
+
type: 'string',
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
definitions: {
|
|
189
|
+
'foo/bar': {
|
|
190
|
+
type: 'string',
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('does not infinitely recurse on circular definitions', () => {
|
|
197
|
+
const schema = {
|
|
198
|
+
$schema: 'http://json-schema.org/draft-07/schema#',
|
|
199
|
+
$ref: '#/definitions/Node',
|
|
200
|
+
definitions: {
|
|
201
|
+
Node: {
|
|
202
|
+
type: 'object',
|
|
203
|
+
properties: {
|
|
204
|
+
child: { $ref: '#/definitions/Node' },
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const result = dereferenceLocal(schema) as Record<string, unknown>;
|
|
211
|
+
|
|
212
|
+
expect(result.type).toBe('object');
|
|
213
|
+
expect((result.properties as Record<string, unknown>).child).toEqual({
|
|
214
|
+
type: 'object',
|
|
215
|
+
properties: {
|
|
216
|
+
child: { $ref: '#/definitions/Node' },
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('throws when a reference is not internal', () => {
|
|
222
|
+
const schema = {
|
|
223
|
+
$schema: 'http://json-schema.org/draft-07/schema#',
|
|
224
|
+
$ref: 'https://example.com/schema.json',
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
expect(() => dereferenceLocal(schema)).toThrow(
|
|
228
|
+
'Only internal references starting with "#/" are supported. Received: "https://example.com/schema.json"',
|
|
229
|
+
);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('throws when a JSON pointer cannot be resolved', () => {
|
|
233
|
+
const schema = {
|
|
234
|
+
$schema: 'http://json-schema.org/draft-07/schema#',
|
|
235
|
+
properties: {
|
|
236
|
+
missing: { $ref: '#/definitions/DoesNotExist' },
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
expect(() => dereferenceLocal(schema)).toThrow(
|
|
241
|
+
'Could not resolve JSON pointer reference: "#/definitions/DoesNotExist" at segment "definitions"',
|
|
242
|
+
);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { JSONSchema } from '@squiz/json-schema-library';
|
|
2
|
+
|
|
3
|
+
export type GenericSchema = Record<string, any>;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Synchronously dereferences internal ($ref: "#/...") JSON schema references.
|
|
7
|
+
*/
|
|
8
|
+
export function dereferenceLocal<T extends GenericSchema = JSONSchema>(schema: T): T {
|
|
9
|
+
function resolvePointer(root: Record<string, any>, pointer: string): any {
|
|
10
|
+
if (!pointer.startsWith('#/')) {
|
|
11
|
+
throw new Error(`Only internal references starting with "#/" are supported. Received: "${pointer}"`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const pathSegments = pointer.replace(/^#\//, '').split('/');
|
|
15
|
+
|
|
16
|
+
return pathSegments.reduce((acc, key) => {
|
|
17
|
+
// Decode RFC 6901 JSON Pointer escaped sequences (~1 -> /, ~0 -> ~)
|
|
18
|
+
const unescapedKey = key.replace(/~1/g, '/').replace(/~0/g, '~');
|
|
19
|
+
|
|
20
|
+
if (acc && typeof acc === 'object' && unescapedKey in acc) {
|
|
21
|
+
return acc[unescapedKey];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
throw new Error(`Could not resolve JSON pointer reference: "${pointer}" at segment "${key}"`);
|
|
25
|
+
}, root);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function walk(node: any, root: Record<string, any>, seen = new WeakSet<object>()): any {
|
|
29
|
+
if (node === null || typeof node !== 'object') {
|
|
30
|
+
return node;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Prevent infinite loops on circular references
|
|
34
|
+
if (seen.has(node)) {
|
|
35
|
+
return node;
|
|
36
|
+
}
|
|
37
|
+
seen.add(node);
|
|
38
|
+
|
|
39
|
+
if (Array.isArray(node)) {
|
|
40
|
+
return node.map((item) => walk(item, root, seen));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Check if current object contains a string $ref property
|
|
44
|
+
if (typeof node.$ref === 'string') {
|
|
45
|
+
const resolvedTarget = resolvePointer(root, node.$ref);
|
|
46
|
+
|
|
47
|
+
// Separate $ref from rest of keys (JSON Schema draft 2019-09+ allows sibling properties)
|
|
48
|
+
const { $ref: _ref, ...rest } = node;
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
...walk(resolvedTarget, root, seen),
|
|
52
|
+
...walk(rest, root, seen),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const result: Record<string, any> = {};
|
|
57
|
+
for (const [key, value] of Object.entries(node)) {
|
|
58
|
+
result[key] = walk(value, root, seen);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return walk(schema, schema) as T;
|
|
65
|
+
}
|