From c9d32d61ef0a90f889f070f89f423cb1b1f8ff83 Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Tue, 10 Oct 2023 16:16:18 -0700 Subject: [PATCH] tree2: Cleanup creation of field schema (#17699) ## Description Adopts a new pattern for documenting workarounds for https://github.com/microsoft/TypeScript/issues/55758, making the documentation more centralized, and more clear about what the desired extends clauses are. Also avoids using the constructor for FieldSchema and instead use static builders whose type parameters can be constrained. This, when combined with `const` generic type parameters removes the reason for existence of the generic FieldSchema builders on SchemaBuilder, so those have been removed (the ones which depend on specific field kinds are kept). Also adds some runtime validation in FieldSchema for cases the type system can't fully handle. ## Breaking Changes Users of SchemaBuilder.field (or new FieldSchema) and SchmeaBuilder.fieldRecursive should use FieldSchema.create and FieldSchema.createUnsafe. --- api-report/tree2.api.md | 50 ++++++------ .../src/domains/json/jsonDomainSchema.ts | 4 +- .../src/domains/nodeKey/nodeKeySchema.ts | 4 +- .../tree2/src/domains/testRecursiveDomain.ts | 6 +- .../editable-tree-2/lazyTree.ts | 4 +- .../dds/tree2/src/feature-libraries/index.ts | 1 + .../src/feature-libraries/schemaBuilder.ts | 49 ++++++----- .../feature-libraries/schemaBuilderBase.ts | 8 +- .../feature-libraries/typed-schema/index.ts | 2 +- .../typed-schema/internal.ts | 2 - .../typed-schema/schemaCollection.ts | 2 +- .../typed-schema/typedTreeSchema.ts | 81 ++++++++++++------- experimental/dds/tree2/src/index.ts | 1 + .../contextuallyTyped.spec.ts | 9 ++- .../editable-tree-2/editableTreeTypes.spec.ts | 3 +- .../editable-tree-2/unboxed.spec.ts | 5 +- .../editableTree.editing.spec.ts | 7 +- .../editable-tree/editableTree.spec.ts | 17 ++-- .../editable-tree/mockData.ts | 38 ++++----- .../editable-tree/utilities.spec.ts | 4 +- .../schemaEvolutionExamples.spec.ts | 36 ++++----- .../node-key/nodeKeyIndex.bench.ts | 10 +-- .../node-key/nodeKeyIndex.spec.ts | 5 +- .../schema-aware/schemaAware.spec.ts | 14 ++-- .../schema-aware/schemaComplex.ts | 8 +- .../feature-libraries/schemaBuilder.spec.ts | 16 ++-- .../typedSchema/example.spec.ts | 8 +- .../typedSchema/typedTreeSchema.spec.ts | 3 +- .../dds/tree2/src/test/forestTestSuite.ts | 3 +- .../dds/tree2/src/test/scalableTestTrees.ts | 13 ++- .../shared-tree-core/sharedTreeCore.spec.ts | 3 +- .../test/shared-tree/schematizeTree.spec.ts | 2 +- .../dds/tree2/src/test/snapshots/testTrees.ts | 10 ++- experimental/dds/tree2/src/test/testTrees.ts | 2 +- .../tree-react-api/src/test/schema.ts | 10 +-- 35 files changed, 238 insertions(+), 202 deletions(-) diff --git a/api-report/tree2.api.md b/api-report/tree2.api.md index a29f3d352bb7..41a3ada736c0 100644 --- a/api-report/tree2.api.md +++ b/api-report/tree2.api.md @@ -681,10 +681,11 @@ interface Fields { } // @alpha @sealed -export class FieldSchema { - constructor(kind: Kind, allowedTypes: Types); +export class FieldSchema = AllowedTypes> { // (undocumented) readonly allowedTypes: Types; + static create(kind: Kind, allowedTypes: Types): FieldSchema; + static createUnsafe(kind: Kind, allowedTypes: Types): FieldSchema; static readonly empty: FieldSchema; equals(other: FieldSchema): boolean; // (undocumented) @@ -933,8 +934,6 @@ declare namespace InternalTypedSchemaTypes { MapSchemaSpecification, LeafSchemaSpecification, MapFieldSchema, - RecursiveTreeSchemaSpecification, - RecursiveTreeSchema, FlexList, FlexListToNonLazyArray, ConstantFlexListToNonLazyArray, @@ -1457,7 +1456,7 @@ interface NodeKeyField extends TreeField { // @alpha export const nodeKeyField: { - __n_id__: FieldSchema]>; }; @@ -1667,8 +1666,8 @@ export function recordDependency(dependent: ObservingDependent | undefined, depe // @alpha (undocumented) const recursiveStruct: TreeSchema<"Test Recursive Domain.struct", { structFields: { -recursive: FieldSchema TreeSchema<"Test Recursive Domain.struct", any>]>; -number: FieldSchema TreeSchema<"Test Recursive Domain.struct", any>]>; +readonly number: FieldSchema]>; }; @@ -1677,19 +1676,13 @@ leafValue: import("..").ValueSchema.Number; // @alpha (undocumented) const recursiveStruct2: TreeSchema<"Test Recursive Domain.struct2", { structFields: { -readonly recursive: FieldSchema TreeSchema<"Test Recursive Domain.struct2", any>]>; -readonly number: FieldSchema TreeSchema<"Test Recursive Domain.struct2", any>]>; +readonly number: FieldSchema]>; }; }>; -// @alpha -type RecursiveTreeSchema = unknown; - -// @alpha -type RecursiveTreeSchemaSpecification = unknown; - // @alpha type _RecursiveTrick = never; @@ -1773,26 +1766,26 @@ export { SchemaAware } // @alpha @sealed export class SchemaBuilder extends SchemaBuilderBase { - fieldNode(name: Name, fieldSchema: T): TreeSchema<`${TScope}.${Name}`, { + fieldNode(name: Name, fieldSchema: T): TreeSchema<`${TScope}.${Name}`, { structFields: { [""]: NormalizeField_2; }; }>; - fieldNodeRecursive(name: Name, t: T): TreeSchema<`${TScope}.${Name}`, { + fieldNodeRecursive>(name: Name, t: T): TreeSchema<`${TScope}.${Name}`, { structFields: { [""]: T; }; }>; - static fieldOptional(...allowedTypes: T): FieldSchema; - static fieldRequired(...allowedTypes: T): FieldSchema; - static fieldSequence(...t: T): FieldSchema; - leaf(name: Name, t: T): TreeSchema<`${TScope}.${Name}`, { + static fieldOptional(...allowedTypes: T): FieldSchema; + static fieldRequired(...allowedTypes: T): FieldSchema; + static fieldSequence(...t: T): FieldSchema; + leaf(name: Name, t: T): TreeSchema<`${TScope}.${Name}`, { leafValue: T; }>; - map(name: Name, fieldSchema: T): TreeSchema<`${TScope}.${Name}`, { + map(name: Name, fieldSchema: T): TreeSchema<`${TScope}.${Name}`, { mapFields: NormalizeField_2; }>; - mapRecursive(name: Name, t: T): TreeSchema<`${TScope}.${Name}`, { + mapRecursive>(name: Name, t: T): TreeSchema<`${TScope}.${Name}`, { mapFields: T; }>; struct>(name: Name, t: T): TreeSchema<`${TScope}.${Name}`, { @@ -1800,10 +1793,10 @@ export class SchemaBuilder; }; }>; - structRecursive(name: Name, t: T): TreeSchema<`${TScope}.${Name}`, { + structRecursive>>(name: Name, t: T): TreeSchema<`${TScope}.${Name}`, { structFields: T; }>; - toDocumentSchema(root: TSchema): TypedSchemaCollection>; + toDocumentSchema(root: TSchema): TypedSchemaCollection>; } // @alpha @@ -1817,7 +1810,7 @@ export class SchemaBuilderBase>(schema: T): void; static field(kind: Kind, ...allowedTypes: T): FieldSchema; - static fieldRecursive>(kind: Kind, ...allowedTypes: T): FieldSchema; + static fieldRecursive>>(kind: Kind, ...allowedTypes: T): FieldSchema; finalize(): SchemaLibrary; readonly name: string; // (undocumented) @@ -2103,7 +2096,7 @@ export interface TreeNode extends Tree { } // @alpha -export class TreeSchema { +export class TreeSchema = TreeSchemaSpecification> { constructor(builder: Named, name: Name, info: T); // (undocumented) readonly builder: Named; @@ -2289,6 +2282,9 @@ TName extends infer S & TreeSchemaIdentifier ? S : string // @alpha type UnbrandList = T extends [infer Head, ...infer Tail] ? [Unbrand, ...UnbrandList] : []; +// @alpha +export type Unenforced<_DesiredExtendsConstraint> = unknown; + // @alpha type UntypedApi = { [ApiMode.Editable]: UntypedTree; diff --git a/experimental/dds/tree2/src/domains/json/jsonDomainSchema.ts b/experimental/dds/tree2/src/domains/json/jsonDomainSchema.ts index 676a1cd7d62d..de39bfbc9d6d 100644 --- a/experimental/dds/tree2/src/domains/json/jsonDomainSchema.ts +++ b/experimental/dds/tree2/src/domains/json/jsonDomainSchema.ts @@ -50,7 +50,7 @@ export const jsonRoot = [() => jsonObject, () => jsonArray, ...jsonPrimitives] a */ export const jsonObject = builder.mapRecursive( "Object", - new FieldSchema(FieldKinds.optional, jsonRoot), + FieldSchema.createUnsafe(FieldKinds.optional, jsonRoot), ); /** @@ -58,7 +58,7 @@ export const jsonObject = builder.mapRecursive( */ export const jsonArray = builder.fieldNodeRecursive( "Array", - new FieldSchema(FieldKinds.sequence, jsonRoot), + FieldSchema.createUnsafe(FieldKinds.sequence, jsonRoot), ); /** diff --git a/experimental/dds/tree2/src/domains/nodeKey/nodeKeySchema.ts b/experimental/dds/tree2/src/domains/nodeKey/nodeKeySchema.ts index c708f16431a4..f1e66e360148 100644 --- a/experimental/dds/tree2/src/domains/nodeKey/nodeKeySchema.ts +++ b/experimental/dds/tree2/src/domains/nodeKey/nodeKeySchema.ts @@ -6,11 +6,11 @@ import { assert } from "@fluidframework/core-utils"; import { ValueSchema } from "../../core"; import { - SchemaBuilder, nodeKeyFieldKey, FieldKinds, nodeKeyTreeIdentifier, SchemaBuilderInternal, + FieldSchema, } from "../../feature-libraries"; const builder = new SchemaBuilderInternal({ scope: "com.fluidframework.nodeKey" }); @@ -35,7 +35,7 @@ assert(nodeKeyTreeSchema.name === nodeKeyTreeIdentifier, "mismatched identifiers * @alpha */ export const nodeKeyField = { - [nodeKeyFieldKey]: SchemaBuilder.field(FieldKinds.nodeKey, nodeKeyTreeSchema), + [nodeKeyFieldKey]: FieldSchema.create(FieldKinds.nodeKey, [nodeKeyTreeSchema]), }; /** diff --git a/experimental/dds/tree2/src/domains/testRecursiveDomain.ts b/experimental/dds/tree2/src/domains/testRecursiveDomain.ts index bf9538e1fffc..582fba280967 100644 --- a/experimental/dds/tree2/src/domains/testRecursiveDomain.ts +++ b/experimental/dds/tree2/src/domains/testRecursiveDomain.ts @@ -10,7 +10,7 @@ * Currently we do not have tooling in place to test this in our test suite, and exporting these types here is a temporary crutch to aid in diagnosing this issue. */ -import { AllowedTypes, FieldKinds, SchemaBuilder } from "../feature-libraries"; +import { AllowedTypes, FieldKinds, FieldSchema, SchemaBuilder } from "../feature-libraries"; import { areSafelyAssignable, isAny, requireFalse, requireTrue } from "../util"; import * as leaf from "./leafDomain"; @@ -20,7 +20,7 @@ const builder = new SchemaBuilder({ scope: "Test Recursive Domain", libraries: [ * @alpha */ export const recursiveStruct = builder.structRecursive("struct", { - recursive: SchemaBuilder.fieldRecursive(FieldKinds.optional, () => recursiveStruct), + recursive: FieldSchema.createUnsafe(FieldKinds.optional, [() => recursiveStruct]), number: SchemaBuilder.fieldRequired(leaf.number), }); @@ -34,7 +34,7 @@ fixRecursiveReference(recursiveReference); * @alpha */ export const recursiveStruct2 = builder.struct("struct2", { - recursive: SchemaBuilder.field(FieldKinds.optional, recursiveReference), + recursive: FieldSchema.create(FieldKinds.optional, [recursiveReference]), number: SchemaBuilder.fieldRequired(leaf.number), }); diff --git a/experimental/dds/tree2/src/feature-libraries/editable-tree-2/lazyTree.ts b/experimental/dds/tree2/src/feature-libraries/editable-tree-2/lazyTree.ts index a8031845888d..fb238c095245 100644 --- a/experimental/dds/tree2/src/feature-libraries/editable-tree-2/lazyTree.ts +++ b/experimental/dds/tree2/src/feature-libraries/editable-tree-2/lazyTree.ts @@ -231,7 +231,7 @@ export abstract class LazyTree // Additionally this approach makes it possible for a user to take an EditableTree node, get its parent, check its schema, down cast based on that, then edit that detached field (ex: removing the node in it). // This MIGHT work properly with existing merge resolution logic (it must keep client in sync and be unable to violate schema), but this either needs robust testing or to be explicitly banned (error before s3ending the op). // Issues like replacing a node in the a removed sequenced then undoing the remove could easily violate schema if not everything works exactly right! - fieldSchema = new FieldSchema(FieldKinds.sequence, [Any]); + fieldSchema = FieldSchema.create(FieldKinds.sequence, [Any]); } } else { cursor.exitField(); @@ -471,7 +471,7 @@ export abstract class LazyStruct assert(field instanceof LazyNodeKeyField, "unexpected node key field"); // TODO: ideally we would do something like this, but that adds dependencies we can't have here: // assert( - // field.is(new FieldSchema(FieldKinds.nodeKey, [nodeKeyTreeSchema])), + // field.is(FieldSchema.create(FieldKinds.nodeKey, [nodeKeyTreeSchema])), // "invalid node key field", // ); diff --git a/experimental/dds/tree2/src/feature-libraries/index.ts b/experimental/dds/tree2/src/feature-libraries/index.ts index 0783dc7b651d..a000448eb369 100644 --- a/experimental/dds/tree2/src/feature-libraries/index.ts +++ b/experimental/dds/tree2/src/feature-libraries/index.ts @@ -157,6 +157,7 @@ export { bannedFieldNames, fieldApiPrefixes, validateStructFieldName, + Unenforced, } from "./typed-schema"; export { SchemaBuilderBase, SchemaLibrary } from "./schemaBuilderBase"; diff --git a/experimental/dds/tree2/src/feature-libraries/schemaBuilder.ts b/experimental/dds/tree2/src/feature-libraries/schemaBuilder.ts index 1e6c642792fc..7f620417d268 100644 --- a/experimental/dds/tree2/src/feature-libraries/schemaBuilder.ts +++ b/experimental/dds/tree2/src/feature-libraries/schemaBuilder.ts @@ -8,7 +8,14 @@ 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 } from "./typed-schema"; +import { + AllowedTypes, + TreeSchema, + FieldSchema, + Any, + TypedSchemaCollection, + Unenforced, +} from "./typed-schema"; import { FieldKind } from "./modular-schema"; // TODO: tests and examples for this file @@ -66,14 +73,14 @@ export class SchemaBuilder< * 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 note on {@link InternalTypedSchemaTypes#RecursiveTreeSchema} for details. + * See {@link Unenforced} for details. * * TODO: Make this work with ImplicitFieldSchema. */ - public structRecursive( - name: Name, - t: T, - ): TreeSchema<`${TScope}.${Name}`, { structFields: T }> { + public structRecursive< + Name extends TName, + const T extends Unenforced>, + >(name: Name, t: T): TreeSchema<`${TScope}.${Name}`, { structFields: T }> { return this.struct( name, t as unknown as RestrictiveReadonlyRecord, @@ -83,7 +90,7 @@ export class SchemaBuilder< /** * Define (and add to this library) a {@link TreeSchema} for a {@link MapNode}. */ - public map( + public map( name: Name, fieldSchema: T, ): TreeSchema<`${TScope}.${Name}`, { mapFields: NormalizeField }> { @@ -98,11 +105,11 @@ export class SchemaBuilder< * 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 note on {@link InternalTypedSchemaTypes#RecursiveTreeSchema} for details. + * See {@link Unenforced} for details. * * TODO: Make this work with ImplicitFieldSchema. */ - public mapRecursive( + public mapRecursive>( name: Name, t: T, ): TreeSchema<`${TScope}.${Name}`, { mapFields: T }> { @@ -121,7 +128,7 @@ export class SchemaBuilder< * 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( + public fieldNode( name: Name, fieldSchema: T, ): TreeSchema< @@ -139,11 +146,11 @@ export class SchemaBuilder< * 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 note on {@link InternalTypedSchemaTypes#RecursiveTreeSchema} for details. + * See {@link Unenforced} for details. * * TODO: Make this work with ImplicitFieldSchema. */ - public fieldNodeRecursive( + public fieldNodeRecursive>( name: Name, t: T, ): TreeSchema<`${TScope}.${Name}`, { structFields: { [""]: T } }> { @@ -167,7 +174,7 @@ export class SchemaBuilder< * TODO: Maybe ban undefined from allowed values here. * TODO: Decide and document how unwrapping works for non-primitive terminals. */ - public leaf( + public leaf( name: Name, t: T, ): TreeSchema<`${TScope}.${Name}`, { leafValue: T }> { @@ -180,10 +187,10 @@ export class SchemaBuilder< * Define a schema for an {@link OptionalField}. * Shorthand or passing `FieldKinds.optional` to {@link FieldSchema}. */ - public static fieldOptional( + public static fieldOptional( ...allowedTypes: T ): FieldSchema { - return new FieldSchema(FieldKinds.optional, allowedTypes); + return FieldSchema.create(FieldKinds.optional, allowedTypes); } /** @@ -195,19 +202,19 @@ export class SchemaBuilder< * - 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( + public static fieldRequired( ...allowedTypes: T ): FieldSchema { - return new FieldSchema(FieldKinds.required, allowedTypes); + return FieldSchema.create(FieldKinds.required, allowedTypes); } /** * Define a schema for a {@link Sequence} field. */ - public static fieldSequence( + public static fieldSequence( ...t: T ): FieldSchema { - return new FieldSchema(FieldKinds.sequence, t); + return FieldSchema.create(FieldKinds.sequence, t); } /** @@ -218,7 +225,7 @@ export class SchemaBuilder< * * May only be called once after adding content to builder is complete. */ - public toDocumentSchema( + public toDocumentSchema( root: TSchema, ): TypedSchemaCollection> { return this.toDocumentSchemaInternal(normalizeField(root, DefaultFieldKind)); @@ -273,7 +280,7 @@ export function normalizeField; } const allowedTypes = normalizeAllowedTypes(schema); - return new FieldSchema(defaultKind, allowedTypes) as unknown as NormalizeField< + return FieldSchema.create(defaultKind, allowedTypes) as unknown as NormalizeField< TSchema, TDefault >; diff --git a/experimental/dds/tree2/src/feature-libraries/schemaBuilderBase.ts b/experimental/dds/tree2/src/feature-libraries/schemaBuilderBase.ts index af0658b1c802..e009cab9477a 100644 --- a/experimental/dds/tree2/src/feature-libraries/schemaBuilderBase.ts +++ b/experimental/dds/tree2/src/feature-libraries/schemaBuilderBase.ts @@ -14,8 +14,8 @@ import { TreeSchema, FieldSchema, TypedSchemaCollection, - RecursiveTreeSchema, FlexList, + Unenforced, } from "./typed-schema"; import { FieldKind } from "./modular-schema"; @@ -156,7 +156,7 @@ export class SchemaBuilderBase { - return new FieldSchema(kind, allowedTypes); + return FieldSchema.create(kind, allowedTypes); } /** @@ -172,9 +172,9 @@ export class SchemaBuilderBase, + T extends FlexList>, >(kind: Kind, ...allowedTypes: T): FieldSchema { - return new FieldSchema(kind, allowedTypes); + return FieldSchema.createUnsafe(kind, allowedTypes); } } diff --git a/experimental/dds/tree2/src/feature-libraries/typed-schema/index.ts b/experimental/dds/tree2/src/feature-libraries/typed-schema/index.ts index 9c0073da4605..b1f37cef9260 100644 --- a/experimental/dds/tree2/src/feature-libraries/typed-schema/index.ts +++ b/experimental/dds/tree2/src/feature-libraries/typed-schema/index.ts @@ -19,7 +19,7 @@ export { schemaIsMap, schemaIsStruct, TypedSchemaCollection, - RecursiveTreeSchema, + Unenforced, } from "./typedTreeSchema"; export { ViewSchema } from "./view"; diff --git a/experimental/dds/tree2/src/feature-libraries/typed-schema/internal.ts b/experimental/dds/tree2/src/feature-libraries/typed-schema/internal.ts index 43cde48f3f9a..bfb4365d9ca1 100644 --- a/experimental/dds/tree2/src/feature-libraries/typed-schema/internal.ts +++ b/experimental/dds/tree2/src/feature-libraries/typed-schema/internal.ts @@ -17,8 +17,6 @@ export { MapSchemaSpecification, LeafSchemaSpecification, MapFieldSchema, - RecursiveTreeSchemaSpecification, - RecursiveTreeSchema, } from "./typedTreeSchema"; export { diff --git a/experimental/dds/tree2/src/feature-libraries/typed-schema/schemaCollection.ts b/experimental/dds/tree2/src/feature-libraries/typed-schema/schemaCollection.ts index dc5ce9053cf2..4961170bbae3 100644 --- a/experimental/dds/tree2/src/feature-libraries/typed-schema/schemaCollection.ts +++ b/experimental/dds/tree2/src/feature-libraries/typed-schema/schemaCollection.ts @@ -132,7 +132,7 @@ export function buildViewSchemaCollection( // Thus a library can be used as SchemaData, but if used for full document's SchemaData, // the document will be forced to be empty (due to having an empty root field): // this seems unlikely to cause issues in practice, and results in convenient type compatibility. - rootFieldSchema: rootFieldSchema ?? new FieldSchema(FieldKinds.forbidden, []), + rootFieldSchema: rootFieldSchema ?? FieldSchema.create(FieldKinds.forbidden, []), treeSchema, adapters, policy: defaultSchemaPolicy, diff --git a/experimental/dds/tree2/src/feature-libraries/typed-schema/typedTreeSchema.ts b/experimental/dds/tree2/src/feature-libraries/typed-schema/typedTreeSchema.ts index d3a568bb13b1..ce55d126520e 100644 --- a/experimental/dds/tree2/src/feature-libraries/typed-schema/typedTreeSchema.ts +++ b/experimental/dds/tree2/src/feature-libraries/typed-schema/typedTreeSchema.ts @@ -51,31 +51,17 @@ export type NormalizeStructFields = NormalizeStruc >; /** - * Placeholder for to `TreeSchema` to use in constraints where `TreeSchema` is desired but using it causes - * recursive types to fail to compile due to TypeScript limitations. + * A placeholder to use in extends constraints when using the real type breaks compilation of some recursive types due to [a design limitation of TypeScript](https://github.com/microsoft/TypeScript/issues/55758). * - * Using `TreeSchema` instead in some key "extends" clauses cause recursive types to error with: - * "'theSchema' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer." - * - * TODO: how much more specific of a type can be provided without triggering the above error? + * These extends constraints only serve as documentation: + * to avoid breaking compilation, this type has to not actually enforce anything, and thus is just `unknown`. + * Therefore the type safety is the responsibility of the user of the API. * @alpha */ -export type RecursiveTreeSchema = unknown; - -/** - * Placeholder for to `TreeSchemaSpecification` to use in constraints where `TreeSchemaSpecification` is desired but using it causes - * recursive types to fail to compile due to TypeScript limitations. - * - * See `RecursiveTreeSchema`. - * - * TODO: how much more specific of a type can be provided without triggering the above error? - * @alpha - */ -export type RecursiveTreeSchemaSpecification = unknown; +export type Unenforced<_DesiredExtendsConstraint> = unknown; { - type _check1 = requireAssignableTo; - type _check2 = requireAssignableTo; + type _check = requireAssignableTo>; } /** @@ -86,7 +72,7 @@ export type RecursiveTreeSchemaSpecification = unknown; */ export class TreeSchema< Name extends string = string, - T extends RecursiveTreeSchemaSpecification = TreeSchemaSpecification, + T extends Unenforced = TreeSchemaSpecification, > { // Allows reading fields through the normal map, but without losing type information. public readonly structFields: ObjectToMap< @@ -309,16 +295,43 @@ export type TreeSchemaSpecification = [ * This can include policy for how to use this schema for "view" purposes, and well as how to expose editing APIs. * * @remarks - * `Types` here must extend `AllowedTypes`, but this cannot be enforced with an "extends" clause due to the need to support recursive schema and - * [a design limitation of TypeScript](https://github.com/microsoft/TypeScript/issues/55758). + * `Types` here must extend `AllowedTypes`, but this cannot be enforced with an "extends" clause: see {@link Unenforced} for details. * * @sealed @alpha */ -export class FieldSchema { +export class FieldSchema< + out Kind extends FieldKind = FieldKind, + const out Types extends Unenforced = AllowedTypes, +> { /** * Schema for a field which must always be empty. */ - public static readonly empty = new FieldSchema(FieldKinds.forbidden, []); + public static readonly empty = FieldSchema.create(FieldKinds.forbidden, []); + + /** + * Constructs a FieldSchema. + * @privateRemarks + * Alias for the constructor, but with extends clause for the `Types` parameter that {@link FieldSchema} can not have (due to recursive type issues). + */ + public static create( + kind: Kind, + allowedTypes: Types, + ): FieldSchema { + return new FieldSchema(kind, allowedTypes); + } + + /** + * Constructs a FieldSchema, but missing the extends clause which breaks most recursive types. + * @remarks + * `Types` here must extend `AllowedTypes`, but this cannot be enforced with an "extends" clause: see {@link Unenforced} for details. + * Prefer {@link FieldSchema.create} when possible. + */ + public static createUnsafe( + kind: Kind, + allowedTypes: Types, + ): FieldSchema { + return new FieldSchema(kind, allowedTypes); + } protected _typeCheck?: MakeNominal; @@ -332,10 +345,19 @@ export class FieldSchema allowedTypesToTypeSet(this.allowedTypes as unknown as AllowedTypes), ); @@ -369,7 +391,6 @@ export class FieldSchema TreeSchema)[] = normalizeFlexList(t); - const names = list.map((f) => f().name); + const names = list.map((f) => { + const type = f(); + assert(type instanceof TreeSchema, "invalid allowed type"); + return type.name; + }); return new Set(names); } diff --git a/experimental/dds/tree2/src/index.ts b/experimental/dds/tree2/src/index.ts index 1672d5946fb1..4334eaa1969d 100644 --- a/experimental/dds/tree2/src/index.ts +++ b/experimental/dds/tree2/src/index.ts @@ -273,6 +273,7 @@ export { SchemaBuilderBase, ImplicitFieldSchema, ImplicitAllowedTypes, + Unenforced, } from "./feature-libraries"; export { diff --git a/experimental/dds/tree2/src/test/feature-libraries/contextuallyTyped.spec.ts b/experimental/dds/tree2/src/test/feature-libraries/contextuallyTyped.spec.ts index db9113495b53..399b4e5f7bf7 100644 --- a/experimental/dds/tree2/src/test/feature-libraries/contextuallyTyped.spec.ts +++ b/experimental/dds/tree2/src/test/feature-libraries/contextuallyTyped.spec.ts @@ -17,7 +17,12 @@ import { // Allow importing from this specific file which is being tested: /* eslint-disable-next-line import/no-internal-modules */ } from "../../feature-libraries/contextuallyTyped"; -import { FieldKinds, SchemaBuilder, jsonableTreeFromCursor } from "../../feature-libraries"; +import { + FieldKinds, + FieldSchema, + SchemaBuilder, + jsonableTreeFromCursor, +} from "../../feature-libraries"; describe("ContextuallyTyped", () => { it("isPrimitiveValue", () => { @@ -150,7 +155,7 @@ describe("ContextuallyTyped", () => { const nodeSchema = builder.structRecursive("node", { foo: SchemaBuilder.fieldRequired(generatedSchema), - child: SchemaBuilder.fieldRecursive(FieldKinds.optional, () => nodeSchema), + child: FieldSchema.createUnsafe(FieldKinds.optional, [() => nodeSchema]), }); const nodeSchemaData = builder.toDocumentSchema( diff --git a/experimental/dds/tree2/src/test/feature-libraries/editable-tree-2/editableTreeTypes.spec.ts b/experimental/dds/tree2/src/test/feature-libraries/editable-tree-2/editableTreeTypes.spec.ts index fe6e5bab7b6c..8e633677129e 100644 --- a/experimental/dds/tree2/src/test/feature-libraries/editable-tree-2/editableTreeTypes.spec.ts +++ b/experimental/dds/tree2/src/test/feature-libraries/editable-tree-2/editableTreeTypes.spec.ts @@ -48,6 +48,7 @@ import { SchemaBuilder, StructSchema, TreeSchema, + FieldSchema, } from "../../../feature-libraries"; describe("editableTreeTypes", () => { @@ -100,7 +101,7 @@ describe("editableTreeTypes", () => { /** * Test Recursive Field. */ - foo: SchemaBuilder.fieldRecursive(FieldKinds.optional, () => recursiveStruct), + foo: FieldSchema.createUnsafe(FieldKinds.optional, [() => recursiveStruct]), /** * Data field. */ diff --git a/experimental/dds/tree2/src/test/feature-libraries/editable-tree-2/unboxed.spec.ts b/experimental/dds/tree2/src/test/feature-libraries/editable-tree-2/unboxed.spec.ts index 46764a444550..e79ac3651a58 100644 --- a/experimental/dds/tree2/src/test/feature-libraries/editable-tree-2/unboxed.spec.ts +++ b/experimental/dds/tree2/src/test/feature-libraries/editable-tree-2/unboxed.spec.ts @@ -19,6 +19,7 @@ import { Any, FieldKind, FieldKinds, + FieldSchema, SchemaBuilder, } from "../../../feature-libraries"; import { Context } from "../../../feature-libraries/editable-tree-2/context"; @@ -95,7 +96,7 @@ describe("unboxedField", () => { const builder = new SchemaBuilder({ scope: "test", libraries: [leafDomain.library] }); const structSchema = builder.structRecursive("struct", { name: SchemaBuilder.fieldRequired(leafDomain.string), - child: SchemaBuilder.fieldRecursive(FieldKinds.optional, () => structSchema), + child: FieldSchema.createUnsafe(FieldKinds.optional, [() => structSchema]), }); const fieldSchema = SchemaBuilder.fieldOptional(structSchema); const schema = builder.toDocumentSchema(fieldSchema); @@ -191,7 +192,7 @@ describe("unboxedTree", () => { const builder = new SchemaBuilder({ scope: "test", libraries: [leafDomain.library] }); const structSchema = builder.structRecursive("struct", { name: SchemaBuilder.fieldRequired(leafDomain.string), - child: SchemaBuilder.fieldRecursive(FieldKinds.optional, () => structSchema), + child: FieldSchema.createUnsafe(FieldKinds.optional, [() => structSchema]), }); const rootSchema = SchemaBuilder.fieldOptional(structSchema); const schema = builder.toDocumentSchema(rootSchema); diff --git a/experimental/dds/tree2/src/test/feature-libraries/editable-tree/editableTree.editing.spec.ts b/experimental/dds/tree2/src/test/feature-libraries/editable-tree/editableTree.editing.spec.ts index c841323af92a..60a2d8a0adfb 100644 --- a/experimental/dds/tree2/src/test/feature-libraries/editable-tree/editableTree.editing.spec.ts +++ b/experimental/dds/tree2/src/test/feature-libraries/editable-tree/editableTree.editing.spec.ts @@ -23,6 +23,7 @@ import { EditableTree, treeStatus, TreeStatus, + FieldSchema, } from "../../../feature-libraries"; import { viewWithContent } from "../../utils"; import { @@ -49,10 +50,10 @@ const rootSchemaName: TreeSchemaIdentifier = brand("Test"); function getTestSchema(fieldKind: Kind) { const builder = new SchemaBuilder({ scope: "getTestSchema", libraries: [personSchemaLibrary] }); const rootNodeSchema = builder.struct("Test", { - foo: SchemaBuilder.field(fieldKind, stringSchema), - foo2: SchemaBuilder.field(fieldKind, stringSchema), + foo: FieldSchema.create(fieldKind, [stringSchema]), + foo2: FieldSchema.create(fieldKind, [stringSchema]), }); - return builder.toDocumentSchema(SchemaBuilder.field(FieldKinds.optional, rootNodeSchema)); + return builder.toDocumentSchema(FieldSchema.create(FieldKinds.optional, [rootNodeSchema])); } describe("editable-tree: editing", () => { diff --git a/experimental/dds/tree2/src/test/feature-libraries/editable-tree/editableTree.spec.ts b/experimental/dds/tree2/src/test/feature-libraries/editable-tree/editableTree.spec.ts index 42caf9cf4135..d0e7b9618e7e 100644 --- a/experimental/dds/tree2/src/test/feature-libraries/editable-tree/editableTree.spec.ts +++ b/experimental/dds/tree2/src/test/feature-libraries/editable-tree/editableTree.spec.ts @@ -28,6 +28,7 @@ import { parentField, contextSymbol, SchemaBuilder, + FieldSchema, } from "../../../feature-libraries"; import { @@ -286,7 +287,7 @@ describe("editable-tree: read-only", () => { const emptyOptional = buildTestTree( {}, - SchemaBuilder.field(FieldKinds.required, optionalChildSchema), + FieldSchema.create(FieldKinds.required, [optionalChildSchema]), ).unwrappedRoot; assert(isEditableTree(emptyOptional)); // Check empty field does not show up: @@ -296,7 +297,7 @@ describe("editable-tree: read-only", () => { { child: { [typeNameSymbol]: int32Schema.name, [valueSymbol]: 1 }, }, - SchemaBuilder.field(FieldKinds.required, optionalChildSchema), + FieldSchema.create(FieldKinds.required, [optionalChildSchema]), ).unwrappedRoot; assert(isEditableTree(fullOptional)); // Check full field does show up: @@ -306,7 +307,7 @@ describe("editable-tree: read-only", () => { { [valueSymbol]: 1, }, - SchemaBuilder.field(FieldKinds.required, float64Schema), + FieldSchema.create(FieldKinds.required, [float64Schema]), ).root.content; assert(isEditableTree(hasValue)); // Value does show up when not empty: @@ -314,7 +315,7 @@ describe("editable-tree: read-only", () => { }); it("sequence roots are sequence fields", () => { - const rootSchema = SchemaBuilder.field(FieldKinds.sequence, optionalChildSchema); + const rootSchema = FieldSchema.create(FieldKinds.sequence, [optionalChildSchema]); const schemaData = buildTestSchema(rootSchema); // Test empty { @@ -375,7 +376,7 @@ describe("editable-tree: read-only", () => { }); it("primitives are unwrapped at root", () => { - const rootSchema = SchemaBuilder.field(FieldKinds.required, int32Schema); + const rootSchema = FieldSchema.create(FieldKinds.required, [int32Schema]); const schemaData = buildTestSchema(rootSchema); const forest = setupForest(schemaData, 1); const context = getReadonlyEditableTreeContext(forest, schemaData); @@ -387,9 +388,9 @@ describe("editable-tree: read-only", () => { it("primitives under node are unwrapped, but may be accessed without unwrapping", () => { const builder = new SchemaBuilder({ scope: "test", libraries: [personSchemaLibrary] }); const parentSchema = builder.struct("parent", { - child: SchemaBuilder.field(FieldKinds.required, stringSchema), + child: stringSchema, }); - const rootSchema = SchemaBuilder.field(FieldKinds.required, parentSchema); + const rootSchema = FieldSchema.create(FieldKinds.required, [parentSchema]); const schemaData = builder.toDocumentSchema(rootSchema); const forest = setupForest(schemaData, { child: "x" }); const context = getReadonlyEditableTreeContext(forest, schemaData); @@ -404,7 +405,7 @@ describe("editable-tree: read-only", () => { }); it("array nodes get unwrapped", () => { - const rootSchema = SchemaBuilder.field(FieldKinds.required, phonesSchema); + const rootSchema = FieldSchema.create(FieldKinds.required, [phonesSchema]); assert(getPrimaryField(phonesSchema) !== undefined); const schemaData = buildTestSchema(rootSchema); diff --git a/experimental/dds/tree2/src/test/feature-libraries/editable-tree/mockData.ts b/experimental/dds/tree2/src/test/feature-libraries/editable-tree/mockData.ts index 7f3d86f2e4bf..d9e73cd9d922 100644 --- a/experimental/dds/tree2/src/test/feature-libraries/editable-tree/mockData.ts +++ b/experimental/dds/tree2/src/test/feature-libraries/editable-tree/mockData.ts @@ -50,13 +50,13 @@ export const float64Schema = builder.leaf("Float64", ValueSchema.Number); export const boolSchema = builder.leaf("Bool", ValueSchema.Boolean); export const simplePhonesSchema = builder.struct("Test:SimplePhones-1.0.0", { - [EmptyKey]: SchemaBuilder.field(FieldKinds.sequence, stringSchema), + [EmptyKey]: FieldSchema.create(FieldKinds.sequence, [stringSchema]), }); export const complexPhoneSchema = builder.struct("Test:Phone-1.0.0", { - number: SchemaBuilder.field(FieldKinds.required, stringSchema), - prefix: SchemaBuilder.field(FieldKinds.required, stringSchema), - extraPhones: SchemaBuilder.field(FieldKinds.optional, simplePhonesSchema), + number: stringSchema, + prefix: stringSchema, + extraPhones: FieldSchema.create(FieldKinds.optional, [simplePhonesSchema]), }); export const phonesSchema = builder.fieldNode( @@ -71,26 +71,26 @@ export const phonesSchema = builder.fieldNode( ); export const addressSchema = builder.struct("Test:Address-1.0.0", { - zip: SchemaBuilder.field(FieldKinds.required, stringSchema, int32Schema), - street: SchemaBuilder.field(FieldKinds.optional, stringSchema), - city: SchemaBuilder.field(FieldKinds.optional, stringSchema), - country: SchemaBuilder.field(FieldKinds.optional, stringSchema), - phones: SchemaBuilder.field(FieldKinds.optional, phonesSchema), - sequencePhones: SchemaBuilder.field(FieldKinds.sequence, stringSchema), + zip: [stringSchema, int32Schema], + street: FieldSchema.create(FieldKinds.optional, [stringSchema]), + city: FieldSchema.create(FieldKinds.optional, [stringSchema]), + country: FieldSchema.create(FieldKinds.optional, [stringSchema]), + phones: FieldSchema.create(FieldKinds.optional, [phonesSchema]), + sequencePhones: FieldSchema.create(FieldKinds.sequence, [stringSchema]), }); export const mapStringSchema = builder.map( "Map", - SchemaBuilder.field(FieldKinds.optional, stringSchema), + FieldSchema.create(FieldKinds.optional, [stringSchema]), ); export const personSchema = builder.struct("Test:Person-1.0.0", { - name: SchemaBuilder.field(FieldKinds.required, stringSchema), - age: SchemaBuilder.field(FieldKinds.optional, int32Schema), - adult: SchemaBuilder.field(FieldKinds.optional, boolSchema), - salary: SchemaBuilder.field(FieldKinds.optional, float64Schema, int32Schema, stringSchema), - friends: SchemaBuilder.field(FieldKinds.optional, mapStringSchema), - address: SchemaBuilder.field(FieldKinds.optional, addressSchema), + name: stringSchema, + age: FieldSchema.create(FieldKinds.optional, [int32Schema]), + adult: FieldSchema.create(FieldKinds.optional, [boolSchema]), + salary: FieldSchema.create(FieldKinds.optional, [float64Schema, int32Schema, stringSchema]), + friends: FieldSchema.create(FieldKinds.optional, [mapStringSchema]), + address: FieldSchema.create(FieldKinds.optional, [addressSchema]), }); export const optionalChildSchema = builder.struct("Test:OptionalChild-1.0.0", { @@ -99,10 +99,10 @@ export const optionalChildSchema = builder.struct("Test:OptionalChild-1.0.0", { export const arraySchema = builder.fieldNode( "Test:Array-1.0.0", - SchemaBuilder.field(FieldKinds.sequence, stringSchema, int32Schema), + FieldSchema.create(FieldKinds.sequence, [stringSchema, int32Schema]), ); -export const rootPersonSchema = SchemaBuilder.field(FieldKinds.optional, personSchema); +export const rootPersonSchema = FieldSchema.create(FieldKinds.optional, [personSchema]); export const personSchemaLibrary = builder.finalize(); diff --git a/experimental/dds/tree2/src/test/feature-libraries/editable-tree/utilities.spec.ts b/experimental/dds/tree2/src/test/feature-libraries/editable-tree/utilities.spec.ts index 4dddd8c8e9f9..b224c3972fdb 100644 --- a/experimental/dds/tree2/src/test/feature-libraries/editable-tree/utilities.spec.ts +++ b/experimental/dds/tree2/src/test/feature-libraries/editable-tree/utilities.spec.ts @@ -10,7 +10,7 @@ import { getPrimaryField, getFieldKind, getFieldSchema, - SchemaBuilder, + FieldSchema, } from "../../../feature-libraries"; import { FieldKey, FieldStoredSchema, EmptyKey } from "../../../core"; import { @@ -44,7 +44,7 @@ describe("editable-tree utilities", () => { schema, }; - const rootSchema = SchemaBuilder.field(FieldKinds.required, arraySchema); + const rootSchema = FieldSchema.create(FieldKinds.required, [arraySchema]); const fullSchemaData = buildTestSchema(rootSchema); const primary = getPrimaryField(arraySchema); assert(primary !== undefined); diff --git a/experimental/dds/tree2/src/test/feature-libraries/modular-schema/schemaEvolutionExamples.spec.ts b/experimental/dds/tree2/src/test/feature-libraries/modular-schema/schemaEvolutionExamples.spec.ts index e4e6610ba2d6..1f309a40f7a1 100644 --- a/experimental/dds/tree2/src/test/feature-libraries/modular-schema/schemaEvolutionExamples.spec.ts +++ b/experimental/dds/tree2/src/test/feature-libraries/modular-schema/schemaEvolutionExamples.spec.ts @@ -87,12 +87,12 @@ describe("Schema Evolution Examples", () => { // String made of unicode code points, allowing for sequence editing of a string. const text = contentTypesBuilder.struct("Text", { - children: SchemaBuilder.field(FieldKinds.sequence, codePoint), + children: FieldSchema.create(FieldKinds.sequence, [codePoint]), }); const point = contentTypesBuilder.struct("Point", { - x: SchemaBuilder.field(FieldKinds.required, number), - y: SchemaBuilder.field(FieldKinds.required, number), + x: number, + y: number, }); const defaultContentLibrary = contentTypesBuilder.finalize(); @@ -105,16 +105,16 @@ describe("Schema Evolution Examples", () => { // A type that can be used to position items without an inherent position within the canvas. const positionedCanvasItem = containersBuilder.struct("PositionedCanvasItem", { - position: SchemaBuilder.field(FieldKinds.required, point), - content: SchemaBuilder.field(FieldKinds.required, text), + position: point, + content: text, }); const canvas = containersBuilder.struct("Canvas", { - items: SchemaBuilder.field(FieldKinds.sequence, positionedCanvasItem), + items: FieldSchema.create(FieldKinds.sequence, [positionedCanvasItem]), }); - const root: FieldSchema = SchemaBuilder.field(FieldKinds.required, canvas); + const root: FieldSchema = FieldSchema.create(FieldKinds.required, [canvas]); - const tolerantRoot = SchemaBuilder.field(FieldKinds.optional, canvas); + const tolerantRoot = FieldSchema.create(FieldKinds.optional, [canvas]); const treeViewSchema = containersBuilder.finalize(); @@ -244,20 +244,20 @@ describe("Schema Evolution Examples", () => { }); const counter = builderWithCounter.struct("Counter", { - count: SchemaBuilder.field(FieldKinds.required, number), + count: number, }); // Lets allow counters inside positionedCanvasItem, instead of just text: const positionedCanvasItem2 = builderWithCounter.struct("PositionedCanvasItem", { - position: SchemaBuilder.field(FieldKinds.required, point), - content: SchemaBuilder.field(FieldKinds.required, text, counter), + position: point, + content: [text, counter], }); // And canvas is still the same storage wise, but its view schema references the updated positionedCanvasItem2: const canvas2 = builderWithCounter.struct("Canvas", { - items: SchemaBuilder.field(FieldKinds.sequence, positionedCanvasItem2), + items: FieldSchema.create(FieldKinds.sequence, [positionedCanvasItem2]), }); // Once again we will simulate reloading the app with different schema by modifying the view schema. const viewCollection3: TypedSchemaCollection = builderWithCounter.toDocumentSchema( - SchemaBuilder.field(FieldKinds.optional, canvas2), + FieldSchema.create(FieldKinds.optional, [canvas2]), ); const view3 = new ViewSchema(defaultSchemaPolicy, adapters, viewCollection3); @@ -379,12 +379,12 @@ describe("Schema Evolution Examples", () => { // ); // const builder = new SchemaBuilder("adapters examples", defaultContentLibrary); // const formattedText = builder.structRecursive(formattedTextIdentifier, { - // content: SchemaBuilder.fieldRecursive( + // content: FieldSchema.createUnsafe( // FieldKinds.sequence, // () => formattedText, // codePoint, // ), - // size: SchemaBuilder.field(FieldKinds.required, number), + // size: (number), // }); // // We are also updating positionedCanvasItem to accept the new type. @@ -395,13 +395,13 @@ describe("Schema Evolution Examples", () => { // // as no version of the app need both view schema at the same time // // (except for some approaches for staging roll-outs which are not covered here). // const positionedCanvasItemNew = builder.struct(positionedCanvasItemIdentifier, { - // position: SchemaBuilder.field(FieldKinds.required, point), + // position: (point), // // Note that we are specifically excluding the old text here - // content: SchemaBuilder.field(FieldKinds.required, formattedText), + // content: (formattedText), // }); // // And canvas is still the same storage wise, but its view schema references the updated positionedCanvasItem2: // const canvas2 = builder.struct(canvasIdentifier, { - // items: SchemaBuilder.field(FieldKinds.sequence, positionedCanvasItemNew), + // items: FieldSchema.create(FieldKinds.sequence, positionedCanvasItemNew), // }); // const viewCollection: SchemaCollection = builder.toDocumentSchema( diff --git a/experimental/dds/tree2/src/test/feature-libraries/node-key/nodeKeyIndex.bench.ts b/experimental/dds/tree2/src/test/feature-libraries/node-key/nodeKeyIndex.bench.ts index 2f949f1c25f7..dc9ad969f4ce 100644 --- a/experimental/dds/tree2/src/test/feature-libraries/node-key/nodeKeyIndex.bench.ts +++ b/experimental/dds/tree2/src/test/feature-libraries/node-key/nodeKeyIndex.bench.ts @@ -34,18 +34,16 @@ import { ApiMode } from "../../../feature-libraries/schema-aware"; const builder = new SchemaBuilder("node key index benchmarks", {}, nodeKeySchema); const nodeSchema = builder.struct("node", { - // child: SchemaBuilder.fieldRecursive( + // child: FieldSchema.createUnsafe( // FieldKinds.optional, - // () => nodeSchema, - // () => nodeWithKeySchema, + // [() => nodeSchema, () => nodeWithKeySchema], // ), }); const nodeWithKeySchema = builder.struct("nodeWithKey", { ...nodeKeyField, - // child: SchemaBuilder.fieldRecursive( + // child: FieldSchema.createUnsafe( // FieldKinds.optional, - // () => nodeWithKeySchema, - // () => nodeSchema, + // [() => nodeWithKeySchema, () => nodeSchema], // ), }); const schemaData = builder.intoDocumentSchema( diff --git a/experimental/dds/tree2/src/test/feature-libraries/node-key/nodeKeyIndex.spec.ts b/experimental/dds/tree2/src/test/feature-libraries/node-key/nodeKeyIndex.spec.ts index 6885be46f32f..4a113d2428f6 100644 --- a/experimental/dds/tree2/src/test/feature-libraries/node-key/nodeKeyIndex.spec.ts +++ b/experimental/dds/tree2/src/test/feature-libraries/node-key/nodeKeyIndex.spec.ts @@ -17,6 +17,7 @@ import { TypedField, Any, createMockNodeKeyManager, + FieldSchema, } from "../../../feature-libraries"; // eslint-disable-next-line import/no-internal-modules import { NodeKeys } from "../../../feature-libraries/editable-tree-2/nodeKeys"; @@ -26,7 +27,7 @@ import { AllowedUpdateType } from "../../../core"; const builder = new SchemaBuilder({ scope: "node key index tests", libraries: [nodeKeySchema] }); const nodeSchema = builder.structRecursive("node", { ...nodeKeyField, - child: SchemaBuilder.fieldRecursive(FieldKinds.optional, () => nodeSchema), + child: FieldSchema.createUnsafe(FieldKinds.optional, [() => nodeSchema]), }); const nodeSchemaData = builder.toDocumentSchema(SchemaBuilder.fieldOptional(nodeSchema)); @@ -261,7 +262,7 @@ describe("Node Key Index", () => { libraries: [nodeKeySchema], }); const nodeSchemaNoKey = builder2.structRecursive("node", { - child: SchemaBuilder.fieldRecursive(FieldKinds.optional, () => nodeSchemaNoKey), + child: FieldSchema.createUnsafe(FieldKinds.optional, [() => nodeSchemaNoKey]), }); const nodeSchemaDataNoKey = builder2.toDocumentSchema( SchemaBuilder.fieldOptional(nodeSchemaNoKey), diff --git a/experimental/dds/tree2/src/test/feature-libraries/schema-aware/schemaAware.spec.ts b/experimental/dds/tree2/src/test/feature-libraries/schema-aware/schemaAware.spec.ts index f7929f672df6..5b2564dfce8a 100644 --- a/experimental/dds/tree2/src/test/feature-libraries/schema-aware/schemaAware.spec.ts +++ b/experimental/dds/tree2/src/test/feature-libraries/schema-aware/schemaAware.spec.ts @@ -59,14 +59,14 @@ import { SimpleNodeDataFor } from "./schemaAwareSimple"; // Check the various ways to refer to child types produce the same results { - const numberField1 = SchemaBuilder.field(required, numberSchema); + const numberField1 = FieldSchema.create(required, [numberSchema]); const numberField2 = SchemaBuilder.fieldRequired(numberSchema); - const numberField3 = SchemaBuilder.fieldRecursive(required, numberSchema); + const numberField3 = FieldSchema.createUnsafe(required, [numberSchema]); type check1_ = requireAssignableTo; type check2_ = requireAssignableTo; type check3_ = requireAssignableTo; - const numberFieldLazy = SchemaBuilder.field(required, () => numberSchema); + const numberFieldLazy = FieldSchema.create(required, [() => numberSchema]); type NonLazy = InternalTypedSchemaTypes.FlexListToNonLazyArray< typeof numberFieldLazy.allowedTypes >; @@ -89,7 +89,7 @@ import { SimpleNodeDataFor } from "./schemaAwareSimple"; // Recursive case: const boxSchema = builder.structRecursive("box", { - children: SchemaBuilder.fieldRecursive(sequence, ballSchema, () => boxSchema), + children: FieldSchema.createUnsafe(sequence, [ballSchema, () => boxSchema]), }); { @@ -148,7 +148,7 @@ import { SimpleNodeDataFor } from "./schemaAwareSimple"; // A concrete example for the "x" field: type BallXFieldInfo = typeof ballSchema.structFieldsObject.x; type BallXFieldTypes = BallXFieldInfo["allowedTypes"]; - type check_ = requireAssignableTo; + type check_ = requireAssignableTo; type Child = AllowedTypesToTypedTrees; @@ -285,7 +285,7 @@ import { SimpleNodeDataFor } from "./schemaAwareSimple"; { const builder2 = new SchemaBuilder({ scope: "SchemaAwareRecursiveTest" }); const rec = builder2.structRecursive("rec", { - x: SchemaBuilder.fieldRecursive(optional, () => rec), + x: FieldSchema.createUnsafe(optional, [() => rec]), }); type RecObjectSchema = typeof rec; @@ -464,7 +464,7 @@ describe("SchemaAware Editing", () => { children: SchemaBuilder.fieldSequence(leaf.string), }); const schema = builder.toDocumentSchema( - SchemaBuilder.field(FieldKinds.required, rootNodeSchema), + FieldSchema.create(FieldKinds.required, [rootNodeSchema]), ); const view = createSharedTreeView().schematize({ schema, diff --git a/experimental/dds/tree2/src/test/feature-libraries/schema-aware/schemaComplex.ts b/experimental/dds/tree2/src/test/feature-libraries/schema-aware/schemaComplex.ts index 6633e07bb401..05341bb46be6 100644 --- a/experimental/dds/tree2/src/test/feature-libraries/schema-aware/schemaComplex.ts +++ b/experimental/dds/tree2/src/test/feature-libraries/schema-aware/schemaComplex.ts @@ -6,7 +6,7 @@ /* eslint-disable no-inner-declarations */ import { FieldKinds, ValueSchema, SchemaAware } from "../../../"; -import { SchemaBuilder, TreeSchema } from "../../../feature-libraries"; +import { FieldSchema, SchemaBuilder, TreeSchema } from "../../../feature-libraries"; import { requireAssignableTo } from "../../../util"; const builder = new SchemaBuilder({ scope: "Complex Schema Example" }); @@ -15,11 +15,7 @@ const builder = new SchemaBuilder({ scope: "Complex Schema Example" }); export const stringTaskSchema = builder.leaf("StringTask", ValueSchema.String); // Polymorphic recursive schema: export const listTaskSchema = builder.structRecursive("ListTask", { - items: SchemaBuilder.fieldRecursive( - FieldKinds.sequence, - stringTaskSchema, - () => listTaskSchema, - ), + items: FieldSchema.createUnsafe(FieldKinds.sequence, [stringTaskSchema, () => listTaskSchema]), }); { diff --git a/experimental/dds/tree2/src/test/feature-libraries/schemaBuilder.spec.ts b/experimental/dds/tree2/src/test/feature-libraries/schemaBuilder.spec.ts index 487a21cf9f27..682eafae3200 100644 --- a/experimental/dds/tree2/src/test/feature-libraries/schemaBuilder.spec.ts +++ b/experimental/dds/tree2/src/test/feature-libraries/schemaBuilder.spec.ts @@ -29,7 +29,7 @@ describe("SchemaBuilder", () => { const builder = new SchemaBuilder({ scope: "test" }); const recursiveStruct = builder.structRecursive("recursiveStruct", { - foo: SchemaBuilder.fieldRecursive(FieldKinds.optional, () => recursiveStruct), + foo: FieldSchema.createUnsafe(FieldKinds.optional, [() => recursiveStruct]), }); type _1 = requireTrue< @@ -51,7 +51,7 @@ describe("SchemaBuilder", () => { () => TreeSchema >; const recursiveStruct = builder.struct("recursiveStruct2", { - foo: SchemaBuilder.field(FieldKinds.optional, recursiveReference), + foo: FieldSchema.create(FieldKinds.optional, [recursiveReference]), }); type _0 = requireFalse>; @@ -74,7 +74,7 @@ describe("SchemaBuilder", () => { const recursiveReference = () => recursiveStruct; fixRecursiveReference(recursiveReference); const recursiveStruct = builder.struct("recursiveStruct2", { - foo: SchemaBuilder.field(FieldKinds.optional, recursiveReference), + foo: FieldSchema.create(FieldKinds.optional, [recursiveReference]), }); type _0 = requireFalse>; @@ -130,17 +130,17 @@ describe("SchemaBuilder", () => { it("normalizeField", () => { // Check types are normalized correctly - const directAny = new FieldSchema(FieldKinds.optional, [Any]); + const directAny = FieldSchema.create(FieldKinds.optional, [Any]); assert(directAny.equals(normalizeField(Any, FieldKinds.optional))); assert(directAny.equals(normalizeField([Any], FieldKinds.optional))); assert( directAny.equals( - normalizeField(new FieldSchema(FieldKinds.optional, [Any]), FieldKinds.optional), + normalizeField(FieldSchema.create(FieldKinds.optional, [Any]), FieldKinds.optional), ), ); assert( - new FieldSchema(FieldKinds.optional, []).equals( + FieldSchema.create(FieldKinds.optional, []).equals( normalizeField([], FieldKinds.optional), ), ); @@ -150,14 +150,14 @@ describe("SchemaBuilder", () => { }); assert( - new FieldSchema(FieldKinds.optional, [treeSchema]).equals( + FieldSchema.create(FieldKinds.optional, [treeSchema]).equals( normalizeField([treeSchema], FieldKinds.optional), ), ); // Check provided field kind is used assert( - new FieldSchema(FieldKinds.required, [treeSchema]).equals( + FieldSchema.create(FieldKinds.required, [treeSchema]).equals( normalizeField([treeSchema], FieldKinds.required), ), ); diff --git a/experimental/dds/tree2/src/test/feature-libraries/typedSchema/example.spec.ts b/experimental/dds/tree2/src/test/feature-libraries/typedSchema/example.spec.ts index 17ccf75fcbfe..d8686df2bede 100644 --- a/experimental/dds/tree2/src/test/feature-libraries/typedSchema/example.spec.ts +++ b/experimental/dds/tree2/src/test/feature-libraries/typedSchema/example.spec.ts @@ -4,14 +4,14 @@ */ import { leaf } from "../../../domains"; -import { FieldKinds, SchemaBuilder } from "../../../feature-libraries"; +import { FieldKinds, FieldSchema, SchemaBuilder } from "../../../feature-libraries"; const builder = new SchemaBuilder({ scope: "example", libraries: [leaf.library] }); // Declare struct const ballSchema = builder.struct("Ball", { - x: SchemaBuilder.fieldRequired(leaf.number), - y: SchemaBuilder.fieldRequired(leaf.number), + x: leaf.number, + y: leaf.number, }); // We can inspect the schema. @@ -25,7 +25,7 @@ const invalidChildSchema = ballSchema.structFields.get("z"); // Declare an recursive aggregate type via struct fields. // Note that the type name can be used instead of the schema to allow recursion. const diagramSchema = builder.structRecursive("Diagram", { - children: SchemaBuilder.fieldRecursive(FieldKinds.sequence, () => diagramSchema, ballSchema), + children: FieldSchema.createUnsafe(FieldKinds.sequence, [() => diagramSchema, ballSchema]), }); const rootField = SchemaBuilder.fieldOptional(diagramSchema); diff --git a/experimental/dds/tree2/src/test/feature-libraries/typedSchema/typedTreeSchema.spec.ts b/experimental/dds/tree2/src/test/feature-libraries/typedSchema/typedTreeSchema.spec.ts index 91b74830f1de..f9e5e6a7b9b4 100644 --- a/experimental/dds/tree2/src/test/feature-libraries/typedSchema/typedTreeSchema.spec.ts +++ b/experimental/dds/tree2/src/test/feature-libraries/typedSchema/typedTreeSchema.spec.ts @@ -9,6 +9,7 @@ import { isAssignableTo, requireAssignableTo, requireFalse, requireTrue } from " import { Any, FieldNodeSchema, + FieldSchema, LeafSchema, MapSchema, StructSchema, @@ -28,7 +29,7 @@ describe("typedTreeSchema", () => { // TODO: once schema kinds are separated, test struct with EmptyKey. const recursiveStruct = builder.structRecursive("recursiveStruct", { - foo: SchemaBuilder.fieldRecursive(FieldKinds.optional, () => recursiveStruct), + foo: FieldSchema.createUnsafe(FieldKinds.optional, [() => recursiveStruct]), }); it("schema is", () => { diff --git a/experimental/dds/tree2/src/test/forestTestSuite.ts b/experimental/dds/tree2/src/test/forestTestSuite.ts index 1da02f65564b..4f7a894b5923 100644 --- a/experimental/dds/tree2/src/test/forestTestSuite.ts +++ b/experimental/dds/tree2/src/test/forestTestSuite.ts @@ -43,6 +43,7 @@ import { isNeverField, SchemaBuilder, cursorForTypedTreeData, + FieldSchema, } from "../feature-libraries"; import { MockDependent, applyTestDelta, expectEqualFieldPaths } from "./utils"; import { testGeneralPurposeTreeCursor, testTreeSchema } from "./cursorTestSuite"; @@ -100,7 +101,7 @@ export function testForest(config: ForestTestConfiguration): void { const schema = new InMemoryStoredSchemaRepository(); const forest = factory(schema); - const rootFieldSchema = SchemaBuilder.field(FieldKinds.optional, ...jsonRoot); + const rootFieldSchema = FieldSchema.create(FieldKinds.optional, jsonRoot); schema.update({ ...jsonSchema, rootFieldSchema, diff --git a/experimental/dds/tree2/src/test/scalableTestTrees.ts b/experimental/dds/tree2/src/test/scalableTestTrees.ts index 4cc483801150..d10c2d045fdc 100644 --- a/experimental/dds/tree2/src/test/scalableTestTrees.ts +++ b/experimental/dds/tree2/src/test/scalableTestTrees.ts @@ -5,6 +5,7 @@ import { strict as assert } from "assert"; import { FieldKinds, + FieldSchema, isEditableField, isEditableTree, SchemaAware, @@ -34,7 +35,7 @@ const deepBuilder = new SchemaBuilder({ // Test data in "deep" mode: a linked list with a number at the end. const linkedListSchema = deepBuilder.structRecursive("linkedList", { - foo: SchemaBuilder.fieldRecursive(FieldKinds.required, () => linkedListSchema, jsonNumber), + foo: FieldSchema.createUnsafe(FieldKinds.required, [() => linkedListSchema, jsonNumber]), }); const wideBuilder = new SchemaBuilder({ @@ -44,16 +45,12 @@ const wideBuilder = new SchemaBuilder({ }); export const wideRootSchema = wideBuilder.struct("WideRoot", { - foo: SchemaBuilder.field(FieldKinds.sequence, jsonNumber), + foo: FieldSchema.create(FieldKinds.sequence, [jsonNumber]), }); -export const wideSchema = wideBuilder.toDocumentSchema( - SchemaBuilder.field(FieldKinds.required, wideRootSchema), -); +export const wideSchema = wideBuilder.toDocumentSchema(wideRootSchema); -export const deepSchema = deepBuilder.toDocumentSchema( - SchemaBuilder.field(FieldKinds.required, linkedListSchema, jsonNumber), -); +export const deepSchema = deepBuilder.toDocumentSchema([linkedListSchema, jsonNumber]); /** * JS object like a deep tree. diff --git a/experimental/dds/tree2/src/test/shared-tree-core/sharedTreeCore.spec.ts b/experimental/dds/tree2/src/test/shared-tree-core/sharedTreeCore.spec.ts index c6462e824f7a..02f99a4baed8 100644 --- a/experimental/dds/tree2/src/test/shared-tree-core/sharedTreeCore.spec.ts +++ b/experimental/dds/tree2/src/test/shared-tree-core/sharedTreeCore.spec.ts @@ -32,6 +32,7 @@ import { DefaultChangeset, DefaultEditBuilder, FieldKinds, + FieldSchema, SchemaBuilder, singleTextCursor, typeNameSymbol, @@ -331,7 +332,7 @@ describe("SharedTreeCore", () => { const b = new SchemaBuilder({ scope: "0x4a6 repro", libraries: [leaf.library] }); const node = b.structRecursive("test node", { - child: SchemaBuilder.fieldRecursive(FieldKinds.optional, () => node, leaf.number), + child: FieldSchema.createUnsafe(FieldKinds.optional, [() => node, leaf.number]), }); const schema = b.toDocumentSchema(SchemaBuilder.fieldOptional(node)); diff --git a/experimental/dds/tree2/src/test/shared-tree/schematizeTree.spec.ts b/experimental/dds/tree2/src/test/shared-tree/schematizeTree.spec.ts index 767e8a7e35a3..2c9dc4f8b890 100644 --- a/experimental/dds/tree2/src/test/shared-tree/schematizeTree.spec.ts +++ b/experimental/dds/tree2/src/test/shared-tree/schematizeTree.spec.ts @@ -48,7 +48,7 @@ const emptySchema = new SchemaBuilder({ rejectEmpty: false, rejectForbidden: false, }, -}).toDocumentSchema(SchemaBuilder.field(FieldKinds.forbidden)); +}).toDocumentSchema(FieldSchema.empty); function expectSchema(actual: SchemaData, expected: SchemaData): void { // Check schema match diff --git a/experimental/dds/tree2/src/test/snapshots/testTrees.ts b/experimental/dds/tree2/src/test/snapshots/testTrees.ts index b04ea1dbfb75..f53e86a8cbcd 100644 --- a/experimental/dds/tree2/src/test/snapshots/testTrees.ts +++ b/experimental/dds/tree2/src/test/snapshots/testTrees.ts @@ -7,7 +7,13 @@ import { MockFluidDataStoreRuntime } from "@fluidframework/test-runtime-utils"; import { brand, useDeterministicStableId } from "../../util"; import { AllowedUpdateType, FieldKey, UpPath, rootFieldKey } from "../../core"; import { ISharedTree, ISharedTreeView, SharedTreeFactory } from "../../shared-tree"; -import { Any, FieldKinds, SchemaBuilder, singleTextCursor } from "../../feature-libraries"; +import { + Any, + FieldKinds, + FieldSchema, + SchemaBuilder, + singleTextCursor, +} from "../../feature-libraries"; import { typeboxValidator } from "../../external-utilities"; import { leaf } from "../../domains"; @@ -137,7 +143,7 @@ export function generateTestTrees(): { name: string; tree: () => ISharedTree }[] }); const seqMapSchema = innerBuilder.mapRecursive( "SeqMap", - SchemaBuilder.fieldRecursive(FieldKinds.sequence, () => seqMapSchema), + FieldSchema.createUnsafe(FieldKinds.sequence, [() => seqMapSchema]), ); const docSchema = innerBuilder.toDocumentSchema( SchemaBuilder.fieldSequence(seqMapSchema), diff --git a/experimental/dds/tree2/src/test/testTrees.ts b/experimental/dds/tree2/src/test/testTrees.ts index afe3c8357d33..fb7b6051c7ee 100644 --- a/experimental/dds/tree2/src/test/testTrees.ts +++ b/experimental/dds/tree2/src/test/testTrees.ts @@ -128,7 +128,7 @@ type NumericMapData = SchemaAware.AllowedTypesToTypedTrees< export const anyMap = builder.map("anyMap", SchemaBuilder.fieldSequence(Any)); export const recursiveType = builder.structRecursive("recursiveType", { - field: SchemaBuilder.fieldRecursive(FieldKinds.optional, () => recursiveType), + field: FieldSchema.createUnsafe(FieldKinds.optional, [() => recursiveType]), }); export const library = builder.finalize(); diff --git a/experimental/framework/tree-react-api/src/test/schema.ts b/experimental/framework/tree-react-api/src/test/schema.ts index 5c296629a241..8b55df6d775c 100644 --- a/experimental/framework/tree-react-api/src/test/schema.ts +++ b/experimental/framework/tree-react-api/src/test/schema.ts @@ -3,17 +3,15 @@ * Licensed under the MIT License. */ -import { FieldKinds, SchemaBuilder, TypedField, leaf } from "@fluid-experimental/tree2"; +import { SchemaBuilder, TypedField, leaf } from "@fluid-experimental/tree2"; const builder = new SchemaBuilder({ scope: "tree-react-api", libraries: [leaf.library] }); export const inventory = builder.struct("Contoso:Inventory-1.0.0", { - nuts: SchemaBuilder.field(FieldKinds.required, leaf.number), - bolts: SchemaBuilder.field(FieldKinds.required, leaf.number), + nuts: leaf.number, + bolts: leaf.number, }); -export const inventoryField = SchemaBuilder.field(FieldKinds.required, inventory); - -export const schema = builder.toDocumentSchema(inventoryField); +export const schema = builder.toDocumentSchema(inventory); export type Inventory = TypedField;