Skip to content

Commit

Permalink
procedural data generation for identifiers (#15800)
Browse files Browse the repository at this point in the history
## Description

This PR refactors functions from `contextuallyTyped.ts` to take in a
`context` object containing a `FieldSource` to generate a `FieldGenerator` used to populate missing (but required) fields based on the schema
  • Loading branch information
daesun-park authored Jun 13, 2023
1 parent fce757d commit a51cd2a
Show file tree
Hide file tree
Showing 12 changed files with 249 additions and 36 deletions.
20 changes: 18 additions & 2 deletions api-report/tree2.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,10 +269,10 @@ export interface CursorAdapter<TNode> {
}

// @alpha
export function cursorForTypedTreeData<T extends TreeSchema>(schemaData: SchemaDataAndPolicy, schema: T, data: TypedNode<T, ApiMode.Simple>): ITreeCursorSynchronous;
export function cursorForTypedTreeData<T extends TreeSchema>(context: TreeDataContext, schema: T, data: TypedNode<T, ApiMode.Simple>): ITreeCursorSynchronous;

// @alpha
export function cursorFromContextualData(schemaData: SchemaDataAndPolicy, typeSet: TreeTypeSet, data: ContextuallyTypedNodeData): ITreeCursorSynchronous;
export function cursorFromContextualData(context: TreeDataContext, typeSet: TreeTypeSet, data: ContextuallyTypedNodeData): ITreeCursorSynchronous;

// @alpha (undocumented)
export const enum CursorLocationType {
Expand Down Expand Up @@ -384,6 +384,7 @@ export interface EditableTree extends Iterable<EditableField>, ContextuallyTyped
// @alpha
export interface EditableTreeContext extends ISubscribable<ForestEvents> {
clear(): void;
fieldSource?(key: FieldKey, schema: FieldStoredSchema): undefined | FieldGenerator;
free(): void;
prepareForEdit(): void;
get root(): EditableField;
Expand Down Expand Up @@ -507,6 +508,9 @@ export interface FieldEditor<TChangeset> {
buildChildChange(childIndex: number, change: NodeChangeset): TChangeset;
}

// @alpha
export type FieldGenerator = () => MapTree[];

// @alpha
export type FieldKey = LocalFieldKey | GlobalFieldKeySymbol;

Expand Down Expand Up @@ -1109,6 +1113,12 @@ interface LocalFields {
interface MakeNominal {
}

// @alpha
export interface MapTree extends NodeData {
// (undocumented)
fields: Map<FieldKey, MapTree[]>;
}

// @alpha
type Mark<TTree = ProtoNode> = Skip | Modify<TTree> | Delete<TTree> | MoveOut<TTree> | MoveIn | Insert<TTree>;

Expand Down Expand Up @@ -1663,6 +1673,12 @@ export interface TreeAdapter {
readonly output: TreeSchemaIdentifier;
}

// @alpha
export interface TreeDataContext {
fieldSource?(key: FieldKey, schema: FieldStoredSchema): undefined | FieldGenerator;
readonly schema: SchemaDataAndPolicy;
}

// @alpha (undocumented)
export interface TreeLocation {
// (undocumented)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ export class AppState implements IAppState {
clientsSequence.insertNodes(
clientsSequence.length,
cursorFromContextualData(
clientsSequence.context.schema,
{
schema: clientsSequence.context.schema,
fieldSource: () => undefined,
},
rootAppStateSchema.types,
this.createInitialClientNode(numBubbles),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ export class ClientWrapper implements IClient {
// TODO: better API
bubbles.insertNodes(
bubbles.length,
cursorFromContextualData(bubbles.context.schema, bubbles.fieldSchema.types, bubble),
cursorFromContextualData(
{ schema: bubbles.context.schema, fieldSource: () => undefined },
bubbles.fieldSchema.types,
bubble,
),
);
}

Expand Down
116 changes: 94 additions & 22 deletions experimental/dds/tree2/src/feature-libraries/contextuallyTyped.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
MapTree,
symbolIsFieldKey,
ITreeCursorSynchronous,
symbolFromKey,
} from "../core";
// TODO:
// This module currently is assuming use of defaultFieldKinds.
Expand Down Expand Up @@ -145,27 +146,27 @@ export function getFieldKind(fieldSchema: FieldStoredSchema): FieldKind {
* @returns all allowed child types for `typeSet`.
*/
export function getAllowedTypes(
schema: SchemaDataAndPolicy,
schemaData: SchemaDataAndPolicy,
typeSet: TreeTypeSet,
): ReadonlySet<TreeSchemaIdentifier> {
// TODO: Performance: avoid the `undefined` case being frequent, possibly with caching in the caller of `getPossibleChildTypes`.
return typeSet ?? new Set(schema.treeSchema.keys());
return typeSet ?? new Set(schemaData.treeSchema.keys());
}

/**
* @returns all types, for which the data is schema-compatible.
*/
export function getPossibleTypes(
schemaData: SchemaDataAndPolicy,
context: TreeDataContext,
typeSet: TreeTypeSet,
data: ContextuallyTypedNodeData,
) {
// All types allowed by schema
const allowedTypes = getAllowedTypes(schemaData, typeSet);
const allowedTypes = getAllowedTypes(context.schema, typeSet);

const possibleTypes: TreeSchemaIdentifier[] = [];
for (const allowed of allowedTypes) {
if (shallowCompatibilityTest(schemaData, allowed, data)) {
if (shallowCompatibilityTest(context.schema, allowed, data)) {
possibleTypes.push(allowed);
}
}
Expand Down Expand Up @@ -234,6 +235,41 @@ export type ContextuallyTypedNodeData =
*/
export type ContextuallyTypedFieldData = ContextuallyTypedNodeData | undefined;

/**
* Information needed to interpret a subtree described by {@link ContextuallyTypedNodeData} and {@link ContextuallyTypedFieldData}.
* @alpha
* TODO:
* Currently being exposed at the package level which also requires us to export MapTree at the package level.
* Refactor the FieldGenerator to use JsonableTree instead of MapTree, and convert them internally.
*/
export interface TreeDataContext {
/**
* Schema for the document which the tree will be used in.
*/
readonly schema: SchemaDataAndPolicy;

/**
* Procedural data generator for fields.
* Fields which provide generators here can be omitted in the input contextually typed data.
*
* @remarks
* TODO:
* For implementers of this which are not pure (like identifier generation),
* order of invocation should be made consistent and documented.
* This will be important for identifier elision optimizations in tree encoding for session based identifier generation.
*/
fieldSource?(key: FieldKey, schema: FieldStoredSchema): undefined | FieldGenerator;
}

/**
* Generates field content for a MapTree on demand.
* @alpha
* TODO:
* Currently being exposed at the package level which also requires us to export MapTree at the package level.
* Refactor the FieldGenerator to use JsonableTree instead of MapTree, and convert them internally.
*/
export type FieldGenerator = () => MapTree[];

/**
* Checks the type of a `ContextuallyTypedNodeData`.
*/
Expand Down Expand Up @@ -344,11 +380,11 @@ function shallowCompatibilityTest(
* @alpha
*/
export function cursorFromContextualData(
schemaData: SchemaDataAndPolicy,
context: TreeDataContext,
typeSet: TreeTypeSet,
data: ContextuallyTypedNodeData,
): ITreeCursorSynchronous {
const mapTree = applyTypesFromContext(schemaData, typeSet, data);
const mapTree = applyTypesFromContext(context, typeSet, data);
return singleMapTreeCursor(mapTree);
}

Expand All @@ -357,12 +393,12 @@ export function cursorFromContextualData(
* @alpha
*/
export function cursorForTypedTreeData<T extends TreeSchema>(
schemaData: SchemaDataAndPolicy,
context: TreeDataContext,
schema: T,
data: TypedNode<T, ApiMode.Simple>,
): ITreeCursorSynchronous {
return cursorFromContextualData(
schemaData,
context,
new Set([schema.name]),
data as ContextuallyTypedNodeData,
);
Expand All @@ -378,7 +414,7 @@ export function cursorForTypedData<T extends AllowedTypes>(
data: AllowedTypesToTypedTrees<ApiMode.Simple, T>,
): ITreeCursorSynchronous {
return cursorFromContextualData(
schemaData,
{ schema: schemaData },
allowedTypesToTypeSet(schema),
data as unknown as ContextuallyTypedNodeData,
);
Expand All @@ -391,11 +427,11 @@ export function cursorForTypedData<T extends AllowedTypes>(
* TODO: migrate APIs which take arrays of cursors to take cursors in fields mode.
*/
export function cursorsFromContextualData(
schemaData: SchemaDataAndPolicy,
context: TreeDataContext,
field: FieldStoredSchema,
data: ContextuallyTypedNodeData | undefined,
): ITreeCursorSynchronous[] {
const mapTrees = applyFieldTypesFromContext(schemaData, field, data);
const mapTrees = applyFieldTypesFromContext(context, field, data);
return mapTrees.map(singleMapTreeCursor);
}

Expand All @@ -408,7 +444,11 @@ export function cursorsForTypedFieldData<T extends FieldSchema>(
schema: T,
data: TypedField<T, ApiMode.Simple>,
): ITreeCursorSynchronous {
return cursorFromContextualData(schemaData, schema.types, data as ContextuallyTypedNodeData);
return cursorFromContextualData(
{ schema: schemaData },
schema.types,
data as ContextuallyTypedNodeData,
);
}

/**
Expand All @@ -422,11 +462,11 @@ export function cursorsForTypedFieldData<T extends FieldSchema>(
* This should not be reexported from the parent module.
*/
export function applyTypesFromContext(
schemaData: SchemaDataAndPolicy,
context: TreeDataContext,
typeSet: TreeTypeSet,
data: ContextuallyTypedNodeData,
): MapTree {
const possibleTypes: TreeSchemaIdentifier[] = getPossibleTypes(schemaData, typeSet, data);
const possibleTypes: TreeSchemaIdentifier[] = getPossibleTypes(context, typeSet, data);

assert(
possibleTypes.length !== 0,
Expand All @@ -438,7 +478,8 @@ export function applyTypesFromContext(
);

const type = possibleTypes[0];
const schema = lookupTreeSchema(schemaData, type);
const schema = lookupTreeSchema(context.schema, type);

if (isPrimitiveValue(data)) {
// This check avoids returning an out of schema node
// in the case where schema permits the value, but has required fields.
Expand All @@ -457,7 +498,7 @@ export function applyTypesFromContext(
primary !== undefined,
0x4d6 /* array data reported comparable with the schema without a primary field */,
);
const children = applyFieldTypesFromContext(schemaData, primary.schema, data);
const children = applyFieldTypesFromContext(context, primary.schema, data);
return {
value: undefined,
type,
Expand All @@ -467,13 +508,27 @@ export function applyTypesFromContext(
const fields: Map<FieldKey, MapTree[]> = new Map();
for (const key of fieldKeysFromData(data)) {
assert(!fields.has(key), 0x6b3 /* Keys should not be duplicated */);
const childSchema = getFieldSchema(key, schemaData, schema);
const children = applyFieldTypesFromContext(schemaData, childSchema, data[key]);
const childSchema = getFieldSchema(key, context.schema, schema);
const children = applyFieldTypesFromContext(context, childSchema, data[key]);

if (children.length > 0) {
fields.set(key, children);
}
}

for (const key of schema.globalFields.keys()) {
const currentKey = symbolFromKey(key);
if (data[currentKey] === undefined) {
setFieldForKey(currentKey, context, schema, fields);
}
}

for (const key of schema.localFields.keys()) {
if (data[key] === undefined) {
setFieldForKey(key, context, schema, fields);
}
}

const value = data[valueSymbol];
assert(
allowsValue(schema.value, value),
Expand All @@ -483,6 +538,23 @@ export function applyTypesFromContext(
}
}

function setFieldForKey(
key: FieldKey,
context: TreeDataContext,
schema: TreeStoredSchema,
fields: Map<FieldKey, MapTree[]>,
): void {
const requiredFieldSchema = getFieldSchema(key, context.schema, schema);
const multiplicity = getFieldKind(requiredFieldSchema).multiplicity;
if (multiplicity === Multiplicity.Value && context.fieldSource !== undefined) {
const fieldGenerator = context.fieldSource(key, requiredFieldSchema);
if (fieldGenerator !== undefined) {
const children = fieldGenerator();
fields.set(key, children);
}
}
}

function fieldKeysFromData(data: ContextuallyTypedNodeDataObject): FieldKey[] {
const keys: (string | symbol)[] = Reflect.ownKeys(data).filter(
(key) => typeof key === "string" || symbolIsFieldKey(key),
Expand All @@ -501,7 +573,7 @@ function fieldKeysFromData(data: ContextuallyTypedNodeDataObject): FieldKey[] {
* This should not be reexported from the parent module.
*/
export function applyFieldTypesFromContext(
schemaData: SchemaDataAndPolicy,
context: TreeDataContext,
field: FieldStoredSchema,
data: ContextuallyTypedFieldData,
): MapTree[] {
Expand All @@ -516,13 +588,13 @@ export function applyFieldTypesFromContext(
if (multiplicity === Multiplicity.Sequence) {
assert(isArrayLike(data), 0x4d9 /* expected array for a sequence field */);
const children = Array.from(data, (child) =>
applyTypesFromContext(schemaData, field.types, child),
applyTypesFromContext(context, field.types, child),
);
return children;
}
assert(
multiplicity === Multiplicity.Value || multiplicity === Multiplicity.Optional,
0x4da /* single value provided for an unsupported field */,
);
return [applyTypesFromContext(schemaData, field.types, data)];
return [applyTypesFromContext(context, field.types, data)];
}
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ export class FieldProxyTarget extends ProxyTarget<FieldAnchor> implements Editab
}
}

return cursorsFromContextualData(this.context.schema, this.fieldSchema, content);
return cursorsFromContextualData(this.context, this.fieldSchema, content);
}

public get [proxyTargetSymbol](): FieldProxyTarget {
Expand Down Expand Up @@ -498,7 +498,7 @@ const fieldProxyHandler: AdaptingProxyHandler<FieldProxyTarget, EditableField> =
);

const cursor = cursorFromContextualData(
target.context.schema,
target.context,
target.fieldSchema.types,
value as ContextuallyTypedNodeData,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@ import {
Anchor,
SchemaDataAndPolicy,
ForestEvents,
FieldStoredSchema,
FieldKey,
} from "../../core";
import { ISubscribable } from "../../events";
import { DefaultEditBuilder } from "../defaultChangeFamily";
import { FieldGenerator } from "../contextuallyTyped";
import { EditableField, NewFieldContent, UnwrappedEditableField } from "./editableTreeTypes";
import { makeField, unwrappedField } from "./editableField";
import { ProxyTarget } from "./ProxyTarget";
Expand Down Expand Up @@ -101,6 +104,11 @@ export interface EditableTreeContext extends ISubscribable<ForestEvents> {
* to create new trees starting from the root.
*/
clear(): void;

/**
* FieldSource used to get a FieldGenerator to populate required fields during procedural contextual data generation.
*/
fieldSource?(key: FieldKey, schema: FieldStoredSchema): undefined | FieldGenerator;
}

/**
Expand Down
2 changes: 2 additions & 0 deletions experimental/dds/tree2/src/feature-libraries/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ export {
cursorForTypedData,
cursorForTypedTreeData,
cursorsForTypedFieldData,
FieldGenerator,
TreeDataContext,
} from "./contextuallyTyped";

export { ForestSummarizer } from "./forestSummarizer";
Expand Down
Loading

0 comments on commit a51cd2a

Please sign in to comment.