-
Notifications
You must be signed in to change notification settings - Fork 129
Expand file tree
/
Copy pathbundle-schemas.js
More file actions
254 lines (218 loc) · 7.64 KB
/
bundle-schemas.js
File metadata and controls
254 lines (218 loc) · 7.64 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { writeFile } from 'node:fs/promises';
import Bundler from '@hyperjump/json-schema-bundle';
(async function () {
bundle('https://json-schema.org/draft/2019-09/schema', 'draft-2019-09', 'https://json-schema.org/draft/2019-09');
bundle('https://json-schema.org/draft/2020-12/schema', 'draft-2020-12', 'https://json-schema.org/draft/2020-12');
}());
async function bundle(uri, filename, derivedURL) {
const metaSchema = await Bundler.get(uri);
let bundle = await Bundler.bundle(metaSchema);
bundle = JSON.parse(JSON.stringify(bundle, null, 2).replace(/"undefined": ""/g, '"$dynamicAnchor": "meta"'));
await writeFile(`./${filename}.json`, JSON.stringify(bundle, null, 2), 'utf8');
bundle = flattenDraftMetaSchema(bundle);
const jsified = getCopyright(derivedURL) + 'export default ' + printObject(bundle);
await writeFile(`./${filename}-flat.json`, JSON.stringify(bundle, null, 2), 'utf8');
await writeFile(`./src/services/schemas/${filename}-flat.ts`, jsified, 'utf8');
}
function getCopyright(derivedURL) {
return [
'/*---------------------------------------------------------------------------------------------',
' * Copyright (c) Microsoft Corporation. All rights reserved.',
' * Licensed under the MIT License. See License.txt in the project root for license information.',
' *--------------------------------------------------------------------------------------------*/',
'',
'// This file is generated - do not edit directly!',
'// Derived from ' + derivedURL,
].join('\n') + '\n\n';
}
function printLiteral(value) {
if (typeof value === 'string') {
return `'${value}'`;
}
return value;
}
function printKey(value) {
if (value.match(/^[a-zA-Z_$][a-zA-Z0-9_$]*$/)) {
return `${value}`;
}
return `'${value}'`;
}
function indent(level) {
return '\t'.repeat(level);
}
function printObject(obj, indentLevel = 0) {
const result = [];
if (Array.isArray(obj)) {
result.push('[');
for (const item of obj) {
if (typeof item === 'object' && item !== null) {
result.push(`${indent(indentLevel + 1)}${printObject(item, indentLevel + 1)},`);
} else {
result.push(`${indent(indentLevel + 1)}${printLiteral(item)},`);
}
}
result.push(`${indent(indentLevel)}]`);
return result.join('\n');
}
if (obj === null) {
result.push('null');
return result.join('\n');
}
result.push('{');
for (const [key, value] of Object.entries(obj)) {
if (typeof value === 'object' && value !== null) {
result.push(`${indent(indentLevel + 1)}${printKey(key)}: ${printObject(value, indentLevel + 1)},`);
} else {
result.push(`${indent(indentLevel + 1)}${printKey(key)}: ${printLiteral(value)},`);
}
}
result.push(`${indent(indentLevel)}}`);
return result.join('\n');
}
// flatten
const DEFAULT_ANCHOR = 'meta';
function visit(node, callback) {
if (!node || typeof node !== 'object') return;
if (Array.isArray(node)) {
for (const item of node) {
visit(item, callback);
}
return;
}
for (const key of Object.keys(node)) {
callback(node, key);
visit(node[key], callback);
}
}
/** Recursively replace $dynamicRef:#meta with $ref:#meta */
function replaceDynamicRefs(node, anchorName = DEFAULT_ANCHOR) {
visit(node, (n, k) => {
const v = n[k];
if (k === '$dynamicRef' && v === '#' + anchorName) {
n.$ref = '#';
delete n.$dynamicRef;
}
});
}
/** Recursively replace $dynamicRef:#meta with $ref:#meta */
function replaceRecursiveRefs(node, anchorName = DEFAULT_ANCHOR) {
visit(node, (n, k) => {
const v = n[k];
if (k === '$recursiveRef') {
n.$ref = v;
delete n.$recursiveRef;
}
});
}
/** Replace refs that point to a vocabulary */
function replaceOldRefs(node, anchorName = DEFAULT_ANCHOR) {
visit(node, (n, k) => {
const v = n[k];
if (k === '$ref' && typeof v === 'string' && v.startsWith(anchorName + '/')) {
const segments = v.split('#');
if (segments.length === 2) {
n.$ref = `#${segments[1]}`;
}
}
});
}
/** Remove all $dynamicAnchor occurrences (except keep keyword definition property) */
function stripDynamicAnchors(node) {
visit(node, (n, k) => {
if (k === '$dynamicAnchor') {
delete n[k];
}
});
}
/** Collect vocabulary object definitions from $defs */
function collectVocabularies(schema) {
const vocabularies = [];
const defs = schema.$defs || {};
for (const [, value] of Object.entries(defs)) {
if (value && typeof value === 'object' && !Array.isArray(value) && value.$id && value.$dynamicAnchor === DEFAULT_ANCHOR && value.properties) {
vocabularies.push(value);
}
}
return vocabularies;
}
/** Merge properties from each vocabulary into root.properties (shallow) */
function mergeVocabularyProperties(root, vocabularies) {
if (!root.properties) root.properties = {};
replaceOldRefs(root);
for (const vocab of vocabularies) {
for (const [propName, propSchema] of Object.entries(vocab.properties || {})) {
if (!(propName in root.properties)) {
root.properties[propName] = propSchema;
} else {
// Simple heuristic: if both are objects, attempt shallow merge, else keep existing
const existing = root.properties[propName];
if (isPlainObject(existing) && isPlainObject(propSchema)) {
root.properties[propName] = { ...existing, ...propSchema };
}
}
}
}
}
function isPlainObject(o) {
return !!o && typeof o === 'object' && !Array.isArray(o);
}
/** Gather unified $defs from vocab $defs (only specific keys) */
function buildUnifiedDefs(schema, vocabularies) {
const unified = schema.$defs && !referencesVocabulary(schema.$defs) ? { ...schema.$defs } : {};
function harvest(defsObj) {
if (!defsObj || typeof defsObj !== 'object') return;
for (const [k, v] of Object.entries(defsObj)) {
if (!(k in unified)) {
unified[k] = v;
} else {
console.warn(`Warning: duplicate definition for key ${k} found while building unified $defs. Keeping the first occurrence.`);
}
}
}
for (const vocab of vocabularies) harvest(vocab.$defs);
// Adjust schemaArray items dynamicRef->ref later with global replacement
return unified;
}
function referencesVocabulary(defs) {
return Object.keys(defs).some(k => k.startsWith('https://json-schema.org/draft/'));
}
function flattenDraftMetaSchema(original) {
// Clone to avoid mutating input reference
const schema = JSON.parse(JSON.stringify(original));
const anchorName = schema.$dynamicAnchor || DEFAULT_ANCHOR;
// 1. Collect vocabulary schemas
const vocabularies = collectVocabularies(schema);
// 2. Merge vocabulary properties into root
mergeVocabularyProperties(schema, vocabularies);
// 3. Build unified $defs
const unifiedDefs = buildUnifiedDefs(schema, vocabularies);
// 4. Remove top-level allOf (flatten composition)
delete schema.allOf;
// 5. Remove vocabulary objects from $defs
if (schema.$defs) {
for (const k of Object.keys(schema.$defs)) {
if (schema.$defs[k] && schema.$defs[k].$id && schema.$defs[k].$dynamicAnchor === anchorName) {
delete schema.$defs[k];
}
}
}
// 6. Assign unified defs
schema.$defs = unifiedDefs;
// 7. Convert dynamic recursion markers
replaceDynamicRefs(schema, anchorName);
replaceRecursiveRefs(schema, anchorName);
stripDynamicAnchors(schema);
// 8. Add static anchor at root
delete schema.$dynamicAnchor;
// 9. Update title to signal flattening
if (schema.title) {
schema.title = '(Flattened static) ' + schema.title;
} else {
schema.title = 'Flattened Draft 2020-12 meta-schema';
}
return schema;
}