Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Edits] Support for deprecatedID and instanceID edit semantics #352

Merged
merged 12 commits into from
Mar 24, 2025
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
engine: prep for instance meta as special case building instance node…
… children

This splits the current `children.ts` module roughly into:

- Branchy node-specific construction logic that already existed prior to #336.
- Coordination of input to those node constructors introduced in #336. This logic is hopefully a little bit clearer now too.

The latter module’s logic will be expanded in the next commit, to include said special case logic for instance meta children.
eyelidlessness committed Mar 20, 2025
commit e5c169565343b35cab746e70ce313b69fd7bcee1
2 changes: 1 addition & 1 deletion packages/xforms-engine/src/instance/Group.ts
Original file line number Diff line number Diff line change
@@ -20,7 +20,7 @@ import { createNodeLabel } from '../lib/reactivity/text/createNodeLabel.ts';
import { createAggregatedViolations } from '../lib/reactivity/validation/createAggregatedViolations.ts';
import type { DescendantNodeSharedStateSpec } from './abstract/DescendantNode.ts';
import { DescendantNode } from './abstract/DescendantNode.ts';
import { buildChildren } from './children.ts';
import { buildChildren } from './children/buildChildren.ts';
import type { GeneralChildNode, GeneralParentNode } from './hierarchy.ts';
import type { EvaluationContext } from './internal-api/EvaluationContext.ts';
import type { ClientReactiveSerializableParentNode } from './internal-api/serialization/ClientReactiveSerializableParentNode.ts';
2 changes: 1 addition & 1 deletion packages/xforms-engine/src/instance/Root.ts
Original file line number Diff line number Diff line change
@@ -24,7 +24,7 @@ import { createAggregatedViolations } from '../lib/reactivity/validation/createA
import type { BodyClassList } from '../parse/body/BodyDefinition.ts';
import type { RootDefinition } from '../parse/model/RootDefinition.ts';
import { DescendantNode } from './abstract/DescendantNode.ts';
import { buildChildren } from './children.ts';
import { buildChildren } from './children/buildChildren.ts';
import type { GeneralChildNode } from './hierarchy.ts';
import type { EvaluationContext } from './internal-api/EvaluationContext.ts';
import type { ClientReactiveSerializableParentNode } from './internal-api/serialization/ClientReactiveSerializableParentNode.ts';
2 changes: 1 addition & 1 deletion packages/xforms-engine/src/instance/Subtree.ts
Original file line number Diff line number Diff line change
@@ -18,7 +18,7 @@ import { createSharedNodeState } from '../lib/reactivity/node-state/createShared
import { createAggregatedViolations } from '../lib/reactivity/validation/createAggregatedViolations.ts';
import type { DescendantNodeSharedStateSpec } from './abstract/DescendantNode.ts';
import { DescendantNode } from './abstract/DescendantNode.ts';
import { buildChildren } from './children.ts';
import { buildChildren } from './children/buildChildren.ts';
import type { GeneralChildNode, GeneralParentNode } from './hierarchy.ts';
import type { EvaluationContext } from './internal-api/EvaluationContext.ts';
import type { ClientReactiveSerializableParentNode } from './internal-api/serialization/ClientReactiveSerializableParentNode.ts';
Original file line number Diff line number Diff line change
@@ -1,59 +1,35 @@
import { UnreachableError } from '@getodk/common/lib/error/UnreachableError.ts';
import type { GroupDefinition } from '../client/GroupNode.ts';
import type { InputDefinition } from '../client/InputNode.ts';
import type { ModelValueDefinition } from '../client/ModelValueNode.ts';
import type { RankDefinition } from '../client/RankNode.ts';
import type { SelectDefinition } from '../client/SelectNode.ts';
import type { SubtreeDefinition } from '../client/SubtreeNode.ts';
import type { TriggerNodeDefinition } from '../client/TriggerNode.ts';
import type { UploadNodeDefinition } from '../client/unsupported/UploadNode.ts';
import { ErrorProductionDesignPendingError } from '../error/ErrorProductionDesignPendingError.ts';
import type { StaticDocument } from '../integration/xpath/static-dom/StaticDocument.ts';
import type { StaticElement } from '../integration/xpath/static-dom/StaticElement.ts';
import type { LeafNodeDefinition } from '../parse/model/LeafNodeDefinition.ts';
import type { NodeDefinition } from '../parse/model/NodeDefinition.ts';
import { NoteNodeDefinition } from '../parse/model/NoteNodeDefinition.ts';
import type { GroupDefinition } from '../../client/GroupNode.ts';
import type { InputDefinition } from '../../client/InputNode.ts';
import type { ModelValueDefinition } from '../../client/ModelValueNode.ts';
import type { RankDefinition } from '../../client/RankNode.ts';
import type { SelectDefinition } from '../../client/SelectNode.ts';
import type { SubtreeDefinition } from '../../client/SubtreeNode.ts';
import type { TriggerNodeDefinition } from '../../client/TriggerNode.ts';
import type { UploadNodeDefinition } from '../../client/unsupported/UploadNode.ts';
import { ErrorProductionDesignPendingError } from '../../error/ErrorProductionDesignPendingError.ts';
import type { LeafNodeDefinition } from '../../parse/model/LeafNodeDefinition.ts';
import { NoteNodeDefinition } from '../../parse/model/NoteNodeDefinition.ts';
import type {
AnyRangeNodeDefinition,
RangeLeafNodeDefinition,
} from '../parse/model/RangeNodeDefinition.ts';
import { RangeNodeDefinition } from '../parse/model/RangeNodeDefinition.ts';
import type { SubtreeDefinition as ModelSubtreeDefinition } from '../parse/model/SubtreeDefinition.ts';
import type { InstanceNode } from './abstract/InstanceNode.ts';
import { Group } from './Group.ts';
import type { GeneralChildNode, GeneralParentNode } from './hierarchy.ts';
import { InputControl } from './InputControl.ts';
import { ModelValue } from './ModelValue.ts';
import { Note } from './Note.ts';
import { RangeControl } from './RangeControl.ts';
import { RankControl } from './RankControl.ts';
import { RepeatRangeControlled } from './repeat/RepeatRangeControlled.ts';
import { RepeatRangeUncontrolled } from './repeat/RepeatRangeUncontrolled.ts';
import { SelectControl } from './SelectControl.ts';
import { Subtree } from './Subtree.ts';
import { TriggerControl } from './TriggerControl.ts';
import { UploadControl } from './unsupported/UploadControl.ts';

