-
Notifications
You must be signed in to change notification settings - Fork 21
Expand file tree
/
Copy pathoptimize.ts
More file actions
275 lines (235 loc) · 8.73 KB
/
optimize.ts
File metadata and controls
275 lines (235 loc) · 8.73 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
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
import { combineComments } from './comments';
import { isPrimitive, type CombinedType, type Primitive, type Schema } from './ir';
export type OptimizeFn = (schema: Schema) => Schema;
export function foldIntersection(schema: Schema, optimize: OptimizeFn): Schema {
if (schema.type !== 'intersection') {
return schema;
}
const innerSchemas = schema.schemas.map(optimize);
let combinedObject: Schema & { type: 'object' } = {
type: 'object',
properties: {},
required: [],
};
let result: Schema = combinedObject;
innerSchemas.forEach((innerSchema) => {
if (innerSchema.type === 'object') {
Object.assign(combinedObject.properties, innerSchema.properties);
combinedObject.required.push(...innerSchema.required);
} else if (result.type === 'intersection') {
result.schemas.push(innerSchema);
} else {
result = {
type: 'intersection',
schemas: [combinedObject, innerSchema],
};
}
});
// If the combined object is empty (no properties) and result is an intersection,
// we can simplify by removing the empty object
const hasProperties = Object.keys(combinedObject.properties).length > 0;
if (!hasProperties && result !== combinedObject) {
// At this point, result must be an intersection since it was reassigned
const intersectionResult = result as unknown as CombinedType;
// Remove the empty object from the intersection
const nonEmptySchemas = intersectionResult.schemas.filter(
(s: Schema) => !(s.type === 'object' && Object.keys(s.properties).length === 0),
);
if (nonEmptySchemas.length === 0) {
return combinedObject;
} else if (nonEmptySchemas.length === 1) {
return nonEmptySchemas[0]!;
} else {
return { type: 'intersection', schemas: nonEmptySchemas };
}
}
return result;
}
function consolidateUnion(schema: Schema): Schema {
if (schema.type !== 'union') return schema;
if (schema.schemas.length === 1) return schema.schemas[0]!;
if (schema.schemas.length === 0) return { type: 'undefined' };
const consolidatableTypes = ['boolean', 'number', 'string'];
const innerSchemas = schema.schemas.map(optimize);
const booleanSchemas = innerSchemas.filter(
(s) => s.type === 'boolean' || s.decodedType === 'boolean',
);
if (booleanSchemas.length === innerSchemas.length && booleanSchemas.length > 0) {
return { type: 'boolean' };
}
const isConsolidatableType = (s: Schema): boolean => {
return (
(s.primitive && consolidatableTypes.includes(s.type)) ||
(s.decodedType !== undefined && consolidatableTypes.includes(s.decodedType))
);
};
/**
* We need to check three things:
* 1. All the schemas satisfy isConsolidatableType
* 2. All the schemas have the same decodedType type (aka type at runtime, or the `A` type of the codec)
* 3. At least one of the schemas is a primitive type
*
* If all these conditions are satisfied, we can prove to ourselves that this is a union that
* we can consolidate to the decodedType (runtime) type.
*/
const allConsolidatable = innerSchemas.every(isConsolidatableType);
const hasPrimitive = innerSchemas.some((s: Schema) => s.primitive);
const innerSchemaTypes = new Set(innerSchemas.map((s) => s.decodedType || s.type));
const areSameRuntimeType = innerSchemaTypes.size === 1;
if (allConsolidatable && areSameRuntimeType && hasPrimitive) {
return { type: Array.from(innerSchemaTypes)[0] as Primitive['type'] };
} else {
return schema;
}
}
function mergeUnions(schema: Schema): Schema {
if (schema.type !== 'union') return schema;
if (schema.schemas.length === 1) return schema.schemas[0]!;
if (schema.schemas.length === 0) return { type: 'undefined' };
// Stringified schemas (i.e. hashes of the schemas) to avoid duplicates
const resultingSchemas: Set<string> = new Set();
// Function to make the result of JSON.stringify deterministic (i.e. keys are all sorted alphabetically)
const sortObj = (obj: object): object =>
obj === null || typeof obj !== 'object'
? obj
: Array.isArray(obj)
? obj.map(sortObj)
: Object.assign(
{},
...Object.entries(obj)
.sort(([keyA], [keyB]) => keyA.localeCompare(keyB))
.map(([k, v]) => ({ [k]: sortObj(v) })),
);
// Deterministic version of JSON.stringify
const deterministicStringify = (obj: object) => JSON.stringify(sortObj(obj));
schema.schemas.forEach((innerSchema) => {
if (innerSchema.type === 'union') {
const merged = mergeUnions(innerSchema);
resultingSchemas.add(deterministicStringify(merged));
} else {
resultingSchemas.add(deterministicStringify(innerSchema));
}
});
if (resultingSchemas.size === 1) return JSON.parse(Array.from(resultingSchemas)[0]!);
return {
type: 'union',
schemas: Array.from(resultingSchemas)
.filter((s) => s != undefined)
.map((s) => JSON.parse(s)),
};
}
export function simplifyUnion(schema: Schema, optimize: OptimizeFn): Schema {
if (schema.type !== 'union') return schema;
if (schema.schemas.length === 1) return schema.schemas[0]!;
if (schema.schemas.length === 0) return { type: 'undefined' };
const innerSchemas = schema.schemas.map(optimize);
const literals: Record<Primitive['type'], Set<any>> = {
string: new Set(),
number: new Set(),
integer: new Set(),
boolean: new Set(),
null: new Set(),
};
const remainder: Schema[] = [];
innerSchemas.forEach((innerSchema) => {
if (isPrimitive(innerSchema) && innerSchema.enum !== undefined) {
if (
innerSchema.comment ||
innerSchema.enumDescriptions ||
innerSchema.enumsDeprecated
) {
remainder.push(innerSchema);
} else {
innerSchema.enum.forEach((value) => {
literals[innerSchema.type].add(value);
});
}
} else {
remainder.push(innerSchema);
}
});
const result: Schema = {
type: 'union',
schemas: remainder,
};
for (const [key, value] of Object.entries(literals)) {
if (value.size > 0) {
result.schemas.push({ type: key as any, enum: Array.from(value) });
}
}
if (result.schemas.length === 1) {
return result.schemas[0]!;
} else {
return result;
}
}
export function filterUndefinedUnion(schema: Schema): [boolean, Schema] {
if (schema.type !== 'union') {
return [false, schema];
}
const undefinedIndex = schema.schemas.findIndex((s) => s.type === 'undefined');
if (undefinedIndex < 0) {
return [false, schema];
}
const schemas = schema.schemas.filter((s) => s.type !== 'undefined');
if (schemas.length === 0) {
return [true, { type: 'undefined' }];
} else if (schemas.length === 1) {
return [true, withComment(schemas[0]!, schema)];
} else {
return [true, withComment({ type: 'union', schemas }, schema)];
}
}
// This function is a helper that adds back any comments that were removed during optimization
function withComment(newSchema: Schema, oldSchema: Schema): Schema {
if (oldSchema.comment) {
newSchema.comment = combineComments(oldSchema);
}
return newSchema;
}
export function optimize(schema: Schema): Schema {
if (schema.type === 'object') {
const properties: Record<string, Schema> = {};
const required: string[] = [];
for (const [key, prop] of Object.entries(schema.properties)) {
const optimized = optimize(prop);
if (optimized.type === 'undefined') {
continue;
}
const [isOptional, filteredSchema] = filterUndefinedUnion(optimized);
properties[key] = filteredSchema;
if (schema.required.indexOf(key) >= 0 && !isOptional) {
required.push(key);
}
}
const schemaObject: Schema = { type: 'object', properties, required };
return withComment(schemaObject, schema);
} else if (schema.type === 'intersection') {
const newSchema = foldIntersection(schema, optimize);
return withComment(newSchema, schema);
} else if (schema.type === 'union') {
const consolidated = consolidateUnion(schema);
const simplified = simplifyUnion(consolidated, optimize);
const merged = mergeUnions(simplified);
return withComment(merged, schema);
} else if (schema.type === 'array') {
const optimized = optimize(schema.items);
return withComment({ type: 'array', items: optimized }, schema);
} else if (schema.type === 'record') {
return withComment(
{
type: 'record',
...(schema.domain ? { domain: optimize(schema.domain) } : {}),
codomain: optimize(schema.codomain),
},
schema,
);
} else if (schema.type === 'tuple') {
const schemas = schema.schemas.map(optimize);
return withComment({ type: 'tuple', schemas }, schema);
} else if (schema.type === 'ref') {
return schema;
} else {
return schema;
}
}