-
Notifications
You must be signed in to change notification settings - Fork 517
/
schemaBuilder.ts
315 lines (293 loc) · 10.6 KB
/
schemaBuilder.ts
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
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/
import { assert } from "@fluidframework/core-utils";
import { ValueSchema } from "../core";
import { Assume, RestrictiveReadonlyRecord, transformObjectMap } from "../util";
import { SchemaBuilderBase } from "./schemaBuilderBase";
import { FieldKinds } from "./default-field-kinds";
import {
AllowedTypes,
TreeSchema,
FieldSchema,
Any,
TypedSchemaCollection,
Unenforced,
} from "./typed-schema";
import { FieldKind } from "./modular-schema";
// TODO: tests and examples for this file
/**
* Builds schema libraries, and the schema within them.
*
* @remarks
* Fields, when inferred from {@link ImplicitFieldSchema}, default to the `Required` {@link FieldKind}.
*
* This type has some built in defaults which impact compatibility.
* This includes which {@link FieldKind}s it uses.
* To ensure that these defaults can be updated without compatibility issues,
* this class is versioned: the number in its name indicates its compatibility,
* and if its defaults are changed to ones that would not be compatible with a version of the application using the previous versions,
* this number will be updated to make it impossible for an app to implicitly do a compatibility breaking change by updating this package.
* Major package version updates are allowed to break API compatibility, but must not break content compatibility unless a corresponding code change is made in the app to opt in.
*
* @privateRemarks
* TODO: Maybe rename to SchemaBuilder1 because of the versioning implications above.
* @sealed @alpha
*/
export class SchemaBuilder<
TScope extends string = string,
TName extends number | string = string,
> extends SchemaBuilderBase<TScope, TName> {
/**
* Define (and add to this library) a {@link TreeSchema} for a {@link Struct} node.
*
* The name must be unique among all TreeSchema in the the document schema.
*/
public struct<
const Name extends TName,
const T extends RestrictiveReadonlyRecord<string, ImplicitFieldSchema>,
>(
name: Name,
t: T,
): TreeSchema<
`${TScope}.${Name}`,
{ structFields: { [key in keyof T]: NormalizeField<T[key], DefaultFieldKind> } }
> {
const schema = new TreeSchema(this, this.scoped(name), {
structFields: transformObjectMap(
t,
(field): FieldSchema => normalizeField(field, DefaultFieldKind),
) as {
[key in keyof T]: NormalizeField<T[key], DefaultFieldKind>;
},
});
this.addNodeSchema(schema);
return schema;
}
/**
* Same as `struct` but with less type safety and works for recursive objects.
* Reduced type safety is a side effect of a workaround for a TypeScript limitation.
*
* See {@link Unenforced} for details.
*
* TODO: Make this work with ImplicitFieldSchema.
*/
public structRecursive<
Name extends TName,
const T extends Unenforced<RestrictiveReadonlyRecord<string, ImplicitFieldSchema>>,
>(name: Name, t: T): TreeSchema<`${TScope}.${Name}`, { structFields: T }> {
return this.struct(
name,
t as unknown as RestrictiveReadonlyRecord<string, ImplicitFieldSchema>,
) as unknown as TreeSchema<`${TScope}.${Name}`, { structFields: T }>;
}
/**
* Define (and add to this library) a {@link TreeSchema} for a {@link MapNode}.
*/
public map<Name extends TName, const T extends ImplicitFieldSchema>(
name: Name,
fieldSchema: T,
): TreeSchema<`${TScope}.${Name}`, { mapFields: NormalizeField<T, DefaultFieldKind> }> {
const schema = new TreeSchema(this, this.scoped(name), {
mapFields: normalizeField(fieldSchema, DefaultFieldKind),
});
this.addNodeSchema(schema);
return schema;
}
/**
* Same as `map` but with less type safety and works for recursive objects.
* Reduced type safety is a side effect of a workaround for a TypeScript limitation.
*
* See {@link Unenforced} for details.
*
* TODO: Make this work with ImplicitFieldSchema.
*/
public mapRecursive<Name extends TName, const T extends Unenforced<ImplicitFieldSchema>>(
name: Name,
t: T,
): TreeSchema<`${TScope}.${Name}`, { mapFields: T }> {
return this.map(name, t as unknown as ImplicitFieldSchema) as unknown as TreeSchema<
`${TScope}.${Name}`,
{ mapFields: T }
>;
}
/**
* Define (and add to this library) a {@link TreeSchema} for a {@link FieldNode}.
*
* The name must be unique among all TreeSchema in the the document schema.
*
* @privateRemarks
* TODO: Write and link document outlining field vs node data model and the separation of concerns related to that.
* TODO: Maybe find a better name for this.
*/
public fieldNode<Name extends TName, const T extends ImplicitFieldSchema>(
name: Name,
fieldSchema: T,
): TreeSchema<
`${TScope}.${Name}`,
{ structFields: { [""]: NormalizeField<T, DefaultFieldKind> } }
> {
const schema = new TreeSchema(this, this.scoped(name), {
structFields: { [""]: normalizeField(fieldSchema, DefaultFieldKind) },
});
this.addNodeSchema(schema);
return schema;
}
/**
* Same as `fieldNode` but with less type safety and works for recursive objects.
* Reduced type safety is a side effect of a workaround for a TypeScript limitation.
*
* See {@link Unenforced} for details.
*
* TODO: Make this work with ImplicitFieldSchema.
*/
public fieldNodeRecursive<Name extends TName, const T extends Unenforced<ImplicitFieldSchema>>(
name: Name,
t: T,
): TreeSchema<`${TScope}.${Name}`, { structFields: { [""]: T } }> {
return this.fieldNode(name, t as unknown as ImplicitFieldSchema) as unknown as TreeSchema<
`${TScope}.${Name}`,
{ structFields: { [""]: T } }
>;
}
// TODO: move this to SchemaBuilderInternal once usages of it have been replaces with use of the leaf domain.
/**
* Define (and add to this library) a {@link TreeSchema} for a node that wraps a value.
* Such nodes will be implicitly unwrapped to the value in some APIs.
*
* The name must be unique among all TreeSchema in the the document schema.
*
* In addition to the normal properties of all nodes (having a schema for example),
* Leaf nodes only contain a value.
* Leaf nodes cannot have fields.
*
* TODO: Maybe ban undefined from allowed values here.
* TODO: Decide and document how unwrapping works for non-primitive terminals.
*/
public leaf<Name extends TName, const T extends ValueSchema>(
name: Name,
t: T,
): TreeSchema<`${TScope}.${Name}`, { leafValue: T }> {
const schema = new TreeSchema(this, this.scoped(name), { leafValue: t });
this.addNodeSchema(schema);
return schema;
}
/**
* Define a schema for an {@link OptionalField}.
* Shorthand or passing `FieldKinds.optional` to {@link FieldSchema}.
*/
public static fieldOptional<const T extends AllowedTypes>(
...allowedTypes: T
): FieldSchema<typeof FieldKinds.optional, T> {
return FieldSchema.create(FieldKinds.optional, allowedTypes);
}
/**
* Define a schema for a {@link RequiredField}.
* Shorthand or passing `FieldKinds.required` to {@link FieldSchema}.
*
* @privateRemarks
* TODO: Consider adding even shorter syntax where:
* - AllowedTypes can be used as a FieldSchema (Or SchemaBuilder takes a default field kind).
* - A TreeSchema can be used as AllowedTypes in the non-polymorphic case.
*/
public static fieldRequired<const T extends AllowedTypes>(
...allowedTypes: T
): FieldSchema<typeof FieldKinds.required, T> {
return FieldSchema.create(FieldKinds.required, allowedTypes);
}
/**
* Define a schema for a {@link Sequence} field.
*/
public static fieldSequence<const T extends AllowedTypes>(
...t: T
): FieldSchema<typeof FieldKinds.sequence, T> {
return FieldSchema.create(FieldKinds.sequence, t);
}
/**
* Produce a TypedSchemaCollection which captures the content added to this builder, any additional SchemaLibraries that were added to it and a root field.
* Can be used with schematize to provide schema aware access to document content.
*
* @remarks
*
* May only be called once after adding content to builder is complete.
*/
public toDocumentSchema<const TSchema extends ImplicitFieldSchema>(
root: TSchema,
): TypedSchemaCollection<NormalizeField<TSchema, DefaultFieldKind>> {
return this.toDocumentSchemaInternal(normalizeField(root, DefaultFieldKind));
}
}
const DefaultFieldKind = FieldKinds.required;
/**
* Default field kind {@link SchemaBuilder} uses with {@link ImplicitFieldSchema}.
* @alpha
*/
export type DefaultFieldKind = typeof FieldKinds.required;
/**
* Extends {@link SchemaBuilder1} with functionality only used to create built in special libraries.
* @privateRemarks Should not be package exported.
*/
export class SchemaBuilderInternal<
TScope extends `com.fluidframework.${string}`,
> extends SchemaBuilder<TScope> {}
/**
* Normalizes an {@link ImplicitFieldSchema} into a {@link FieldSchema}.
* @alpha
*/
export type NormalizeField<
TSchema extends ImplicitFieldSchema,
TDefault extends FieldKind,
> = TSchema extends FieldSchema
? TSchema
: FieldSchema<TDefault, NormalizeAllowedTypes<Assume<TSchema, ImplicitAllowedTypes>>>;
/**
* Normalizes an {@link ImplicitAllowedTypes} into {@link AllowedTypes}.
* @alpha
*/
export type NormalizeAllowedTypes<TSchema extends ImplicitAllowedTypes> = TSchema extends TreeSchema
? readonly [TSchema]
: TSchema extends Any
? readonly [Any]
: TSchema;
/**
* Normalizes an {@link ImplicitFieldSchema} into a {@link FieldSchema}.
*/
export function normalizeField<TSchema extends ImplicitFieldSchema, TDefault extends FieldKind>(
schema: TSchema,
defaultKind: TDefault,
): NormalizeField<TSchema, TDefault> {
if (schema instanceof FieldSchema) {
return schema as NormalizeField<TSchema, TDefault>;
}
const allowedTypes = normalizeAllowedTypes(schema);
return FieldSchema.create(defaultKind, allowedTypes) as unknown as NormalizeField<
TSchema,
TDefault
>;
}
/**
* Normalizes an {@link ImplicitAllowedTypes} into {@link AllowedTypes}.
*/
export function normalizeAllowedTypes<TSchema extends ImplicitAllowedTypes>(
schema: TSchema,
): NormalizeAllowedTypes<TSchema> {
if (schema === Any) {
return [Any] as unknown as NormalizeAllowedTypes<TSchema>;
}
if (schema instanceof TreeSchema) {
return [schema] as unknown as NormalizeAllowedTypes<TSchema>;
}
assert(Array.isArray(schema), "invalid ImplicitAllowedTypes");
return schema as unknown as NormalizeAllowedTypes<TSchema>;
}
/**
* Type that when combined with a default {@link FieldKind} can be normalized into a {@link FieldSchema}.
* @alpha
*/
export type ImplicitFieldSchema = FieldSchema | ImplicitAllowedTypes;
/**
* Generalized version of AllowedTypes allowing for more concise expressions in some cases.
* @alpha
*/
export type ImplicitAllowedTypes = AllowedTypes | TreeSchema | Any;