type InstanceNodesByNodeset = ReadonlyMap<string, readonly [StaticElement, ...StaticElement[]]>;

const groupChildElementsByNodeset = (
parent: StaticDocument | StaticElement
): InstanceNodesByNodeset => {
const result = new Map<string, [StaticElement, ...StaticElement[]]>();

for (const child of parent.childElements) {
const { nodeset } = child;
const group = result.get(nodeset);

if (group == null) {
result.set(nodeset, [child]);
} else {
group.push(child);
}
}

return result;
};
} from '../../parse/model/RangeNodeDefinition.ts';
import { RangeNodeDefinition } from '../../parse/model/RangeNodeDefinition.ts';
import type { SubtreeDefinition as ModelSubtreeDefinition } from '../../parse/model/SubtreeDefinition.ts';
import { Group } from '../Group.ts';
import type { GeneralChildNode, GeneralParentNode } from '../hierarchy.ts';
import { InputControl } from '../InputControl.ts';
import { ModelValue } from '../ModelValue.ts';
import { Note } from '../Note.ts';
import { RangeControl } from '../RangeControl.ts';
import { RankControl } from '../RankControl.ts';
import { RepeatRangeControlled } from '../repeat/RepeatRangeControlled.ts';
import { RepeatRangeUncontrolled } from '../repeat/RepeatRangeUncontrolled.ts';
import { SelectControl } from '../SelectControl.ts';
import { Subtree } from '../Subtree.ts';
import { TriggerControl } from '../TriggerControl.ts';
import { UploadControl } from '../unsupported/UploadControl.ts';
import { collectChildInputs } from './collectChildInputs.ts';

