Skip to content

Commit 6bf0484

Browse files
FieldKind cleanup (#25869)
## Description Refactors to cleanup FlexFieldKind and its implementations.
1 parent d116cfc commit 6bf0484

File tree

7 files changed

+136
-93
lines changed

7 files changed

+136
-93
lines changed

packages/dds/tree/src/core/schema-stored/schema.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,13 +91,24 @@ export enum ValueSchema {
9191
export type TreeTypeSet = ReadonlySet<TreeNodeSchemaIdentifier>;
9292

9393
/**
94-
* Declarative portion of a Field Kind.
94+
* Declarative portion of a {@link FlexFieldKind}.
9595
*
9696
* @remarks
9797
* Enough info about a field kind to know if a given tree is is schema.
98+
*
99+
* Note that compatibility between trees and schema is not sufficient to evaluate if a schema upgrade should be allowed.
100+
* Currently schema upgrades are restricted to field kind changes which can not be cyclic (like version upgrades but not down grades).
101+
* See {@link FlexFieldKind.allowsFieldSuperset} for more details.
98102
*/
99103
export interface FieldKindData {
104+
/**
105+
* Globally scoped identifier.
106+
*/
100107
readonly identifier: FieldKindIdentifier;
108+
/**
109+
* Bound on the number of children that fields of this kind may have.
110+
* TODO: consider replacing this with numeric upper and lower bounds.
111+
*/
101112
readonly multiplicity: Multiplicity;
102113
}
103114

packages/dds/tree/src/feature-libraries/default-schema/defaultFieldKinds.ts

Lines changed: 60 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,15 @@ export const noChangeHandler: FieldChangeHandler<0> = {
5959
getCrossFieldKeys: () => [],
6060
};
6161

62-
export interface ValueFieldEditor extends FieldEditor<OptionalChangeset> {
62+
/**
63+
* {@link FieldEditor} for required fields (always contain exactly 1 child).
64+
* @remarks
65+
* This shares code with optional fields, since they are the same edit wise except setting to empty is not allowed,
66+
* and the content is always assumed to not be empty.
67+
* This means the actual edits implemented for optional fields are sufficient to support required fields
68+
* which is why this is defined and implemented in terms of optional fields.
69+
*/
70+
export interface RequiredFieldEditor extends FieldEditor<OptionalChangeset> {
6371
/**
6472
* Creates a change which replaces the current value of the field with `newValue`.
6573
* @param ids - The ids for the fill and detach fields.
@@ -68,42 +76,41 @@ export interface ValueFieldEditor extends FieldEditor<OptionalChangeset> {
6876
}
6977

7078
const optionalIdentifier = brandConst("Optional")<FieldKindIdentifier>();
79+
7180
/**
7281
* 0 or 1 items.
7382
*/
74-
export const optional = new FlexFieldKind(
75-
optionalIdentifier,
76-
Multiplicity.Optional,
77-
optionalChangeHandler,
78-
(types, other) =>
83+
export const optional = new FlexFieldKind(optionalIdentifier, Multiplicity.Optional, {
84+
changeHandler: optionalChangeHandler,
85+
allowsTreeSupersetOf: (types, other) =>
7986
(other.kind === sequence.identifier || other.kind === optionalIdentifier) &&
8087
allowsTreeSchemaIdentifierSuperset(types, other.types),
81-
new Set([]),
82-
);
88+
});
8389

84-
export const valueFieldEditor: ValueFieldEditor = {
90+
export const requiredFieldEditor: RequiredFieldEditor = {
8591
...optionalFieldEditor,
8692
set: (ids: {
8793
fill: ChangeAtomId;
8894
detach: ChangeAtomId;
8995
}): OptionalChangeset => optionalFieldEditor.set(false, ids),
9096
};
9197

92-
export const valueChangeHandler: FieldChangeHandler<OptionalChangeset, ValueFieldEditor> = {
93-
...optional.changeHandler,
94-
editor: valueFieldEditor,
98+
export const requiredFieldChangeHandler: FieldChangeHandler<
99+
OptionalChangeset,
100+
RequiredFieldEditor
101+
> = {
102+
...optionalChangeHandler,
103+
editor: requiredFieldEditor,
95104
};
96105

97106
const requiredIdentifier = brandConst("Value")<FieldKindIdentifier>();
98107

99108
/**
100109
* Exactly one item.
101110
*/
102-
export const required = new FlexFieldKind(
103-
requiredIdentifier,
104-
Multiplicity.Single,
105-
valueChangeHandler,
106-
(types, other) =>
111+
export const required = new FlexFieldKind(requiredIdentifier, Multiplicity.Single, {
112+
changeHandler: requiredFieldChangeHandler,
113+
allowsTreeSupersetOf: (types, other) =>
107114
// By omitting Identifier here,
108115
// this is making a policy choice that a schema upgrade cannot be done from required to identifier.
109116
// Since an identifier can be upgraded into a required field,
@@ -113,43 +120,35 @@ export const required = new FlexFieldKind(
113120
other.kind === requiredIdentifier ||
114121
other.kind === optional.identifier) &&
115122
allowsTreeSchemaIdentifierSuperset(types, other.types),
116-
new Set(),
117-
);
123+
});
118124

119125
const sequenceIdentifier = brandConst("Sequence")<FieldKindIdentifier>();
120126

121127
/**
122128
* 0 or more items.
123129
*/
124-
export const sequence = new FlexFieldKind(
125-
sequenceIdentifier,
126-
Multiplicity.Sequence,
127-
sequenceFieldChangeHandler,
128-
(types, other) =>
130+
export const sequence = new FlexFieldKind(sequenceIdentifier, Multiplicity.Sequence, {
131+
changeHandler: sequenceFieldChangeHandler,
132+
allowsTreeSupersetOf: (types, other) =>
129133
other.kind === sequenceIdentifier &&
130134
allowsTreeSchemaIdentifierSuperset(types, other.types),
131-
// TODO: add normalizer/importers for handling ops from other kinds.
132-
new Set([]),
133-
);
135+
});
134136

135137
const identifierFieldIdentifier = brandConst("Identifier")<FieldKindIdentifier>();
136138

137139
/**
138140
* Exactly one identifier.
139141
*/
140-
export const identifier = new FlexFieldKind(
141-
identifierFieldIdentifier,
142-
Multiplicity.Single,
143-
noChangeHandler,
144-
(types, other) =>
142+
export const identifier = new FlexFieldKind(identifierFieldIdentifier, Multiplicity.Single, {
143+
changeHandler: noChangeHandler,
144+
allowsTreeSupersetOf: (types, other) =>
145145
// Allows upgrading from identifier to required: which way this upgrade is allowed to go is a subjective policy choice.
146146
(other.kind === sequence.identifier ||
147147
other.kind === requiredIdentifier ||
148148
other.kind === optional.identifier ||
149149
other.kind === identifierFieldIdentifier) &&
150150
allowsTreeSchemaIdentifierSuperset(types, other.types),
151-
new Set(),
152-
);
151+
});
153152

154153
/**
155154
* Exactly 0 items.
@@ -182,10 +181,12 @@ export const identifier = new FlexFieldKind(
182181
export const forbidden = new FlexFieldKind(
183182
forbiddenFieldKindIdentifier,
184183
Multiplicity.Forbidden,
185-
noChangeHandler,
186-
// All multiplicities other than Value support empty.
187-
(types, other) => fieldKinds.get(other.kind)?.multiplicity !== Multiplicity.Single,
188-
new Set(),
184+
{
185+
changeHandler: noChangeHandler,
186+
// All multiplicities other than Value support empty.
187+
allowsTreeSupersetOf: (types, other) =>
188+
fieldKinds.get(other.kind)?.multiplicity !== Multiplicity.Single,
189+
},
189190
);
190191

191192
export const fieldKindConfigurations: ReadonlyMap<
@@ -270,15 +271,31 @@ export const fieldKinds: ReadonlyMap<FieldKindIdentifier, FlexFieldKind> = new M
270271
// TODO: ensure thy work in generated docs.
271272
// TODO: add these comments to the rest of the cases below.
272273
export interface Required
273-
extends FlexFieldKind<ValueFieldEditor, "Value", Multiplicity.Single> {}
274+
extends FlexFieldKind<RequiredFieldEditor, typeof requiredIdentifier, Multiplicity.Single> {}
274275
export interface Optional
275-
extends FlexFieldKind<OptionalFieldEditor, "Optional", Multiplicity.Optional> {}
276+
extends FlexFieldKind<
277+
OptionalFieldEditor,
278+
typeof optionalIdentifier,
279+
Multiplicity.Optional
280+
> {}
276281
export interface Sequence
277-
extends FlexFieldKind<SequenceFieldEditor, "Sequence", Multiplicity.Sequence> {}
282+
extends FlexFieldKind<
283+
SequenceFieldEditor,
284+
typeof sequenceIdentifier,
285+
Multiplicity.Sequence
286+
> {}
278287
export interface Identifier
279-
extends FlexFieldKind<FieldEditor<0>, "Identifier", Multiplicity.Single> {}
288+
extends FlexFieldKind<
289+
FieldEditor<0>,
290+
typeof identifierFieldIdentifier,
291+
Multiplicity.Single
292+
> {}
280293
export interface Forbidden
281-
extends FlexFieldKind<FieldEditor<0>, "Forbidden", Multiplicity.Forbidden> {}
294+
extends FlexFieldKind<
295+
FieldEditor<0>,
296+
typeof forbiddenFieldKindIdentifier,
297+
Multiplicity.Forbidden
298+
> {}
282299

283300
/**
284301
* Default FieldKinds with their editor types erased.

packages/dds/tree/src/feature-libraries/modular-schema/fieldKind.ts

Lines changed: 47 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -36,43 +36,34 @@ export class FlexFieldKind<
3636
// TODO: stronger typing
3737
// eslint-disable-next-line @typescript-eslint/no-explicit-any
3838
TEditor extends FieldEditor<any> = FieldEditor<any>,
39-
TName extends string = string,
39+
TName extends FieldKindIdentifier = FieldKindIdentifier,
4040
TMultiplicity extends Multiplicity = Multiplicity,
4141
> implements FieldKindData
4242
{
4343
protected _typeCheck!: MakeNominal;
4444

4545
/**
46-
* @param identifier - Globally scoped identifier.
47-
* @param multiplicity - bound on the number of children that fields of this kind may have.
48-
* TODO: replace with numeric upper and lower bounds.
49-
* @param changeHandler - Change handling policy.
50-
* @param allowsTreeSupersetOf - returns true iff `superset` supports all that this does
51-
* and `superset` is an allowed upgrade. Does not have to handle the `never` cases.
52-
* See {@link isNeverField}.
53-
* TODO: when used as a method (instead of a free function like the other superset related functions),
54-
* this name is/signature is confusing and seems backwards.
55-
* @param handlesEditsFrom - Kinds (in addition to this) whose edits can be processed by changeHandler.
56-
* If the kind of a field changes, and edits are rebased across that kind change,
57-
* listing the other old kind here can prevent those edits from being conflicted and
58-
* provide a chance to handle them.
46+
* Change handling policy.
5947
*/
48+
// TODO: stronger typing
49+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
50+
public readonly changeHandler: FieldChangeHandler<any, TEditor>;
51+
6052
public constructor(
61-
public readonly identifier: TName & FieldKindIdentifier,
53+
public readonly identifier: TName,
6254
public readonly multiplicity: TMultiplicity,
6355
// TODO: stronger typing
6456
// eslint-disable-next-line @typescript-eslint/no-explicit-any
65-
public readonly changeHandler: FieldChangeHandler<any, TEditor>,
66-
private readonly allowsTreeSupersetOf: (
67-
originalTypes: TreeTypeSet,
68-
superset: TreeFieldStoredSchema,
69-
) => boolean,
70-
public readonly handlesEditsFrom: ReadonlySet<FieldKindIdentifier>,
71-
) {}
57+
private readonly options: FieldKindOptions<FieldChangeHandler<any, TEditor>>,
58+
) {
59+
this.changeHandler = options.changeHandler;
60+
}
7261

7362
/**
7463
* Returns true if and only if `superset` permits a (non-strict) superset of the subtrees
7564
* allowed by field made from `this` and `originalTypes`.
65+
*
66+
* TODO: clarify the relationship between this and FieldKindData, and issues with cyclic schema upgrades.
7667
*/
7768
public allowsFieldSuperset(
7869
policy: SchemaPolicy,
@@ -93,10 +84,43 @@ export class FlexFieldKind<
9384
if (isNeverField(policy, originalData, superset)) {
9485
return false;
9586
}
96-
return this.allowsTreeSupersetOf(originalTypes, superset);
87+
return this.options.allowsTreeSupersetOf(originalTypes, superset);
9788
}
9889
}
9990

91+
/**
92+
* Additional options for {@link FlexFieldKind}.
93+
*
94+
* @remarks
95+
* Puts the more confusing parameters into this object so they get explicit names to help with clarity.
96+
*/
97+
export interface FieldKindOptions<TFieldChangeHandler> {
98+
/**
99+
* Change handling policy.
100+
*/
101+
readonly changeHandler: TFieldChangeHandler;
102+
103+
/**
104+
* Returns true if and only if `superset` permits a (non-strict) superset of the subtrees
105+
* allowed by field made from `this` and `originalTypes`.
106+
* @remarks
107+
* Used by {@link FlexFieldKind.allowsFieldSuperset}, which handles the `never` cases before calling this.
108+
*/
109+
readonly allowsTreeSupersetOf: (
110+
originalTypes: TreeTypeSet,
111+
superset: TreeFieldStoredSchema,
112+
) => boolean;
113+
114+
/**
115+
* Kinds (in addition to this) whose edits can be processed by changeHandler.
116+
* If the kind of a field changes, and edits are rebased across that kind change,
117+
* listing the other old kind here can prevent those edits from being conflicted and
118+
* provide a chance to handle them.
119+
*/
120+
// TODO: provide this and use it for improved support for rebasing changes across schema upgrades.
121+
// readonly handlesEditsFrom: ReadonlySet<FieldKindIdentifier>;
122+
}
123+
100124
/**
101125
* Policy from the app for interpreting the stored schema.
102126
* The app must ensure consistency for all users of the document.

packages/dds/tree/src/feature-libraries/modular-schema/genericFieldKind.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -169,9 +169,7 @@ function replaceRevisions(
169169
export const genericFieldKind: FlexFieldKind = new FlexFieldKind(
170170
brandConst("ModularEditBuilder.Generic")<FieldKindIdentifier>(),
171171
Multiplicity.Sequence,
172-
genericChangeHandler,
173-
(types, other) => false,
174-
new Set(),
172+
{ changeHandler: genericChangeHandler, allowsTreeSupersetOf: (types, other) => false },
175173
);
176174

177175
/**

packages/dds/tree/src/test/feature-libraries/default-field-kinds/defaultFieldKinds.spec.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ import { strict as assert, fail } from "node:assert";
77

88
import { makeAnonChange } from "../../../core/index.js";
99
import {
10-
type ValueFieldEditor,
11-
valueChangeHandler,
12-
valueFieldEditor,
10+
type RequiredFieldEditor,
11+
requiredFieldChangeHandler,
12+
requiredFieldEditor,
1313
// Allow import from file being tested.
1414
// eslint-disable-next-line import-x/no-internal-modules
1515
} from "../../../feature-libraries/default-schema/defaultFieldKinds.js";
@@ -62,15 +62,15 @@ const childComposer1_2 = (
6262
};
6363

6464
describe("defaultFieldKinds", () => {
65-
describe("valueFieldEditor.set", () => {
66-
it("valueFieldEditor.set", () => {
65+
describe("requiredFieldEditor.set", () => {
66+
it("requiredFieldEditor.set", () => {
6767
const expected = Change.atOnce(
6868
Change.clear("self", brand(1)),
6969
Change.move(brand(41), "self"),
7070
);
7171
const revision = mintRevisionTag();
7272
assertEqual(
73-
valueFieldEditor.set({
73+
requiredFieldEditor.set({
7474
detach: { localId: brand(1), revision },
7575
fill: { localId: brand(41), revision },
7676
}),
@@ -80,11 +80,11 @@ describe("defaultFieldKinds", () => {
8080
});
8181

8282
// TODO:
83-
// These tests are covering value field usage patterns of optional field's rebaser (which value field uses).
83+
// These tests are covering required field usage patterns of optional field's rebaser (which required field uses).
8484
// These patterns should be covered in the optional field tests and not be needed here (except perhaps for a minimal integration test).
85-
describe("value field rebaser", () => {
86-
const fieldHandler: FieldChangeHandler<OptionalChangeset, ValueFieldEditor> =
87-
valueChangeHandler;
85+
describe("required field rebaser", () => {
86+
const fieldHandler: FieldChangeHandler<OptionalChangeset, RequiredFieldEditor> =
87+
requiredFieldChangeHandler;
8888

8989
const childChange1 = Change.child(nodeChange1);
9090
const childChange2 = Change.child(nodeChange2);

packages/dds/tree/src/test/feature-libraries/modular-schema/basicRebasers.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,5 @@ export const valueHandler = {
113113
export const valueField = new FlexFieldKind(
114114
brandConst("Value")<FieldKindIdentifier>(),
115115
Multiplicity.Single,
116-
valueHandler,
117-
(a, b) => false,
118-
new Set(),
116+
{ changeHandler: valueHandler, allowsTreeSupersetOf: (a, b) => false },
119117
);

0 commit comments

Comments
 (0)