Skip to content

Commit

Permalink
Merge branch 'main' into devtools/update-icons
Browse files Browse the repository at this point in the history
  • Loading branch information
Josmithr committed Oct 4, 2024
2 parents 3d792f5 + 09d786a commit 82078e2
Show file tree
Hide file tree
Showing 8 changed files with 321 additions and 68 deletions.
58 changes: 58 additions & 0 deletions packages/dds/tree/src/simple-tree/api/conciseTree.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/

import type { IFluidHandle } from "@fluidframework/core-interfaces";

import type { ITreeCursor } from "../../core/index.js";
import type { TreeLeafValue, ImplicitAllowedTypes } from "../schemaTypes.js";
import type { TreeNodeSchema } from "../core/index.js";
import { customFromCursorInner, type EncodeOptions } from "./customTree.js";
import { getUnhydratedContext } from "../createContext.js";

/**
* Concise encoding of a {@link TreeNode} or {@link TreeLeafValue}.
* @remarks
* This is "concise" meaning that explicit type information is omitted.
* If the schema is compatible with {@link ITreeConfigurationOptions.preventAmbiguity},
* types will be lossless and compatible with {@link TreeBeta.create} (unless the options are used to customize it).
*
* Every {@link TreeNode} is an array or object.
* Any IFluidHandle values have been replaced by `THandle`.
* @privateRemarks
* This can store all possible simple trees,
* but it can not store all possible trees representable by our internal representations like FlexTree and JsonableTree.
*/
export type ConciseTree<THandle = IFluidHandle> =
| Exclude<TreeLeafValue, IFluidHandle>
| THandle
| ConciseTree<THandle>[]
| {
[key: string]: ConciseTree<THandle>;
};

/**
* Used to read a node cursor as a ConciseTree.
*/
export function conciseFromCursor<TCustom>(
reader: ITreeCursor,
rootSchema: ImplicitAllowedTypes,
options: EncodeOptions<TCustom>,
): ConciseTree<TCustom> {
const config: Required<EncodeOptions<TCustom>> = {
useStoredKeys: false,
...options,
};

const schemaMap = getUnhydratedContext(rootSchema).schema;
return conciseFromCursorInner(reader, config, schemaMap);
}

function conciseFromCursorInner<TCustom>(
reader: ITreeCursor,
options: Required<EncodeOptions<TCustom>>,
schema: ReadonlyMap<string, TreeNodeSchema>,
): ConciseTree<TCustom> {
return customFromCursorInner(reader, options, schema, conciseFromCursorInner);
}
119 changes: 119 additions & 0 deletions packages/dds/tree/src/simple-tree/api/customTree.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/

import type { IFluidHandle } from "@fluidframework/core-interfaces";
import { isFluidHandle } from "@fluidframework/runtime-utils/internal";
import { assert } from "@fluidframework/core-utils/internal";

import {
EmptyKey,
forEachField,
inCursorField,
mapCursorField,
type ITreeCursor,
} from "../../core/index.js";
import { fail } from "../../util/index.js";
import type { TreeLeafValue } from "../schemaTypes.js";
import { NodeKind, type TreeNodeSchema } from "../core/index.js";
import {
booleanSchema,
handleSchema,
nullSchema,
numberSchema,
stringSchema,
} from "../leafNodeSchema.js";
import { isObjectNodeSchema } from "../objectNodeTypes.js";

/**
* Options for how to interpret a `CustomTree<TCustom>` without relying on schema.
*/
export interface EncodeOptions<TCustom> {
/**
* Fixup custom input formats.
* @remarks
* See note on {@link ParseOptions.valueConverter}.
*/
valueConverter(data: IFluidHandle): TCustom;
/**
* If true, interpret the input keys of object nodes as stored keys.
* If false, interpret them as property keys.
* @defaultValue false.
*/
readonly useStoredKeys?: boolean;
}

/**
* Tree representation with fields as properties and customized handle and child representations.
*/
export type CustomTree<TChild, THandle> = CustomTreeNode<TChild> | CustomTreeValue<THandle>;

