Skip to content

Commit d9d449b

Browse files
authored
use flat 2019-09 and 2020-12 schemas (#295)
1 parent de2c2a0 commit d9d449b

File tree

5 files changed

+886
-844
lines changed

5 files changed

+886
-844
lines changed

build/bundle-schemas.cjs

Lines changed: 230 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -7,64 +7,248 @@ const fs = require('fs').promises;
77
const Bundler = require("@hyperjump/json-schema-bundle");
88

99
(async function () {
10-
bundle(`https://json-schema.org/draft/2019-09/schema`, 'draft09');
11-
bundle(`https://json-schema.org/draft/2020-12/schema`, 'draft12');
10+
bundle(`https://json-schema.org/draft/2019-09/schema`, 'draft-2019-09', 'https://json-schema.org/draft/2019-09');
11+
bundle(`https://json-schema.org/draft/2020-12/schema`, 'draft-2020-12', 'https://json-schema.org/draft/2020-12');
1212
}());
1313

14-
async function bundle(uri, filename) {
15-
const metaSchema = await Bundler.get(uri);
16-
const bundle = await Bundler.bundle(metaSchema);
17-
const jsonified = JSON.stringify(bundle, null, 2).replace(/"undefined": ""/g, '"$dynamicAnchor": "meta"');
18-
const jsified = 'export default ' + printObject(JSON.parse(jsonified));
19-
fs.writeFile(`./${filename}.json`, jsonified, 'utf8');
20-
fs.writeFile(`./${filename}.js`, jsified, 'utf8');
14+
async function bundle(uri, filename, derivedURL) {
15+
const metaSchema = await Bundler.get(uri);
16+
let bundle = await Bundler.bundle(metaSchema);
17+
bundle = JSON.parse(JSON.stringify(bundle, null, 2).replace(/"undefined": ""/g, '"$dynamicAnchor": "meta"'));
18+
fs.writeFile(`./${filename}.json`, JSON.stringify(bundle, null, 2), 'utf8');
19+
bundle = flattenDraftMetaSchema(bundle);
20+
const jsified = getCopyright(derivedURL) + 'export default ' + printObject(bundle);
21+
fs.writeFile(`./${filename}-flat.json`, JSON.stringify(bundle, null, 2), 'utf8');
22+
fs.writeFile(`./src/services/schemas/${filename}-flat.ts`, jsified, 'utf8');
2123
}
24+
function getCopyright(derivedURL) {
25+
return [
26+
'/*---------------------------------------------------------------------------------------------',
27+
' * Copyright (c) Microsoft Corporation. All rights reserved.',
28+
' * Licensed under the MIT License. See License.txt in the project root for license information.',
29+
' *--------------------------------------------------------------------------------------------*/',
30+
'',
31+
'// This file is generated - do not edit directly!',
32+
'// Derived from ' + derivedURL,
33+
].join('\n') + '\n\n';
34+
}
35+
2236

2337
function printLiteral(value) {
24-
if (typeof value === 'string') {
25-
return `'${value}'`;
26-
}
27-
return value;
38+
if (typeof value === 'string') {
39+
return `'${value}'`;
40+
}
41+
return value;
2842
}
2943

3044
function printKey(value) {
31-
if (value.match(/^[a-zA-Z_$][a-zA-Z0-9_$]*$/)) {
32-
return `${value}`;
33-
}
34-
return `'${value}'`;
45+
if (value.match(/^[a-zA-Z_$][a-zA-Z0-9_$]*$/)) {
46+
return `${value}`;
47+
}
48+
return `'${value}'`;
3549
}
3650

3751
function indent(level) {
38-
return '\t'.repeat(level);
52+
return '\t'.repeat(level);
3953
}
4054

4155
function printObject(obj, indentLevel = 0) {
42-
const result = [];
43-
if (Array.isArray(obj)) {
44-
result.push(`[`);
45-
for (const item of obj) {
46-
if (typeof item === 'object' && item !== null) {
47-
result.push(`${indent(indentLevel + 1)}${printObject(item, indentLevel + 1)},`);
48-
} else {
49-
result.push(`${indent(indentLevel + 1)}${printLiteral(item)},`);
50-
}
51-
}
52-
result.push(`${indent(indentLevel)}]`);
53-
return result.join('\n');
54-
}
55-
if (obj === null) {
56-
result.push(`null`);
57-
return result.join('\n');
58-
}
59-
60-
result.push(`{`);
61-
for (const [key, value] of Object.entries(obj)) {
62-
if (typeof value === 'object' && value !== null) {
63-
result.push(`${indent(indentLevel + 1)}${printKey(key)}: ${printObject(value, indentLevel + 1)},`);
64-
} else {
65-
result.push(`${indent(indentLevel + 1)}${printKey(key)}: ${printLiteral(value)},`);
66-
}
67-
}
68-
result.push(`${indent(indentLevel)}}`);
69-
return result.join('\n');
56+
const result = [];
57+
if (Array.isArray(obj)) {
58+
result.push(`[`);
59+
for (const item of obj) {
60+
if (typeof item === 'object' && item !== null) {
61+
result.push(`${indent(indentLevel + 1)}${printObject(item, indentLevel + 1)},`);
62+
} else {
63+
result.push(`${indent(indentLevel + 1)}${printLiteral(item)},`);
64+
}
65+
}
66+
result.push(`${indent(indentLevel)}]`);
67+
return result.join('\n');
68+
}
69+
if (obj === null) {
70+
result.push(`null`);
71+
return result.join('\n');
72+
}
73+
74+
result.push(`{`);
75+
for (const [key, value] of Object.entries(obj)) {
76+
if (typeof value === 'object' && value !== null) {
77+
result.push(`${indent(indentLevel + 1)}${printKey(key)}: ${printObject(value, indentLevel + 1)},`);
78+
} else {
79+
result.push(`${indent(indentLevel + 1)}${printKey(key)}: ${printLiteral(value)},`);
80+
}
81+
}
82+
result.push(`${indent(indentLevel)}}`);
83+
return result.join('\n');
84+
}
85+
// flatten
86+
87+
const DEFAULT_ANCHOR = 'meta';
88+
89+
function visit(node, callback) {
90+
if (!node || typeof node !== 'object') return;
91+
if (Array.isArray(node)) {
92+
for (const item of node) {
93+
visit(item, callback);
94+
}
95+
return;
96+
}
97+
98+
for (const key of Object.keys(node)) {
99+
callback(node, key);
100+
visit(node[key], callback);
101+
}
102+
}
103+
104+
/** Recursively replace $dynamicRef:#meta with $ref:#meta */
105+
function replaceDynamicRefs(node, anchorName = DEFAULT_ANCHOR) {
106+
visit(node, (n, k) => {
107+
const v = n[k];
108+
if (k === '$dynamicRef' && v === '#' + anchorName) {
109+
n['$ref'] = '#';
110+
delete n['$dynamicRef'];
111+
};
112+
});
113+
}
114+
115+
/** Recursively replace $dynamicRef:#meta with $ref:#meta */
116+
function replaceRecursiveRefs(node, anchorName = DEFAULT_ANCHOR) {
117+
visit(node, (n, k) => {
118+
const v = n[k];
119+
if (k === '$recursiveRef') {
120+
n['$ref'] = v;
121+
delete n['$recursiveRef'];
122+
};
123+
});
124+
}
125+
126+
/** Replace refs that point to a vocabulary */
127+
function replaceOldRefs(node, anchorName = DEFAULT_ANCHOR) {
128+
visit(node, (n, k) => {
129+
const v = n[k];
130+
if (k === '$ref' && typeof v === 'string' && v.startsWith(anchorName + '/')) {
131+
const segments = v.split('#');
132+
if (segments.length === 2) {
133+
n['$ref'] = `#${segments[1]}`;
134+
}
135+
}
136+
});
137+
}
138+
139+
/** Remove all $dynamicAnchor occurrences (except keep keyword definition property) */
140+
function stripDynamicAnchors(node) {
141+
visit(node, (n, k) => {
142+
if (k === '$dynamicAnchor') {
143+
delete n[k];
144+
}
145+
});
146+
}
147+
148+
/** Collect vocabulary object definitions from $defs */
149+
function collectVocabularies(schema) {
150+
const vocabularies = [];
151+
const defs = schema.$defs || {};
152+
for (const [key, value] of Object.entries(defs)) {
153+
if (value && typeof value === 'object' && !Array.isArray(value) && value.$id && value.$dynamicAnchor === DEFAULT_ANCHOR && value.properties) {
154+
vocabularies.push(value);
155+
}
156+
}
157+
return vocabularies;
158+
}
159+
160+
/** Merge properties from each vocabulary into root.properties (shallow) */
161+
function mergeVocabularyProperties(root, vocabularies) {
162+
if (!root.properties) root.properties = {};
163+
replaceOldRefs(root);
164+
for (const vocab of vocabularies) {
165+
for (const [propName, propSchema] of Object.entries(vocab.properties || {})) {
166+
if (!(propName in root.properties)) {
167+
root.properties[propName] = propSchema;
168+
} else {
169+
// Simple heuristic: if both are objects, attempt shallow merge, else keep existing
170+
const existing = root.properties[propName];
171+
if (isPlainObject(existing) && isPlainObject(propSchema)) {
172+
root.properties[propName] = { ...existing, ...propSchema };
173+
}
174+
}
175+
}
176+
}
177+
}
178+
179+
function isPlainObject(o) {
180+
return !!o && typeof o === 'object' && !Array.isArray(o);
181+
}
182+
183+
/** Gather unified $defs from vocab $defs (only specific keys) */
184+
function buildUnifiedDefs(schema, vocabularies) {
185+
const unified = schema.$defs && !referencesVocabulary(schema.$defs) ? { ...schema.$defs } : {};
186+
187+
function harvest(defsObj) {
188+
if (!defsObj || typeof defsObj !== 'object') return;
189+
for (const [k, v] of Object.entries(defsObj)) {
190+
if (!(k in unified)) {
191+
unified[k] = v;
192+
} else {
193+
console.warn(`Warning: duplicate definition for key ${k} found while building unified $defs. Keeping the first occurrence.`);
194+
}
195+
}
196+
}
197+
198+
for (const vocab of vocabularies) harvest(vocab.$defs);
199+
200+
// Adjust schemaArray items dynamicRef->ref later with global replacement
201+
return unified;
202+
}
203+
204+
function referencesVocabulary(defs) {
205+
return Object.keys(defs).some(k => k.startsWith('https://json-schema.org/draft/'));
206+
}
207+
208+
function flattenDraftMetaSchema(original) {
209+
// Clone to avoid mutating input reference
210+
const schema = JSON.parse(JSON.stringify(original));
211+
212+
const anchorName = schema.$dynamicAnchor || DEFAULT_ANCHOR;
213+
214+
// 1. Collect vocabulary schemas
215+
const vocabularies = collectVocabularies(schema);
216+
217+
// 2. Merge vocabulary properties into root
218+
mergeVocabularyProperties(schema, vocabularies);
219+
220+
// 3. Build unified $defs
221+
const unifiedDefs = buildUnifiedDefs(schema, vocabularies);
222+
223+
// 4. Remove top-level allOf (flatten composition)
224+
delete schema.allOf;
225+
226+
// 5. Remove vocabulary objects from $defs
227+
if (schema.$defs) {
228+
for (const k of Object.keys(schema.$defs)) {
229+
if (schema.$defs[k] && schema.$defs[k].$id && schema.$defs[k].$dynamicAnchor === anchorName) {
230+
delete schema.$defs[k];
231+
}
232+
}
233+
}
234+
235+
// 6. Assign unified defs
236+
schema.$defs = unifiedDefs;
237+
238+
// 7. Convert dynamic recursion markers
239+
replaceDynamicRefs(schema, anchorName);
240+
replaceRecursiveRefs(schema, anchorName);
241+
stripDynamicAnchors(schema);
242+
243+
// 8. Add static anchor at root
244+
delete schema.$dynamicAnchor;
245+
246+
// 9. Update title to signal flattening
247+
if (schema.title) {
248+
schema.title = '(Flattened static) ' + schema.title;
249+
} else {
250+
schema.title = 'Flattened Draft 2020-12 meta-schema';
251+
}
252+
253+
return schema;
70254
}

0 commit comments

Comments
 (0)