const isSubtreeDefinition = (
definition: ModelSubtreeDefinition
@@ -152,55 +128,13 @@ const isUploadNodeDefinition = (
};

export const buildChildren = (parent: GeneralParentNode): GeneralChildNode[] => {
/**
* Child nodesets are collected from the {@link parent}'s
* {@link NodeDefinition.template}, ensuring that we produce
* {@link InstanceNode}s for every **model-defined** node, even if a
* corresponding node was not serialized in a {@link parent.instanceNode}.
*
* In other words, by referencing the model-defined template, we are able to
* reproduce nodes which were omitted as non-relevant in a prior serialization
* and/or submission.
*/
const childNodesets = Array.from(
new Set(
parent.definition.template.childElements.map((childElement) => {
return childElement.nodeset;
})
)
);

let instanceChildrenByNodeset: InstanceNodesByNodeset | null;

if (parent.instanceNode == null) {
instanceChildrenByNodeset = null;
} else {
instanceChildrenByNodeset = groupChildElementsByNodeset(parent.instanceNode);
}

const { model } = parent.rootDocument;
const inputs = collectChildInputs(model, parent);

return childNodesets.map((nodeset): GeneralChildNode => {
/**
* Get children of the target nodeset from {@link parent.instanceNode}, if
* that node exists, and if children with that nodeset exist.
*
* If either does not exist (e.g. it was omitted as non-relevant in a prior
* serialization), we continue to reference model-defined templates as we
* recurse down the {@link InstanceNode} subtree.
*
* @see {@link childNodesets}
*/
const instanceNodes = instanceChildrenByNodeset?.get(nodeset) ?? [];
return inputs.map(({ instanceNodes, definition }): GeneralChildNode => {
const [instanceNode = null] = instanceNodes;

const definition = model.getNodeDefinition(nodeset);

switch (definition.type) {
case 'root': {
throw new ErrorProductionDesignPendingError();
}

case 'subtree': {
if (isSubtreeDefinition(definition)) {
return new Subtree(parent, instanceNode, definition);
111 changes: 111 additions & 0 deletions packages/xforms-engine/src/instance/children/collectChildInputs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import type { StaticDocument } from '../../integration/xpath/static-dom/StaticDocument.ts';
import type { StaticElement } from '../../integration/xpath/static-dom/StaticElement.ts';
import type { ModelDefinition } from '../../parse/model/ModelDefinition.ts';
import type {
AnyNodeDefinition,
ChildNodeDefinition,
NodeDefinition,
} from '../../parse/model/NodeDefinition.ts';
import type { InstanceNode } from '../abstract/InstanceNode.ts';
import type { GeneralParentNode } from '../hierarchy.ts';

/**
* Child nodesets are collected from the {@link parent}'s
* {@link NodeDefinition.template}, ensuring that we produce
* {@link InstanceNode}s for every **model-defined** node, even if a
* corresponding node was not serialized in a {@link parent.instanceNode}.
*
* In other words, by referencing the model-defined template, we are able to
* reproduce nodes which were omitted as non-relevant in a prior serialization
* and/or submission.
*
* @todo Since we're building an instance node's children from the nodesets of
* the model-defined node's children, we are _implicitly dropping_ any excess
* nodes from instance input (i.e. any children which don't have a corresponding
* model-defined nodeset). That's probably the right behavior, but we may want
* to warn for such nodes if/when we do drop them.
*/
const collectModelChildNodesets = (parentTemplate: StaticElement): readonly string[] => {
const nodesets = parentTemplate.childElements.map(({ nodeset }) => {
return nodeset;
});

return Array.from(new Set(nodesets));
};

type InstanceNodesByNodeset = ReadonlyMap<string, readonly [StaticElement, ...StaticElement[]]>;

const groupChildElementsByNodeset = (
parent: StaticDocument | StaticElement
): InstanceNodesByNodeset => {
const result = new Map<string, [StaticElement, ...StaticElement[]]>();

for (const child of parent.childElements) {
const { nodeset } = child;
const group = result.get(nodeset);

if (group == null) {
result.set(nodeset, [child]);
} else {
group.push(child);
}
}

return result;
};

type AssertChildNodeDefinition = (
definition: AnyNodeDefinition,
childNodeset: string
) => asserts definition is ChildNodeDefinition;

const assertChildNodeDefinition: AssertChildNodeDefinition = (definition, childNodeset) => {
if (definition.type === 'root') {
throw new Error(`Unexpected root definition for child nodeset: ${childNodeset}`);
}
};

interface InstanceNodeChildInput {
readonly childNodeset: string;
readonly definition: ChildNodeDefinition;
readonly instanceNodes: readonly StaticElement[];
}

export const collectChildInputs = (
model: ModelDefinition,
parent: GeneralParentNode
): readonly InstanceNodeChildInput[] => {
const childNodesets = collectModelChildNodesets(parent.definition.template);

let instanceChildren: InstanceNodesByNodeset | null;

if (parent.instanceNode == null) {
instanceChildren = null;
} else {
instanceChildren = groupChildElementsByNodeset(parent.instanceNode);
}

return childNodesets.map((childNodeset) => {
const definition = model.getNodeDefinition(childNodeset);

assertChildNodeDefinition(definition, childNodeset);

/**
* Get children of the target nodeset from {@link parent.instanceNode}, if
* that node exists, and if children with that nodeset exist.
*
* If either does not exist (e.g. it was omitted as non-relevant in a prior
* serialization), we continue to reference model-defined templates as we
* recurse down the {@link InstanceNode} subtree.
*
* @see {@link childNodesets}
*/
const instanceNodes = instanceChildren?.get(childNodeset) ?? [];

return {
childNodeset,
definition,
instanceNodes,
};
});
};
Original file line number Diff line number Diff line change
@@ -26,7 +26,7 @@ import { createNodeLabel } from '../../lib/reactivity/text/createNodeLabel.ts';
import { createAggregatedViolations } from '../../lib/reactivity/validation/createAggregatedViolations.ts';
import type { DescendantNodeSharedStateSpec } from '../abstract/DescendantNode.ts';
import { DescendantNode } from '../abstract/DescendantNode.ts';
import { buildChildren } from '../children.ts';
import { buildChildren } from '../children/buildChildren.ts';
import type { GeneralChildNode, RepeatRange } from '../hierarchy.ts';
import type { EvaluationContext } from '../internal-api/EvaluationContext.ts';
import type { ClientReactiveSerializableTemplatedNode } from '../internal-api/serialization/ClientReactiveSerializableTemplatedNode.ts';