/**
* TreeLeafValue except the handle type is customized.
*/
export type CustomTreeValue<THandle> = Exclude<TreeLeafValue, IFluidHandle> | THandle;

/**
* Tree node representation with fields as properties and customized child representation.
*/
export type CustomTreeNode<TChild> = TChild[] | { [key: string]: TChild };

/**
* Builds an {@link CustomTree} from a cursor in Nodes mode.
*/
export function customFromCursorInner<TChild, THandle>(
reader: ITreeCursor,
options: Required<EncodeOptions<THandle>>,
schema: ReadonlyMap<string, TreeNodeSchema>,
childHandler: (
reader: ITreeCursor,
options: Required<EncodeOptions<THandle>>,
schema: ReadonlyMap<string, TreeNodeSchema>,
) => TChild,
): CustomTree<TChild, THandle> {
const type = reader.type;
const nodeSchema = schema.get(type) ?? fail("missing schema for type in cursor");

switch (type) {
case numberSchema.identifier:
case booleanSchema.identifier:
case nullSchema.identifier:
case stringSchema.identifier:
assert(reader.value !== undefined, "out of schema: missing value");
assert(!isFluidHandle(reader.value), "out of schema: unexpected FluidHandle");
return reader.value;
case handleSchema.identifier:
assert(reader.value !== undefined, "out of schema: missing value");
assert(isFluidHandle(reader.value), "out of schema: expected FluidHandle");
return options.valueConverter(reader.value);
default: {
assert(reader.value === undefined, "out of schema: unexpected value");
if (nodeSchema.kind === NodeKind.Array) {
const fields = inCursorField(reader, EmptyKey, () =>
mapCursorField(reader, () => childHandler(reader, options, schema)),
);
return fields;
} else {
const fields: Record<string, TChild> = {};
forEachField(reader, () => {
const children = mapCursorField(reader, () => childHandler(reader, options, schema));
if (children.length === 1) {
const storedKey = reader.getFieldKey();
const key =
isObjectNodeSchema(nodeSchema) && !options.useStoredKeys
? nodeSchema.storedKeyToPropertyKey.get(storedKey) ??
fail("missing property key")
: storedKey;
// Length is checked above.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
fields[key] = children[0]!;
} else {
assert(children.length === 0, 0xa19 /* invalid children number */);
}
});
return fields;
}
}
}
}
10 changes: 10 additions & 0 deletions packages/dds/tree/src/simple-tree/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,16 @@ export type {
ReadonlyMapInlined,
} from "./typesUnsafe.js";

export type {
VerboseTreeNode,
ParseOptions,
VerboseTree,
} from "./verboseTree.js";

export type { EncodeOptions } from "./customTree.js";

export type { ConciseTree } from "./conciseTree.js";

export { TreeBeta, type NodeChangedData, type TreeChangeEventsBeta } from "./treeApiBeta.js";

