From 1aa3c8664d714d08982c4848d3813ff7206d9cb1 Mon Sep 17 00:00:00 2001 From: eyelidlessness Date: Mon, 1 Jul 2024 14:22:32 -0700 Subject: [PATCH] Initial engine/client API for `constraint`/`required` validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As discussed in #140 Based on 4c38a22, with some refinements: - Each condition’s validity state is now represented by `valid: boolean` - Minor naming adjustments - Nailed down an initial pair of message constants for engine-fallback messages - Added a method to check whether a message is an engine-fallback, in a way that allows type narrowing and produces those message constants as types - Added a ton of JSDoc documentation clarifying the design, expected usage, some anticipated performance caveats, etc --- packages/xforms-engine/src/client/BaseNode.ts | 80 ++++++- .../xforms-engine/src/client/GroupNode.ts | 2 + .../src/client/RepeatInstanceNode.ts | 2 + .../src/client/RepeatRangeNode.ts | 2 + packages/xforms-engine/src/client/RootNode.ts | 2 + .../xforms-engine/src/client/SelectNode.ts | 4 +- .../xforms-engine/src/client/StringNode.ts | 2 + .../xforms-engine/src/client/SubtreeNode.ts | 2 + .../xforms-engine/src/client/TextRange.ts | 101 ++++++++- .../xforms-engine/src/client/constants.ts | 10 + .../xforms-engine/src/client/validation.ts | 201 ++++++++++++++++++ packages/xforms-engine/src/index.ts | 2 + 12 files changed, 400 insertions(+), 10 deletions(-) create mode 100644 packages/xforms-engine/src/client/constants.ts create mode 100644 packages/xforms-engine/src/client/validation.ts diff --git a/packages/xforms-engine/src/client/BaseNode.ts b/packages/xforms-engine/src/client/BaseNode.ts index 67193b470..c199623ca 100644 --- a/packages/xforms-engine/src/client/BaseNode.ts +++ b/packages/xforms-engine/src/client/BaseNode.ts @@ -4,6 +4,11 @@ import type { NodeAppearances } from './NodeAppearances.ts'; import type { OpaqueReactiveObjectFactory } from './OpaqueReactiveObjectFactory.ts'; import type { TextRange } from './TextRange.ts'; import type { InstanceNodeType } from './node-types.ts'; +import type { + AncestorNodeValidationState, + LeafNodeValidationState, + NodeValidationState, +} from './validation.ts'; export interface BaseNodeState { /** @@ -43,11 +48,17 @@ export interface BaseNodeState { */ get relevant(): boolean; - // Note: according to spec, `required` is NOT inherited from ancestor nodes. - // What this means for a `required` state on subtree nodes is an open - // question. It was also raised on the first engine-internals iteration, and I - // could have sworn it was discussed in that PR, but finding any record of - // this discussion has proven elusive. + /** + * Specifies whether the node must have a non-blank value to be valid (see + * {@link value} for details). + * + * @see {@link https://getodk.github.io/xforms-spec/#bind-attributes} + * + * @default false + * + * @todo What is the expected behavior of `required` expressions defined for + * non-leaf/value nodes? + */ get required(): boolean; /** @@ -106,6 +117,14 @@ export interface BaseNodeState { * * Parent nodes, i.e. nodes which can contain {@link children}, do not store a * value state. For those nodes, their value state should always be `null`. + * + * A node's value is considered "blank" when its primary instance state is an + * empty string, and it is considered "non-blank" otherwise. The engine may + * represent node values according to aspects of the node's definition (such + * as its defined data type, its associated control type if any). The node's + * value being blank or non-blank may contribute to satisfying conditions of + * the node's validity ({@link constraint}, {@link required}). Otherwise, it + * is an internal engine consideration. */ get value(): unknown; } @@ -181,8 +200,55 @@ export interface BaseNode { readonly parent: BaseNode | null; /** - * Each node provides a discrete object representing the stateful aspects of - * that node which will change over time. When a client provides a {@link OpaqueReactiveObjectFactory} + * Each node provides a discrete object representing the stateful aspects\* of + * that node which will change over time. When a client provides a + * {@link OpaqueReactiveObjectFactory}, the engine will update the properties + * of this object as their respective states change, so a client can implement + * reactive updates that respond to changes as they occur. + * + * \* This includes state which is either client-/user-mutable, or state which + * is computed based on the core XForms computation model. Each node also + * exposes {@link validationState}, which reflects the validity of the + * node, or its descendants. */ readonly currentState: BaseNodeState; + + /** + * Represents the validation state of a the node itself, or its descendants. + * + * @see {@link AncestorNodeValidationState} and + * {@link LeafNodeValidationState} for additional details. + * + * While filling a form (i.e. prior to submission), validation state can be + * viewed as computed metadata about the form state. The validation conditions + * and their violation messages produced by a node _may be computed on + * demand_. Clients should assume: + * + * 1. Validation state **will be current** when directly read by the client. + * Accessing validation state _may_ invoke engine computation of that state + * _at that time_. + * + * It **may** also be pre-computed by the engine so that direct reads are + * less computationally expensive, but such optimizations cannot be + * guaranteed by the engine at this time. + * + * 2. For clients providing an {@link OpaqueReactiveObjectFactory}, accessing + * validation state within a reactive context **will produce updates** to + * the validation state, as long as the client retains a subscription to + * that state. + * + * If it is possible to detect interruption of such client- reactive + * subscriptions, the engine _may defer computations_ until subsequent + * client read/re-subscription, in order to reduce unnecessary + * computational overhead. Again, such optimizations cannot be guaranteed + * by the engine at this time. + * + * @todo it's easier to conceive a reliable, general solution to optimizing + * the direct read case, than it is for the client-reactive case (largely + * because our solution for client reactivity is intentionally opaque). If it + * turns out that such optimizations are crucial for overall usability, the + * client-reactive case may best be served by additional APIs for reactive + * clients to explicitly pause and resume recomputation. + */ + readonly validationState: NodeValidationState; } diff --git a/packages/xforms-engine/src/client/GroupNode.ts b/packages/xforms-engine/src/client/GroupNode.ts index 637847ad6..0d1eae77f 100644 --- a/packages/xforms-engine/src/client/GroupNode.ts +++ b/packages/xforms-engine/src/client/GroupNode.ts @@ -4,6 +4,7 @@ import type { BaseNode, BaseNodeState } from './BaseNode.ts'; import type { NodeAppearances } from './NodeAppearances.ts'; import type { RootNode } from './RootNode.ts'; import type { GeneralChildNode, GeneralParentNode } from './hierarchy.ts'; +import type { AncestorNodeValidationState } from './validation.ts'; export interface GroupNodeState extends BaseNodeState { get hint(): null; @@ -34,4 +35,5 @@ export interface GroupNode extends BaseNode { readonly root: RootNode; readonly parent: GeneralParentNode; readonly currentState: GroupNodeState; + readonly validationState: AncestorNodeValidationState; } diff --git a/packages/xforms-engine/src/client/RepeatInstanceNode.ts b/packages/xforms-engine/src/client/RepeatInstanceNode.ts index 9692cd0ca..41308ca22 100644 --- a/packages/xforms-engine/src/client/RepeatInstanceNode.ts +++ b/packages/xforms-engine/src/client/RepeatInstanceNode.ts @@ -5,6 +5,7 @@ import type { NodeAppearances } from './NodeAppearances.ts'; import type { RepeatRangeNode } from './RepeatRangeNode.ts'; import type { RootNode } from './RootNode.ts'; import type { GeneralChildNode } from './hierarchy.ts'; +import type { AncestorNodeValidationState } from './validation.ts'; export interface RepeatInstanceNodeState extends BaseNodeState { // TODO(?): Previous iteration included an `index` getter here. I don't see it @@ -42,4 +43,5 @@ export interface RepeatInstanceNode extends BaseNode { readonly parent: RepeatRangeNode; readonly currentState: RepeatInstanceNodeState; + readonly validationState: AncestorNodeValidationState; } diff --git a/packages/xforms-engine/src/client/RepeatRangeNode.ts b/packages/xforms-engine/src/client/RepeatRangeNode.ts index 966be56b2..7696e48aa 100644 --- a/packages/xforms-engine/src/client/RepeatRangeNode.ts +++ b/packages/xforms-engine/src/client/RepeatRangeNode.ts @@ -5,6 +5,7 @@ import type { RepeatInstanceNode } from './RepeatInstanceNode.ts'; import type { RootNode } from './RootNode.ts'; import type { TextRange } from './TextRange.ts'; import type { GeneralParentNode } from './hierarchy.ts'; +import type { AncestorNodeValidationState } from './validation.ts'; export interface RepeatRangeNodeState extends BaseNodeState { get hint(): null; @@ -97,6 +98,7 @@ export interface RepeatRangeNode extends BaseNode { readonly root: RootNode; readonly parent: GeneralParentNode; readonly currentState: RepeatRangeNodeState; + readonly validationState: AncestorNodeValidationState; addInstances(afterIndex?: number, count?: number): RootNode; diff --git a/packages/xforms-engine/src/client/RootNode.ts b/packages/xforms-engine/src/client/RootNode.ts index be714374a..2f5599b34 100644 --- a/packages/xforms-engine/src/client/RootNode.ts +++ b/packages/xforms-engine/src/client/RootNode.ts @@ -3,6 +3,7 @@ import type { RootDefinition } from '../model/RootDefinition.ts'; import type { BaseNode, BaseNodeState } from './BaseNode.ts'; import type { ActiveLanguage, FormLanguage, FormLanguages } from './FormLanguage.ts'; import type { GeneralChildNode } from './hierarchy.ts'; +import type { AncestorNodeValidationState } from './validation.ts'; export interface RootNodeState extends BaseNodeState { /** @@ -47,6 +48,7 @@ export interface RootNode extends BaseNode { readonly root: RootNode; readonly parent: null; readonly currentState: RootNodeState; + readonly validationState: AncestorNodeValidationState; /** * @todo as with {@link RootNodeState.activeLanguage}, this is the most diff --git a/packages/xforms-engine/src/client/SelectNode.ts b/packages/xforms-engine/src/client/SelectNode.ts index ecb105aba..345da54c9 100644 --- a/packages/xforms-engine/src/client/SelectNode.ts +++ b/packages/xforms-engine/src/client/SelectNode.ts @@ -6,10 +6,11 @@ import type { RootNode } from './RootNode.ts'; import type { StringNode } from './StringNode.ts'; import type { TextRange } from './TextRange.ts'; import type { GeneralParentNode } from './hierarchy.ts'; +import type { LeafNodeValidationState } from './validation.ts'; export interface SelectItem { get value(): string; - get label(): TextRange<'label'> | null; + get label(): TextRange<'item-label'> | null; } export interface SelectNodeState extends BaseNodeState { @@ -48,6 +49,7 @@ export interface SelectNode extends BaseNode { readonly root: RootNode; readonly parent: GeneralParentNode; readonly currentState: SelectNodeState; + readonly validationState: LeafNodeValidationState; /** * For use by a client to update the selection of a select node where: diff --git a/packages/xforms-engine/src/client/StringNode.ts b/packages/xforms-engine/src/client/StringNode.ts index 740dcab68..03492d522 100644 --- a/packages/xforms-engine/src/client/StringNode.ts +++ b/packages/xforms-engine/src/client/StringNode.ts @@ -4,6 +4,7 @@ import type { BaseNode, BaseNodeState } from './BaseNode.ts'; import type { NodeAppearances } from './NodeAppearances.ts'; import type { RootNode } from './RootNode.ts'; import type { GeneralParentNode } from './hierarchy.ts'; +import type { LeafNodeValidationState } from './validation.ts'; export interface StringNodeState extends BaseNodeState { get children(): null; @@ -38,6 +39,7 @@ export interface StringNode extends BaseNode { readonly root: RootNode; readonly parent: GeneralParentNode; readonly currentState: StringNodeState; + readonly validationState: LeafNodeValidationState; /** * For use by a client to update the value of a string node. diff --git a/packages/xforms-engine/src/client/SubtreeNode.ts b/packages/xforms-engine/src/client/SubtreeNode.ts index d5dfe7522..0901a4770 100644 --- a/packages/xforms-engine/src/client/SubtreeNode.ts +++ b/packages/xforms-engine/src/client/SubtreeNode.ts @@ -2,6 +2,7 @@ import type { SubtreeDefinition as BaseSubtreeDefinition } from '../model/Subtre import type { BaseNode, BaseNodeState } from './BaseNode.ts'; import type { RootNode } from './RootNode.ts'; import type { GeneralChildNode, GeneralParentNode } from './hierarchy.ts'; +import type { AncestorNodeValidationState } from './validation.ts'; export interface SubtreeNodeState extends BaseNodeState { get label(): null; @@ -55,4 +56,5 @@ export interface SubtreeNode extends BaseNode { readonly root: RootNode; readonly parent: GeneralParentNode; readonly currentState: SubtreeNodeState; + readonly validationState: AncestorNodeValidationState; } diff --git a/packages/xforms-engine/src/client/TextRange.ts b/packages/xforms-engine/src/client/TextRange.ts index 1d4d3dcad..e2d814b37 100644 --- a/packages/xforms-engine/src/client/TextRange.ts +++ b/packages/xforms-engine/src/client/TextRange.ts @@ -1,7 +1,70 @@ import type { ActiveLanguage } from './FormLanguage.ts'; import type { RootNodeState } from './RootNode.ts'; -export type TextChunkSource = 'itext' | 'output' | 'static'; +/** + * **COMMENTARY** + * + * The spec makes naming and mapping these cases a bit more complex than would + * be ideal. The intent is to clearly identify distinct text definitions (and + * sub-structural parts) from a source form, in a way that semantically lines up + * with the ways they will need to be handled at runtime and conveyed to + * clients. This is the mapping: + * + * - 'output': All output values, i.e.: + * - `output/@value` + * + * - 'translation': + * + * - Valid XPath translation expressions, in a context accepting mixed + * translation/static syntax, i.e.: + * + * - `h:head//bind/@jr:constraintMsg[is-translation-expr()]` + * - `h:head//bind/@jr:requiredMsg[is-translation-expr()]` + * + * Here, `is-translation-expr()` is a fictional shorthand for checking + * that the attribute's value is a valid `jr:itext(...)` FunctionCall + * expression. Note that, per spec, these attributes **do not accept + * arbitrary XPath expressions**! The non-translation case is treated as + * static text, not parsed for e.g. an XPath [string] Literal expression. + * This is why we have introduced this 'translation' case, distinct from + * 'reference', which previously handled translated labels and hints. + * + * - Valid XPath translation expressions, in a context accepting arbitrary + * XPath expressions, i.e.: + * + * - `h:body//label/@ref[is-translation-expr()]` + * + * - 'static': + * - `h:head//bind/@jr:constraintMsg[not(is-translation-expr())]` + * - `h:head//bind/@jr:requiredMsg[not(is-translation-expr())]` + * - `h:body//label/text()` + * - `h:body//hint/text()` + * + * (See notes above for clarification of `is-translation-expr()`.) + * + * - 'reference': Any XPath **non-translation** expression defined as a label's + * (or hint's) `ref` attribute, i.e. + * - `h:body//label/@ref[not(is-translation-expr())]` + * - `h:body//hint/@ref[not(is-translation-expr())]` + * + * (See notes above for clarification of `is-translation-expr()`.) + * + * @todo It's unclear whether this will all become simpler or more compelex when + * we add support for outputs in translations. In theory, the actual translation + * `` nodes map quite well to the `TextRange` concept (i.e. they are a + * range of static and output chunks, just like labels and hints). The potential + * for complications arise from XPath implementation details being largely + * opaque (as in, the `jr:itext` implementation is encapsulated in the `xpath` + * package, and the engine doesn't really deal with itext translations at the + * node level at all). + */ +// prettier-ignore +export type TextChunkSource = + // eslint-disable-next-line @typescript-eslint/sort-type-constituents + | 'output' + | 'reference' + | 'translation' + | 'static'; /** * @todo This (and everything else to do with {@link TextRange}s is for @@ -23,6 +86,39 @@ export interface TextChunk { get formatted(): unknown; } +// eslint-disable-next-line @typescript-eslint/sort-type-constituents +export type ElementTextRole = 'hint' | 'label' | 'item-label'; +export type ValidationTextRole = 'constraintMsg' | 'requiredMsg'; +export type TextRole = ElementTextRole | ValidationTextRole; + +/** + * Specifies the origin of a {@link TextRange}. + * + * - 'form': text is computed from the form definition, as specified for the + * {@link TextRole}. User-facing clients should present text with this origin + * where appropriate. + * + * - 'form-derived': the form definition lacks a text definition for the + * {@link TextRole}, but an appropriate one has been derived from a related + * (and semantically appropriate) aspect of the form (example: a select item + * without a label may derive that label from the item's value). User-facing + * clients should generally present text with this origin where provided; this + * origin clarifies the source of such text. + * + * - 'engine': the form definition lacks a definition for the {@link TextRole}, + * but provides a constant default in its absence. User facing clients may + * disregard these constant text values, or may use them where a sensible + * default is desired. Clients may also use these constants as keys for + * translation purposes, as appropriate. Non-user facing clients may reference + * these constants for e.g. testing purposes. + */ +// prettier-ignore +export type TextOrigin = + // eslint-disable-next-line @typescript-eslint/sort-type-constituents + | 'form' + | 'form-derived' + | 'engine'; + /** * Represents aspects of a form which produce text, which _might_ be: * @@ -53,7 +149,8 @@ export interface TextChunk { * a text range's role may correspond to the "short" or "guidance" `form` of a * {@link https://getodk.github.io/xforms-spec/#languages | translation}). */ -export interface TextRange { +export interface TextRange { + readonly origin: Origin; readonly role: Role; [Symbol.iterator](): Iterable; diff --git a/packages/xforms-engine/src/client/constants.ts b/packages/xforms-engine/src/client/constants.ts new file mode 100644 index 000000000..0fb7f1a15 --- /dev/null +++ b/packages/xforms-engine/src/client/constants.ts @@ -0,0 +1,10 @@ +import type { ValidationTextRole } from './TextRange.ts'; + +export const VALIDATION_TEXT = { + constraintMsg: 'Condition not satisfied: constraint', + requiredMsg: 'Condition not satisfied: required', +} as const satisfies Record; + +type ValidationTextDefaults = typeof VALIDATION_TEXT; + +export type ValidationTextDefault = ValidationTextDefaults[Role]; diff --git a/packages/xforms-engine/src/client/validation.ts b/packages/xforms-engine/src/client/validation.ts new file mode 100644 index 000000000..ecd07d53c --- /dev/null +++ b/packages/xforms-engine/src/client/validation.ts @@ -0,0 +1,201 @@ +import type { NodeID } from '../instance/identity.ts'; +import type { BaseNode, BaseNodeState } from './BaseNode.ts'; +import type { OpaqueReactiveObjectFactory } from './OpaqueReactiveObjectFactory.ts'; +import type { RootNode } from './RootNode.ts'; +import type { TextRange } from './TextRange.ts'; +import type { AnyChildNode } from './hierarchy.ts'; + +// This interface exists so that extensions can share JSDoc for `valid`. +interface BaseValidity { + /** + * Specifies the unambiguous validity state for each validity condition of a + * given node, or for the derived validity of any parent node whose descendants + * are validated. + * + * For {@link ValidationCondition | form-defined conditions}, validity is + * determined as follows: + * + * + * expression | state | blank | non-blank + * ------------:|:----------|:-------:|:---------: + * `constraint` | `true`\* | ✅ | ✅ + * `constraint` | `false` | ✅ | ❌ + * `required` | `false`\* | ✅ | ✅ + * `required` | `true` | ❌ | ✅ + * + * - \* = default (expression not defined) + * - ✅ = `valid: true` + * - ❌ = `valid: false` + */ + readonly valid: boolean; +} + +/** + * Form-defined conditions which determine node validity. + * + * @see {@link https://getodk.github.io/xforms-spec/#bind-attributes | `constraint` and `required` bind attributes} + */ +export type ValidationCondition = 'constraint' | 'required'; + +interface ValidationConditionMessageRoles { + readonly constraint: 'constraintMsg'; + readonly required: 'requiredMsg'; +} + +export type ValidationConditionMessageRole = + ValidationConditionMessageRoles[Condition]; + +/** + * Source of a condition's violation message. + * + * - Form-defined messages (specified by the + * {@link https://getodk.github.io/xforms-spec/#bind-attributes | `jr:constraintMsg` and `jr:requiredMsg`} + * attributes) will be favored when provided by the form, and will be + * translated according to the form's active language (where applicable). + * + * - Otherwise, an engine-defined message will be provided as a fallback. This + * fallback is provided mainly for API consistency, and may be referenced for + * testing purposes; user-facing clients are expected to provide fallback + * messaging language most appropriate for their user neeeds. Engine-defined + * fallback messages **are not translated**. They are intended to be used, if + * at all, as sentinel values when a form-defined message is not available. + */ +// eslint-disable-next-line @typescript-eslint/sort-type-constituents +export type ViolationMessageSource = 'form' | 'engine'; + +/** + * @see {@link ViolationMessage.asString} + */ +// prettier-ignore +type ViolationMessageAsString< + Source extends ViolationMessageSource, + Condition extends ValidationCondition, +> = + Source extends 'form' + ? string + : `Condition not satisfied: ${Condition}`; + +/** + * A violation message is provided for every violation of a form-defined + * {@link ValidationCondition}. + */ +export interface ViolationMessage< + Condition extends ValidationCondition, + Source extends ViolationMessageSource = ViolationMessageSource, +> extends TextRange> { + /** + * - Form-defined violation messages may produce arbitrary text. This text may + * be translated + * ({@link https://getodk.github.io/xforms-spec/#fn:jr:itext | `jr:itext`}), + * and it may be dynamic (translations may reference form state with + * {@link https://getodk.github.io/xforms-spec/#body-elements | ``}). + * + * - When a form-defined violation message is not available, an engine-defined + * message will be provided in its place. Engine-defined violation messages + * are statically defined (and therefore not presently translated by the + * engine). Their static value can also be referenced as a static type, by + * checking {@link isFallbackMessage}. + */ + get asString(): ViolationMessageAsString; +} + +export interface ConditionSatisfied extends BaseValidity { + readonly condition: Condition; + readonly valid: true; + readonly message: null; +} + +export interface ConditionViolation extends BaseValidity { + readonly condition: Condition; + readonly valid: false; + readonly message: ViolationMessage; +} + +export type ConditionValidation = + | ConditionSatisfied + | ConditionViolation; + +export type AnyViolation = ConditionViolation; + +/** + * Represents the validation state of a leaf (or value) node. + * + * Validity is computed for two conditions: + * + * - {@link constraint}: arbitrary form-defined condition which specifies + * whether a (non-blank) value is considered valid + * + * - {@link required}: when a node is required, the node must have a non-blank + * value to be considered valid + * + * Only one of these conditions can be violated (applicability is mutually + * exclusive). As such, {@link violation} provides a convenient way to determine + * whether a leaf/value node is valid with a single (null) check. + * + * @see {@link BaseValidity.valid} for additional details on how these + * conditions are evaluated (and how they interact with one another). + */ +export interface LeafNodeValidationState { + get constraint(): ConditionValidation<'constraint'>; + get required(): ConditionValidation<'required'>; + + /** + * Violations are mutually exclusive: + * + * - {@link constraint} can only be violated by a non-blank value + * - {@link required} can only be violated by a blank value + * + * As such, at most one violation can be present. If none is present, + * the node is considered valid. + */ + get violation(): AnyViolation | null; +} + +/** + * Provides a reference to any leaf/value node which currently violates either + * of its validity conditions. + * + * Any client can safely assume: + * + * - {@link nodeId} will be a stable reference to a node with the same + * {@link BaseNode.nodeId | `nodeId`}. + * + * - {@link node} will have reference equality to the same node object, within + * the active form instance's {@link RootNode} tree + * + * - {@link reference} will be a **current** reference to the same node object's + * **computed** {@link BaseNodeState.reference | `currentState.reference`} + * + * Any client utilizing the engine's reactive APIs (having provided an + * {@link OpaqueReactiveObjectFactory}) can safely assume that {@link reference} + * will be recomputed and updated in tandem with the affected node's own + * computed `currentState.reference` as well. + * + * @todo this type currently exposes multiple ways to reference the affected + * node. This is intended to maximize flexibility: it's not yet clear how + * clients will be best served by which reference mechanism. It is expected that + * each property will be directly computed from the affected node. + */ +export interface DescendantNodeViolationReference { + readonly nodeId: NodeID; + + get reference(): string; + get node(): AnyChildNode; + get violation(): AnyViolation; +} + +/** + * Provides access from any ancestor/parent node, to identify any validity + * violations present on any of its leaf/value node descendants. + * + * @see {@link DescendantNodeViolationReference} for details on how descendants + * may be referenced when such a violation is present. + */ +export interface AncestorNodeValidationState { + get violations(): readonly DescendantNodeViolationReference[]; +} + +// prettier-ignore +export type NodeValidationState = + | AncestorNodeValidationState + | LeafNodeValidationState; diff --git a/packages/xforms-engine/src/index.ts b/packages/xforms-engine/src/index.ts index 60abdac06..17e8af131 100644 --- a/packages/xforms-engine/src/index.ts +++ b/packages/xforms-engine/src/index.ts @@ -14,6 +14,7 @@ export type * from './client/SelectNode.ts'; export type * from './client/StringNode.ts'; export type * from './client/SubtreeNode.ts'; export type * from './client/TextRange.ts'; +export * as constants from './client/constants.ts'; export type { AnyChildNode, AnyLeafNode, @@ -23,6 +24,7 @@ export type { GeneralParentNode, } from './client/hierarchy.ts'; export type * from './client/index.ts'; +export type * from './client/validation.ts'; // TODO: notwithstanding potential conflicts with parallel work on `web-forms` // (former `ui-vue`), these are the last remaining references **outside of