From 5f9bbe011a18ccac08a70340f6d20e60ce30c4a4 Mon Sep 17 00:00:00 2001 From: "Craig Macomber (Microsoft)" <42876482+CraigMacomber@users.noreply.github.com> Date: Thu, 3 Oct 2024 15:36:04 -0700 Subject: [PATCH] tree: Promote schemaCreationUtilities to alpha (#20035) ## Description Promote schemaCreationUtilities to alpha, and a bit of polish for their API and docs. --- .changeset/witty-suits-worry.md | 10 ++ .../dds/tree/api-report/tree.alpha.api.md | 28 ++++++ packages/dds/tree/src/index.ts | 3 +- .../dds/tree/src/simple-tree/api/index.ts | 1 - .../api/schemaCreationUtilities.ts | 91 +++++++++++-------- packages/dds/tree/src/simple-tree/index.ts | 1 - .../api/schemaCreationUtilities.spec.ts | 36 +++++++- .../api-report/fluid-framework.alpha.api.md | 28 ++++++ 8 files changed, 155 insertions(+), 43 deletions(-) create mode 100644 .changeset/witty-suits-worry.md diff --git a/.changeset/witty-suits-worry.md b/.changeset/witty-suits-worry.md new file mode 100644 index 000000000000..9cbd49e1fce0 --- /dev/null +++ b/.changeset/witty-suits-worry.md @@ -0,0 +1,10 @@ +--- +"fluid-framework": minor +"@fluidframework/tree": minor +--- + +Expose experimental alpha APIs for producing schema from enums + +`adaptEnum` and `enumFromStrings` have been added to `@fluidframework/tree/alpha` and `fluid-framework/alpha`. +These unstable alpha APIs are relatively simple helpers on-top of public APIs (source: [schemaCreationUtilities.ts](https://github.com/microsoft/FluidFramework/blob/main/packages/dds/tree/src/simple-tree/schemaCreationUtilities.ts)): +thus if these change or stable alternatives are needed, an application can replicate this functionality using these implementations as an example. diff --git a/packages/dds/tree/api-report/tree.alpha.api.md b/packages/dds/tree/api-report/tree.alpha.api.md index 4343c34a067b..e364c2585f87 100644 --- a/packages/dds/tree/api-report/tree.alpha.api.md +++ b/packages/dds/tree/api-report/tree.alpha.api.md @@ -4,6 +4,15 @@ ```ts +// @alpha +export function adaptEnum>(factory: SchemaFactory, members: TEnum): ((value: TValue) => TreeNode & { + readonly value: TValue; +}) & { readonly [Property in keyof TEnum]: TreeNodeSchemaClass, NodeKind.Object, TreeNode & { + readonly value: TEnum[Property]; + }, never, true, unknown> & (new () => TreeNode & { + readonly value: TEnum[Property]; + }); }; + // @public export type AllowedTypes = readonly LazyItem[]; @@ -31,6 +40,15 @@ export interface CommitMetadata { interface DefaultProvider extends ErasedType<"@fluidframework/tree.FieldProvider"> { } +// @alpha +export function enumFromStrings(factory: SchemaFactory, members: readonly Members[]): ((value: TValue) => TreeNode & { + readonly value: TValue; +}) & Record, NodeKind.Object, TreeNode & { + readonly value: Members; +}, never, true, unknown> & (new () => TreeNode & { + readonly value: Members; +})>; + // @public type ExtractItemType = Item extends () => infer Result ? Result : Item; @@ -495,6 +513,13 @@ export type SharedTreeFormatVersion = typeof SharedTreeFormatVersion; // @alpha export type SharedTreeOptions = Partial & Partial & ForestOptions; +// @alpha +export function singletonSchema(factory: SchemaFactory, name: TName): TreeNodeSchemaClass, NodeKind.Object, TreeNode & { + readonly value: TName; +}, never, true, unknown> & (new () => TreeNode & { + readonly value: TName; +}); + // @public export type TransactionConstraint = NodeInDocumentConstraint; @@ -699,6 +724,9 @@ export interface TreeViewEvents { // @alpha export const typeboxValidator: JsonValidator; +// @alpha +export function typedObjectValues(object: Record): TValues[]; + // @public @deprecated const typeNameSymbol: unique symbol; diff --git a/packages/dds/tree/src/index.ts b/packages/dds/tree/src/index.ts index bb30dbfa54c6..fbed5d792e22 100644 --- a/packages/dds/tree/src/index.ts +++ b/packages/dds/tree/src/index.ts @@ -120,12 +120,11 @@ export { // Recursive Schema APIs type ValidateRecursiveSchema, type FixRecursiveArraySchema, - // experimental @internal APIs: + // experimental @alpha APIs: adaptEnum, enumFromStrings, singletonSchema, typedObjectValues, - type EmptyObject, // test recursive schema for checking that d.ts files handles schema correctly test_RecursiveObject, test_RecursiveObject_base, diff --git a/packages/dds/tree/src/simple-tree/api/index.ts b/packages/dds/tree/src/simple-tree/api/index.ts index a8a91e24e638..abb4113df387 100644 --- a/packages/dds/tree/src/simple-tree/api/index.ts +++ b/packages/dds/tree/src/simple-tree/api/index.ts @@ -23,7 +23,6 @@ export { enumFromStrings, singletonSchema, typedObjectValues, - type EmptyObject, } from "./schemaCreationUtilities.js"; export { treeNodeApi, type TreeNodeApi } from "./treeNodeApi.js"; export { createFromInsertable, cursorFromInsertable } from "./create.js"; diff --git a/packages/dds/tree/src/simple-tree/api/schemaCreationUtilities.ts b/packages/dds/tree/src/simple-tree/api/schemaCreationUtilities.ts index c3b116cff5eb..6c5648317233 100644 --- a/packages/dds/tree/src/simple-tree/api/schemaCreationUtilities.ts +++ b/packages/dds/tree/src/simple-tree/api/schemaCreationUtilities.ts @@ -9,27 +9,29 @@ import { fail } from "../../util/index.js"; import type { SchemaFactory, ScopedSchemaName } from "./schemaFactory.js"; import type { NodeFromSchema } from "../schemaTypes.js"; -import type { NodeKind, TreeNodeSchemaClass, TreeNode } from "../core/index.js"; import type { - InsertableObjectFromSchemaRecord, - ObjectFromSchemaRecord, -} from "../objectNode.js"; - -/** - * Empty Object for use in type computations that should contribute no fields when `&`ed with another type. - * @internal + InternalTreeNode, + NodeKind, + TreeNode, + TreeNodeSchemaClass, +} from "../core/index.js"; + +/* + * This file does two things: + * + * 1. Provides tools for making schema for cases like enums. + * + * 2. Demonstrates the kinds of schema utilities apps can write. + * Nothing in here needs access to package internal APIs. */ -// Using {} instead of interface {} or Record for empty object here produces better IntelliSense in the generated types than `Record` recommended by the linter. -// Making this a type instead of an interface prevents it from showing up in IntelliSense, and also avoids breaking the typing somehow. -// eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/consistent-type-definitions -export type EmptyObject = {}; /** * Create a schema for a node with no state. * @remarks * This is commonly used in unions when the only information needed is which kind of node the value is. * Enums are a common example of this pattern. - * @internal + * @see {@link adaptEnum} + * @alpha */ // Return type is intentionally derived. // eslint-disable-next-line @typescript-eslint/explicit-function-return-type @@ -38,7 +40,7 @@ export function singletonSchema & { readonly value: TName }; + type NodeType = TreeNode & { readonly value: TName }; // Returning SingletonSchema without a type conversion results in TypeScript generating something like `readonly "__#124291@#brand": unknown;` // for the private brand field of TreeNode. @@ -59,7 +59,7 @@ export function singletonSchema, NodeKind.Object, NodeType, - object & InsertableObjectFromSchemaRecord, + never, true > & (new () => NodeType) = SingletonSchema; @@ -71,33 +71,50 @@ export function singletonSchema; - * const nodeFromString: ModeNodes = ModeNodes(Mode.a); - * const nodeFromSchema: ModeNodes = new ModeNodes.a(); - * const nameFromNode: Mode = nodeFromSchema.value; + * // An example schema which has an enum as a child. * class Parent extends schemaFactory.object("Parent", { + * // typedObjectValues extracts a list of all the fields of ModeNodes, which are the schema for each enum member. + * // This means any member of the enum is allowed in this field. * mode: typedObjectValues(ModeNodes), * }) {} + * + * // Example usage of enum-based nodes, showing what type to use and that `.value` can be used to read out the enum value. + * function getValue(node: ModeNodes): Mode { + * return node.value; + * } + * + * // Example constructing a tree containing an enum node from an enum value. + * // The syntax `new ModeNodes.a()` is also supported. + * function setValue(node: Parent): void { + * node.mode = ModeNodes(Mode.a); + * } * ``` * @privateRemarks * TODO: - * Extends this to support numeric enums. - * Maybe require an explicit nested scope to group them under, or at least a warning about collisions. - * Maybe just provide `SchemaFactory.nested` to east creating nested scopes? - * @internal + * Extend this to support numeric enums. + * Maybe provide `SchemaFactory.nested` to ease creating nested scopes? + * @see {@link enumFromStrings} for a similar function that works on arrays of strings instead of an enum. + * @alpha */ // eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export function adaptEnum>( - factory: SchemaFactory, - members: TEnum, -) { +export function adaptEnum< + TScope extends string, + const TEnum extends Record, +>(factory: SchemaFactory, members: TEnum) { type Values = TEnum[keyof TEnum]; const values = Object.values(members) as Values[]; const inverse = new Map(Object.entries(members).map(([key, value]) => [value, key])) as Map< @@ -116,7 +133,7 @@ export function adaptEnum(value: TValue) => { - return new out[inverse.get(value) ?? fail("missing enum value")]({}) as NodeFromSchema< + return new out[inverse.get(value) ?? fail("missing enum value")]() as NodeFromSchema< ReturnType> >; }; @@ -135,7 +152,9 @@ export function adaptEnum( object: Record, @@ -160,7 +179,8 @@ export function typedObjectValues( * * class Parent extends schemaFactory.object("Parent", { mode: typedObjectValues(Mode) }) {} * ``` - * @internal + * @see {@link adaptEnum} for a similar function that works on enums instead of arrays of strings. + * @alpha */ // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export function enumFromStrings( @@ -175,7 +195,7 @@ export function enumFromStrings>>; // eslint-disable-next-line @typescript-eslint/explicit-function-return-type const factoryOut = (value: TValue) => { - return new out[value]({}) as NodeFromSchema< + return new out[value]() as NodeFromSchema< ReturnType> >; }; @@ -192,8 +212,7 @@ export function enumFromStrings( diff --git a/packages/dds/tree/src/simple-tree/index.ts b/packages/dds/tree/src/simple-tree/index.ts index dc4f0dcfb38d..a88a4efc461a 100644 --- a/packages/dds/tree/src/simple-tree/index.ts +++ b/packages/dds/tree/src/simple-tree/index.ts @@ -41,7 +41,6 @@ export { enumFromStrings, singletonSchema, typedObjectValues, - type EmptyObject, test_RecursiveObject, test_RecursiveObject_base, test_RecursiveObjectPojoMode, diff --git a/packages/dds/tree/src/test/simple-tree/api/schemaCreationUtilities.spec.ts b/packages/dds/tree/src/test/simple-tree/api/schemaCreationUtilities.spec.ts index b1d8476796c4..9cdee9488473 100644 --- a/packages/dds/tree/src/test/simple-tree/api/schemaCreationUtilities.spec.ts +++ b/packages/dds/tree/src/test/simple-tree/api/schemaCreationUtilities.spec.ts @@ -39,7 +39,7 @@ describe("schemaCreationUtilities", () => { const view: TreeView = tree.viewWith(config); view.initialize( new Parent({ - mode: new Mode.Bonus({}), + mode: new Mode.Bonus(), }), ); const mode = view.root.mode; @@ -74,6 +74,36 @@ describe("schemaCreationUtilities", () => { class Parent extends schemaFactory.object("Parent", { mode: typedObjectValues(Mode) }) {} }); + it("adaptEnum example from docs", () => { + const schemaFactory = new SchemaFactory("com.myApp"); + // An enum for use in the tree. Must have string keys. + enum Mode { + a = "A", + b = "B", + } + // Define the schema for each member of the enum using a nested scope to group them together. + const ModeNodes = adaptEnum(new SchemaFactory(`${schemaFactory.scope}.Mode`), Mode); + // Defined the types of the nodes which correspond to this the schema. + type ModeNodes = NodeFromSchema<(typeof ModeNodes)[keyof typeof ModeNodes]>; + // An example schema which has an enum as a child. + class Parent extends schemaFactory.object("Parent", { + // typedObjectValues extracts a list of all the fields of ModeNodes, which are the schema for each enum member. + // This means any member of the enum is allowed in this field. + mode: typedObjectValues(ModeNodes), + }) {} + + // Example usage of enum based nodes, showing what type to use and that `.value` can be used to read out the enum value. + function getValue(node: ModeNodes): Mode { + return node.value; + } + + // Example constructing a tree containing an enum node from an enum value. + // The syntax `new ModeNodes.a()` is also supported. + function setValue(node: Parent): void { + node.mode = ModeNodes(Mode.a); + } + }); + it("adaptEnum example", () => { const schemaFactory = new SchemaFactory("x"); @@ -112,7 +142,7 @@ describe("schemaCreationUtilities", () => { const view: TreeView = tree.viewWith(config); view.initialize( new Parent({ - mode: new Mode.Bonus({}), + mode: new Mode.Bonus(), }), ); const mode = view.root.mode; @@ -185,7 +215,7 @@ describe("schemaCreationUtilities", () => { // Can convert enum to unhydrated node: const x = DayNodes(Day.Today); // Can construct unhydrated node from enum's key: - const y = new DayNodes.Today({}); + const y = new DayNodes.Today(); const view = tree.viewWith( new TreeViewConfiguration({ schema: typedObjectValues(DayNodes) }), diff --git a/packages/framework/fluid-framework/api-report/fluid-framework.alpha.api.md b/packages/framework/fluid-framework/api-report/fluid-framework.alpha.api.md index 402f4ffee00c..bd0714ca3b1b 100644 --- a/packages/framework/fluid-framework/api-report/fluid-framework.alpha.api.md +++ b/packages/framework/fluid-framework/api-report/fluid-framework.alpha.api.md @@ -4,6 +4,15 @@ ```ts +// @alpha +export function adaptEnum>(factory: SchemaFactory, members: TEnum): ((value: TValue) => TreeNode & { + readonly value: TValue; +}) & { readonly [Property in keyof TEnum]: TreeNodeSchemaClass, NodeKind.Object, TreeNode & { + readonly value: TEnum[Property]; + }, never, true, unknown> & (new () => TreeNode & { + readonly value: TEnum[Property]; + }); }; + // @public export type AllowedTypes = readonly LazyItem[]; @@ -69,6 +78,15 @@ export interface ContainerSchema { interface DefaultProvider extends ErasedType<"@fluidframework/tree.FieldProvider"> { } +// @alpha +export function enumFromStrings(factory: SchemaFactory, members: readonly Members[]): ((value: TValue) => TreeNode & { + readonly value: TValue; +}) & Record, NodeKind.Object, TreeNode & { + readonly value: Members; +}, never, true, unknown> & (new () => TreeNode & { + readonly value: Members; +})>; + // @public @sealed export abstract class ErasedType { static [Symbol.hasInstance](value: never): value is never; @@ -856,6 +874,13 @@ export type SharedTreeFormatVersion = typeof SharedTreeFormatVersion; // @alpha export type SharedTreeOptions = Partial & Partial & ForestOptions; +// @alpha +export function singletonSchema(factory: SchemaFactory, name: TName): TreeNodeSchemaClass, NodeKind.Object, TreeNode & { + readonly value: TName; +}, never, true, unknown> & (new () => TreeNode & { + readonly value: TName; +}); + // @public export interface Tagged { // (undocumented) @@ -1074,6 +1099,9 @@ export interface TreeViewEvents { // @alpha export const typeboxValidator: JsonValidator; +// @alpha +export function typedObjectValues(object: Record): TValues[]; + // @public @deprecated const typeNameSymbol: unique symbol;