Skip to content

Commit

Permalink
tree2: Cleanup creation of field schema (#17699)
Browse files Browse the repository at this point in the history
## Description

Adopts a new pattern for documenting workarounds for
microsoft/TypeScript#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.
  • Loading branch information
CraigMacomber committed Oct 10, 2023
1 parent ce11ba9 commit c9d32d6
Show file tree
Hide file tree
Showing 35 changed files with 238 additions and 202 deletions.
50 changes: 23 additions & 27 deletions api-report/tree2.api.md
Expand Up @@ -681,10 +681,11 @@ interface Fields {
}

// @alpha @sealed
export class FieldSchema<out Kind extends FieldKind = FieldKind, const out Types = AllowedTypes> {
constructor(kind: Kind, allowedTypes: Types);
export class FieldSchema<out Kind extends FieldKind = FieldKind, const out Types extends Unenforced<AllowedTypes> = AllowedTypes> {
// (undocumented)
readonly allowedTypes: Types;
static create<Kind extends FieldKind, const Types extends AllowedTypes>(kind: Kind, allowedTypes: Types): FieldSchema<Kind, Types>;
static createUnsafe<Kind extends FieldKind, const Types>(kind: Kind, allowedTypes: Types): FieldSchema<Kind, Types>;
static readonly empty: FieldSchema<Forbidden, readonly []>;
equals(other: FieldSchema): boolean;
// (undocumented)
Expand Down Expand Up @@ -933,8 +934,6 @@ declare namespace InternalTypedSchemaTypes {
MapSchemaSpecification,
LeafSchemaSpecification,
MapFieldSchema,
RecursiveTreeSchemaSpecification,
RecursiveTreeSchema,
FlexList,
FlexListToNonLazyArray,
ConstantFlexListToNonLazyArray,
Expand Down Expand Up @@ -1457,7 +1456,7 @@ interface NodeKeyField extends TreeField {

// @alpha
export const nodeKeyField: {
__n_id__: FieldSchema<NodeKeyFieldKind, [TreeSchema<"com.fluidframework.nodeKey.NodeKey", {
__n_id__: FieldSchema<NodeKeyFieldKind, readonly [TreeSchema<"com.fluidframework.nodeKey.NodeKey", {
leafValue: ValueSchema.String;
}>]>;
};
Expand Down Expand Up @@ -1667,8 +1666,8 @@ export function recordDependency(dependent: ObservingDependent | undefined, depe
// @alpha (undocumented)
const recursiveStruct: TreeSchema<"Test Recursive Domain.struct", {
structFields: {
recursive: FieldSchema<Optional, [() => TreeSchema<"Test Recursive Domain.struct", any>]>;
number: FieldSchema<Required_2, [TreeSchema<"com.fluidframework.leaf.number", {
readonly recursive: FieldSchema<Optional, readonly [() => TreeSchema<"Test Recursive Domain.struct", any>]>;
readonly number: FieldSchema<Required_2, readonly [TreeSchema<"com.fluidframework.leaf.number", {
leafValue: import("..").ValueSchema.Number;
}>]>;
};
Expand All @@ -1677,19 +1676,13 @@ leafValue: import("..").ValueSchema.Number;
// @alpha (undocumented)
const recursiveStruct2: TreeSchema<"Test Recursive Domain.struct2", {
structFields: {
readonly recursive: FieldSchema<Optional, [() => TreeSchema<"Test Recursive Domain.struct2", any>]>;
readonly number: FieldSchema<Required_2, [TreeSchema<"com.fluidframework.leaf.number", {
readonly recursive: FieldSchema<Optional, readonly [() => TreeSchema<"Test Recursive Domain.struct2", any>]>;
readonly number: FieldSchema<Required_2, readonly [TreeSchema<"com.fluidframework.leaf.number", {
leafValue: import("..").ValueSchema.Number;
}>]>;
};
}>;

// @alpha
type RecursiveTreeSchema = unknown;

// @alpha
type RecursiveTreeSchemaSpecification = unknown;

// @alpha
type _RecursiveTrick = never;

Expand Down Expand Up @@ -1773,37 +1766,37 @@ export { SchemaAware }

// @alpha @sealed
export class SchemaBuilder<TScope extends string = string, TName extends number | string = string> extends SchemaBuilderBase<TScope, TName> {
fieldNode<Name extends TName, T extends ImplicitFieldSchema>(name: Name, fieldSchema: T): TreeSchema<`${TScope}.${Name}`, {
fieldNode<Name extends TName, const T extends ImplicitFieldSchema>(name: Name, fieldSchema: T): TreeSchema<`${TScope}.${Name}`, {
structFields: {
[""]: NormalizeField_2<T, DefaultFieldKind>;
};
}>;
fieldNodeRecursive<Name extends TName, T>(name: Name, t: T): TreeSchema<`${TScope}.${Name}`, {
fieldNodeRecursive<Name extends TName, const T extends Unenforced<ImplicitFieldSchema>>(name: Name, t: T): TreeSchema<`${TScope}.${Name}`, {
structFields: {
[""]: T;
};
}>;
static fieldOptional<T extends AllowedTypes>(...allowedTypes: T): FieldSchema<typeof FieldKinds.optional, T>;
static fieldRequired<T extends AllowedTypes>(...allowedTypes: T): FieldSchema<typeof FieldKinds.required, T>;
static fieldSequence<T extends AllowedTypes>(...t: T): FieldSchema<typeof FieldKinds.sequence, T>;
leaf<Name extends TName, T extends ValueSchema>(name: Name, t: T): TreeSchema<`${TScope}.${Name}`, {
static fieldOptional<const T extends AllowedTypes>(...allowedTypes: T): FieldSchema<typeof FieldKinds.optional, T>;
static fieldRequired<const T extends AllowedTypes>(...allowedTypes: T): FieldSchema<typeof FieldKinds.required, T>;
static fieldSequence<const T extends AllowedTypes>(...t: T): FieldSchema<typeof FieldKinds.sequence, T>;
leaf<Name extends TName, const T extends ValueSchema>(name: Name, t: T): TreeSchema<`${TScope}.${Name}`, {
leafValue: T;
}>;
map<Name extends TName, T extends ImplicitFieldSchema>(name: Name, fieldSchema: T): TreeSchema<`${TScope}.${Name}`, {
map<Name extends TName, const T extends ImplicitFieldSchema>(name: Name, fieldSchema: T): TreeSchema<`${TScope}.${Name}`, {
mapFields: NormalizeField_2<T, DefaultFieldKind>;
}>;
mapRecursive<Name extends TName, T>(name: Name, t: T): TreeSchema<`${TScope}.${Name}`, {
mapRecursive<Name extends TName, const T extends Unenforced<ImplicitFieldSchema>>(name: Name, t: T): TreeSchema<`${TScope}.${Name}`, {
mapFields: T;
}>;
struct<const Name extends TName, const T extends RestrictiveReadonlyRecord<string, ImplicitFieldSchema>>(name: Name, t: T): TreeSchema<`${TScope}.${Name}`, {
structFields: {
[key in keyof T]: NormalizeField_2<T[key], DefaultFieldKind>;
};
}>;
structRecursive<Name extends TName, T>(name: Name, t: T): TreeSchema<`${TScope}.${Name}`, {
structRecursive<Name extends TName, const T extends Unenforced<RestrictiveReadonlyRecord<string, ImplicitFieldSchema>>>(name: Name, t: T): TreeSchema<`${TScope}.${Name}`, {
structFields: T;
}>;
toDocumentSchema<TSchema extends ImplicitFieldSchema>(root: TSchema): TypedSchemaCollection<NormalizeField_2<TSchema, DefaultFieldKind>>;
toDocumentSchema<const TSchema extends ImplicitFieldSchema>(root: TSchema): TypedSchemaCollection<NormalizeField_2<TSchema, DefaultFieldKind>>;
}

// @alpha
Expand All @@ -1817,7 +1810,7 @@ export class SchemaBuilderBase<TScope extends string, TName extends number | str
// (undocumented)
protected addNodeSchema<T extends TreeSchema<string, any>>(schema: T): void;
static field<Kind extends FieldKind, T extends AllowedTypes>(kind: Kind, ...allowedTypes: T): FieldSchema<Kind, T>;
static fieldRecursive<Kind extends FieldKind, T extends FlexList<RecursiveTreeSchema>>(kind: Kind, ...allowedTypes: T): FieldSchema<Kind, T>;
static fieldRecursive<Kind extends FieldKind, T extends FlexList<Unenforced<TreeSchema>>>(kind: Kind, ...allowedTypes: T): FieldSchema<Kind, T>;
finalize(): SchemaLibrary;
readonly name: string;
// (undocumented)
Expand Down Expand Up @@ -2103,7 +2096,7 @@ export interface TreeNode extends Tree<TreeSchema> {
}

// @alpha
export class TreeSchema<Name extends string = string, T extends RecursiveTreeSchemaSpecification = TreeSchemaSpecification> {
export class TreeSchema<Name extends string = string, T extends Unenforced<TreeSchemaSpecification> = TreeSchemaSpecification> {
constructor(builder: Named<string>, name: Name, info: T);
// (undocumented)
readonly builder: Named<string>;
Expand Down Expand Up @@ -2289,6 +2282,9 @@ TName extends infer S & TreeSchemaIdentifier ? S : string
// @alpha
type UnbrandList<T extends unknown[], B> = T extends [infer Head, ...infer Tail] ? [Unbrand<Head, B>, ...UnbrandList<Tail, B>] : [];

// @alpha
export type Unenforced<_DesiredExtendsConstraint> = unknown;

// @alpha
type UntypedApi<Mode extends ApiMode> = {
[ApiMode.Editable]: UntypedTree;
Expand Down
4 changes: 2 additions & 2 deletions experimental/dds/tree2/src/domains/json/jsonDomainSchema.ts
Expand Up @@ -50,15 +50,15 @@ export const jsonRoot = [() => jsonObject, () => jsonArray, ...jsonPrimitives] a
*/
export const jsonObject = builder.mapRecursive(
"Object",
new FieldSchema(FieldKinds.optional, jsonRoot),
FieldSchema.createUnsafe(FieldKinds.optional, jsonRoot),
);

/**
* @alpha
*/
export const jsonArray = builder.fieldNodeRecursive(
"Array",
new FieldSchema(FieldKinds.sequence, jsonRoot),
FieldSchema.createUnsafe(FieldKinds.sequence, jsonRoot),
);

/**
Expand Down
4 changes: 2 additions & 2 deletions experimental/dds/tree2/src/domains/nodeKey/nodeKeySchema.ts
Expand Up @@ -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" });
Expand All @@ -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]),
};

/**
Expand Down
6 changes: 3 additions & 3 deletions experimental/dds/tree2/src/domains/testRecursiveDomain.ts
Expand Up @@ -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";

Expand All @@ -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),
});

Expand All @@ -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),
});

Expand Down
Expand Up @@ -231,7 +231,7 @@ export abstract class LazyTree<TSchema extends TreeSchema = TreeSchema>
// 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();
Expand Down Expand Up @@ -471,7 +471,7 @@ export abstract class LazyStruct<TSchema extends StructSchema>
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",
// );

Expand Down
1 change: 1 addition & 0 deletions experimental/dds/tree2/src/feature-libraries/index.ts
Expand Up @@ -157,6 +157,7 @@ export {
bannedFieldNames,
fieldApiPrefixes,
validateStructFieldName,
Unenforced,
} from "./typed-schema";

export { SchemaBuilderBase, SchemaLibrary } from "./schemaBuilderBase";
Expand Down
49 changes: 28 additions & 21 deletions experimental/dds/tree2/src/feature-libraries/schemaBuilder.ts
Expand Up @@ -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
Expand Down Expand Up @@ -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 extends TName, T>(
name: Name,
t: T,
): TreeSchema<`${TScope}.${Name}`, { structFields: T }> {
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>,
Expand All @@ -83,7 +90,7 @@ export class SchemaBuilder<
/**
* Define (and add to this library) a {@link TreeSchema} for a {@link MapNode}.
*/
public map<Name extends TName, T extends ImplicitFieldSchema>(
public map<Name extends TName, const T extends ImplicitFieldSchema>(
name: Name,
fieldSchema: T,
): TreeSchema<`${TScope}.${Name}`, { mapFields: NormalizeField<T, DefaultFieldKind> }> {
Expand All @@ -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<Name extends TName, T>(
public mapRecursive<Name extends TName, const T extends Unenforced<ImplicitFieldSchema>>(
name: Name,
t: T,
): TreeSchema<`${TScope}.${Name}`, { mapFields: T }> {
Expand All @@ -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<Name extends TName, T extends ImplicitFieldSchema>(
public fieldNode<Name extends TName, const T extends ImplicitFieldSchema>(
name: Name,
fieldSchema: T,
): TreeSchema<
Expand All @@ -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<Name extends TName, T>(
public fieldNodeRecursive<Name extends TName, const T extends Unenforced<ImplicitFieldSchema>>(
name: Name,
t: T,
): TreeSchema<`${TScope}.${Name}`, { structFields: { [""]: T } }> {
Expand All @@ -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<Name extends TName, T extends ValueSchema>(
public leaf<Name extends TName, const T extends ValueSchema>(
name: Name,
t: T,
): TreeSchema<`${TScope}.${Name}`, { leafValue: T }> {
Expand All @@ -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<T extends AllowedTypes>(
public static fieldOptional<const T extends AllowedTypes>(
...allowedTypes: T
): FieldSchema<typeof FieldKinds.optional, T> {
return new FieldSchema(FieldKinds.optional, allowedTypes);
return FieldSchema.create(FieldKinds.optional, allowedTypes);
}

/**
Expand All @@ -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<T extends AllowedTypes>(
public static fieldRequired<const T extends AllowedTypes>(
...allowedTypes: T
): FieldSchema<typeof FieldKinds.required, T> {
return new FieldSchema(FieldKinds.required, allowedTypes);
return FieldSchema.create(FieldKinds.required, allowedTypes);
}

/**
* Define a schema for a {@link Sequence} field.
*/
public static fieldSequence<T extends AllowedTypes>(
public static fieldSequence<const T extends AllowedTypes>(
...t: T
): FieldSchema<typeof FieldKinds.sequence, T> {
return new FieldSchema(FieldKinds.sequence, t);
return FieldSchema.create(FieldKinds.sequence, t);
}

/**
Expand All @@ -218,7 +225,7 @@ export class SchemaBuilder<
*
* May only be called once after adding content to builder is complete.
*/
public toDocumentSchema<TSchema extends ImplicitFieldSchema>(
public toDocumentSchema<const TSchema extends ImplicitFieldSchema>(
root: TSchema,
): TypedSchemaCollection<NormalizeField<TSchema, DefaultFieldKind>> {
return this.toDocumentSchemaInternal(normalizeField(root, DefaultFieldKind));
Expand Down Expand Up @@ -273,7 +280,7 @@ export function normalizeField<TSchema extends ImplicitFieldSchema, TDefault ext
return schema as NormalizeField<TSchema, TDefault>;
}
const allowedTypes = normalizeAllowedTypes(schema);
return new FieldSchema(defaultKind, allowedTypes) as unknown as NormalizeField<
return FieldSchema.create(defaultKind, allowedTypes) as unknown as NormalizeField<
TSchema,
TDefault
>;
Expand Down

0 comments on commit c9d32d6

Please sign in to comment.