// Exporting the schema (RecursiveObject) to test that recursive types are working correctly.
Expand Down
81 changes: 15 additions & 66 deletions packages/dds/tree/src/simple-tree/api/verboseTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,7 @@ import { assert } from "@fluidframework/core-utils/internal";
import {
aboveRootPlaceholder,
EmptyKey,
forEachField,
inCursorField,
keyAsDetachedField,
mapCursorField,
type FieldKey,
type ITreeCursor,
type ITreeCursorSynchronous,
Expand Down Expand Up @@ -41,6 +38,12 @@ import {
} from "../leafNodeSchema.js";
import { isObjectNodeSchema } from "../objectNodeTypes.js";
import { walkFieldSchema } from "../walkFieldSchema.js";
import {
customFromCursorInner,
type CustomTreeNode,
type CustomTreeValue,
type EncodeOptions,
} from "./customTree.js";
import { getUnhydratedContext } from "../createContext.js";

/**
Expand Down Expand Up @@ -144,24 +147,6 @@ export interface SchemalessParseOptions<TCustom> {
};
}

/**
* Options for how to interpret a `VerboseTree<TCustom>` without relying on schema.
*/
export interface EncodeOptions<TCustom> {
/**
* Fixup custom input formats.
* @remarks
* See note on {@link ParseOptions.valueConverter}.
*/
valueConverter(data: IFluidHandle): TCustom;
/**
* If true, interpret the input keys of object nodes as stored keys.
* If false, interpret them as property keys.
* @defaultValue false.
*/
readonly useStoredKeys?: boolean;
}

/**
* Use info from `schema` to convert `options` to {@link SchemalessParseOptions}.
*/
Expand Down Expand Up @@ -360,50 +345,14 @@ function verboseFromCursorInner<TCustom>(
options: Required<EncodeOptions<TCustom>>,
schema: ReadonlyMap<string, TreeNodeSchema>,
): VerboseTree<TCustom> {
const type = reader.type;
const nodeSchema = schema.get(type) ?? fail("missing schema for type in cursor");

switch (type) {
case numberSchema.identifier:
case booleanSchema.identifier:
case nullSchema.identifier:
case stringSchema.identifier:
assert(reader.value !== undefined, 0xa14 /* out of schema: missing value */);
assert(!isFluidHandle(reader.value), 0xa15 /* out of schema: unexpected FluidHandle */);
return reader.value;
case handleSchema.identifier:
assert(reader.value !== undefined, 0xa16 /* out of schema: missing value */);
assert(isFluidHandle(reader.value), 0xa17 /* out of schema: unexpected FluidHandle */);
return options.valueConverter(reader.value);
default: {
assert(reader.value === undefined, 0xa18 /* out of schema: unexpected value */);
if (nodeSchema.kind === NodeKind.Array) {
const fields = inCursorField(reader, EmptyKey, () =>
mapCursorField(reader, () => verboseFromCursorInner(reader, options, schema)),
);
return { type, fields };
} else {
const fields: Record<string, VerboseTree<TCustom>> = {};
forEachField(reader, () => {
const children = mapCursorField(reader, () =>
verboseFromCursorInner(reader, options, schema),
);
if (children.length === 1) {
const storedKey = reader.getFieldKey();
const key =
isObjectNodeSchema(nodeSchema) && !options.useStoredKeys
? nodeSchema.storedKeyToPropertyKey.get(storedKey) ??
fail("missing property key")
: storedKey;
// Length is checked above.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
fields[key] = children[0]!;
} else {
assert(children.length === 0, 0xa19 /* invalid children number */);
}
});
return { type, fields };
}
}
const fields = customFromCursorInner(reader, options, schema, verboseFromCursorInner);
const nodeSchema = schema.get(reader.type) ?? fail("missing schema for type in cursor");
if (nodeSchema.kind === NodeKind.Leaf) {
return fields as CustomTreeValue<TCustom>;
}

return {
type: reader.type,
fields: fields as CustomTreeNode<TCustom>,
};
}
5 changes: 5 additions & 0 deletions packages/dds/tree/src/simple-tree/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ export {
type JsonLeafSchemaType,
getJsonSchema,
getSimpleSchema,
type VerboseTreeNode,
type EncodeOptions,
type ParseOptions,
type VerboseTree,
type ConciseTree,
ViewSchema,
type Unenforced,
type FieldHasDefaultUnsafe,
Expand Down
23 changes: 23 additions & 0 deletions packages/dds/tree/src/test/simple-tree/api/conciseTree.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*!
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
* Licensed under the MIT License.
*/

import { strict as assert, fail } from "node:assert";

import { JsonUnion, singleJsonCursor } from "../../json/index.js";
// eslint-disable-next-line import/no-internal-modules
import { conciseFromCursor } from "../../../simple-tree/api/conciseTree.js";

describe("simple-tree conciseTree", () => {
it("conciseFromCursor", () => {
assert.deepEqual(
conciseFromCursor(singleJsonCursor({ a: { b: 1 } }), JsonUnion, {
valueConverter: () => fail(),
}),
{
a: { b: 1 },
},
);
});
});
Loading

0 comments on commit 82078e2

Please sign in to comment.