Skip to content

Commit

Permalink
Reusing common code between select and rank
Browse files Browse the repository at this point in the history
  • Loading branch information
latin-panda committed Jan 23, 2025
1 parent 5f9201d commit 8568fe0
Show file tree
Hide file tree
Showing 11 changed files with 146 additions and 153 deletions.
6 changes: 2 additions & 4 deletions packages/xforms-engine/src/error/RankValueTypeError.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { XFormsSpecViolationError } from './XFormsSpecViolationError.ts';
import type { RankDefinition } from '../client/RankNode.ts';
import type { UnsupportedRankValueType } from '../lib/codecs/select/BaseSelectCodec.ts';
import type { UnsupportedBaseItemValueType } from '../lib/codecs/BaseItemCodec.ts';
import { XPathFunctionalityError, type XPathFunctionalityErrorCategory } from './XPathFunctionalityError.ts';

export class RankValueTypeError extends XFormsSpecViolationError {
constructor(definition: RankDefinition<UnsupportedRankValueType>) {
// ToDo fix typing
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
constructor(definition: RankDefinition<UnsupportedBaseItemValueType>) {
const { valueType } = definition;

Check failure on line 8 in packages/xforms-engine/src/error/RankValueTypeError.ts

View workflow job for this annotation

GitHub Actions / Lint (global) (22.12.0)

Unsafe assignment of an error typed value

Check failure on line 8 in packages/xforms-engine/src/error/RankValueTypeError.ts

View workflow job for this annotation

GitHub Actions / Lint (global) (22.12.0)

Unsafe assignment of an error typed value

super(
Expand Down
4 changes: 2 additions & 2 deletions packages/xforms-engine/src/error/SelectValueTypeError.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { SelectDefinition } from '../client/SelectNode.ts';
import type { UnsupportedSelectValueType } from '../lib/codecs/select/BaseSelectCodec.ts';
import type { UnsupportedBaseItemValueType } from '../lib/codecs/BaseItemCodec.ts';
import { XFormsSpecViolationError } from './XFormsSpecViolationError.ts';

/**
Expand All @@ -12,7 +12,7 @@ import { XFormsSpecViolationError } from './XFormsSpecViolationError.ts';
* @see {@link https://github.com/getodk/web-forms/issues/276}
*/
export class SelectValueTypeError extends XFormsSpecViolationError {
constructor(definition: SelectDefinition<UnsupportedSelectValueType>) {
constructor(definition: SelectDefinition<UnsupportedBaseItemValueType>) {
const { valueType } = definition;

super(
Expand Down
8 changes: 4 additions & 4 deletions packages/xforms-engine/src/instance/RankControl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type {
} from '../client/RankNode.ts';
import type { TextRange } from '../client/TextRange.ts';
import type { XFormsXPathElement } from '../integration/xpath/adapter/XFormsXPathNode.ts';
import { createSelectItems } from '../lib/reactivity/createSelectItems.ts';
import { createItemset } from '../lib/reactivity/createBaseItemset.ts';
import type { CurrentState } from '../lib/reactivity/node-state/createCurrentState.ts';
import type { EngineState } from '../lib/reactivity/node-state/createEngineState.ts';
import type { SharedNodeState } from '../lib/reactivity/node-state/createSharedNodeState.ts';
Expand All @@ -25,7 +25,7 @@ import type { EvaluationContext } from './internal-api/EvaluationContext.ts';
import type { ValidationContext } from './internal-api/ValidationContext.ts';
import type { ClientReactiveSubmittableValueNode } from './internal-api/submission/ClientReactiveSubmittableValueNode.ts';
import { RankFunctionalityError, RankValueTypeError } from '../error/RankValueTypeError.ts';
import { ItemCollectionCodec } from '../lib/codecs/ItemCollectionCodec.ts';
import { BaseItemCollectionCodec } from '../lib/codecs/BaseItemCollectionCodec.ts';
import { sharedValueCodecs } from '../lib/codecs/getSharedValueCodec.ts';
import type { AnyNodeDefinition } from '../parse/model/NodeDefinition.ts';

Expand Down Expand Up @@ -74,10 +74,10 @@ export class RankControl
readonly currentState: CurrentState<RankControlStateSpec>;

private constructor(parent: GeneralParentNode, definition: RankDefinition<'string'>) {
const codec = new ItemCollectionCodec(sharedValueCodecs.string);
const codec = new BaseItemCollectionCodec(sharedValueCodecs.string);
super(parent, definition, codec);

const valueOptions = createSelectItems(this); // ToDo extract to reuse function
const valueOptions = createItemset(this, definition.bodyElement.itemset);

Check failure on line 80 in packages/xforms-engine/src/instance/RankControl.ts

View workflow job for this annotation

GitHub Actions / Lint (global) (22.12.0)

Unsafe argument of type error typed assigned to a parameter of type `ItemsetDefinition`

Check failure on line 80 in packages/xforms-engine/src/instance/RankControl.ts

View workflow job for this annotation

GitHub Actions / Lint (global) (22.12.0)

Unsafe member access .bodyElement on an `error` typed value

Check failure on line 80 in packages/xforms-engine/src/instance/RankControl.ts

View workflow job for this annotation

GitHub Actions / Lint (global) (22.12.0)

Unsafe argument of type error typed assigned to a parameter of type `ItemsetDefinition`

Check failure on line 80 in packages/xforms-engine/src/instance/RankControl.ts

View workflow job for this annotation

GitHub Actions / Lint (global) (22.12.0)

Unsafe member access .bodyElement on an `error` typed value
const mapOptionsByValue: Accessor<RankItemMap> = this.scope.runTask(() => {
return createMemo(() => {
return new Map(valueOptions().map((item) => [item.value, item]));
Expand Down
20 changes: 20 additions & 0 deletions packages/xforms-engine/src/lib/codecs/BaseItemCodec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { ValueType } from '../../client/ValueType.ts';
import type { SharedValueCodec } from './getSharedValueCodec.ts';
import { ValueArrayCodec } from './ValueArrayCodec.ts';
import type { CodecDecoder, CodecEncoder } from './ValueCodec.ts';

export type BaseItemValueType = 'string';

export type UnsupportedBaseItemValueType = Exclude<ValueType, BaseItemValueType>;

export abstract class BaseItemCodec<
Values extends readonly string[] = readonly string[],
> extends ValueArrayCodec<BaseItemValueType, Values> {
constructor(
baseCodec: SharedValueCodec<'string'>,
encodeValue: CodecEncoder<Values>,
decodeValue: CodecDecoder<Values>
) {
super(baseCodec, encodeValue, decodeValue);
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { xmlXPathWhitespaceSeparatedList } from '@getodk/common/lib/string/whitespace.ts';
import type { SharedValueCodec } from './getSharedValueCodec.ts';
import type { CodecDecoder, CodecEncoder } from './ValueCodec.ts';
import { BaseSelectCodec } from './select/BaseSelectCodec.ts';
import { BaseItemCodec } from './BaseItemCodec.ts';

/**
* Value codec implementation for `<select>` and `<rank>` controls.
*
* This generalizes the application of a {@link SharedValueCodec} implementation
* over individual select values, where those values are serialized as a
* over individual select and rank values, where those values are serialized as a
* whitespace-separated list. All other encoding and decoding logic is deferred
* to the provided {@link baseCodec}, ensuring that select value types are
* to the provided {@link baseCodec}, ensuring that select and rank value types are
* treated consistently with the same underlying data types for other controls.
*/
export class ItemCollectionCodec extends BaseSelectCodec<readonly string[]> {
export class BaseItemCollectionCodec extends BaseItemCodec<readonly string[]> {
constructor(baseCodec: SharedValueCodec<'string'>) {
const encodeValue: CodecEncoder<readonly string[]> = (value) => {
return value.join(' ');
Expand Down
22 changes: 0 additions & 22 deletions packages/xforms-engine/src/lib/codecs/select/BaseSelectCodec.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { SharedValueCodec } from '../getSharedValueCodec.ts';
import { type CodecDecoder, type CodecEncoder } from '../ValueCodec.ts';
import { BaseSelectCodec } from './BaseSelectCodec.ts';
import type { ItemCollectionCodec } from '../ItemCollectionCodec.ts';
import { BaseItemCodec } from '../BaseItemCodec.ts';
import type { BaseItemCollectionCodec } from '../BaseItemCollectionCodec.ts';

// prettier-ignore
export type SingleValueSelectRuntimeValues =
Expand Down Expand Up @@ -41,7 +41,7 @@ const encodeValueFactory = (
* Value codec implementation for `<select1>` controls.
*
* Note: this implementation is a specialization of the same principles
* underlying {@link ItemCollectionCodec}. It is implemented separately:
* underlying {@link BaseItemCollectionCodec}. It is implemented separately:
*
* 1. to address a semantic difference between `<select>` and `<select1>`
* values: the former are serialized as a space-separated list, but that does
Expand All @@ -50,7 +50,7 @@ const encodeValueFactory = (
* 2. as an optimization, as the more general implementation performs poorly on
* forms which we monitor for performance.
*/
export class SingleValueSelectCodec extends BaseSelectCodec<SingleValueSelectCodecValues> {
export class SingleValueSelectCodec extends BaseItemCodec<SingleValueSelectCodecValues> {
constructor(baseCodec: SharedValueCodec<'string'>) {
const encodeValue = encodeValueFactory(baseCodec);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { UnreachableError } from '@getodk/common/lib/error/UnreachableError.ts';
import type { SelectDefinition } from '../../../client/SelectNode.ts';
import { sharedValueCodecs } from '../getSharedValueCodec.ts';
import { ItemCollectionCodec } from '../ItemCollectionCodec.ts';
import { BaseItemCollectionCodec } from '../BaseItemCollectionCodec.ts';
import { SingleValueSelectCodec } from './SingleValueSelectCodec.ts';

const multipleValueSelectCodec = new ItemCollectionCodec(sharedValueCodecs.string);
const multipleValueSelectCodec = new BaseItemCollectionCodec(sharedValueCodecs.string);

const singleValueSelectCodec = new SingleValueSelectCodec(sharedValueCodecs.string);

// prettier-ignore
export type SelectCodec =
| ItemCollectionCodec
| BaseItemCollectionCodec
| SingleValueSelectCodec;

export const getSelectCodec = (definition: SelectDefinition<'string'>): SelectCodec => {
Expand Down
104 changes: 104 additions & 0 deletions packages/xforms-engine/src/lib/reactivity/createBaseItemset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { UpsertableMap } from '@getodk/common/lib/collections/UpsertableMap.ts';
import type { Accessor } from 'solid-js';
import { createMemo } from 'solid-js';
import type { ActiveLanguage } from '../../client/FormLanguage.ts';
import type { TextRange as ClientTextRange } from '../../client/TextRange.ts';
import type { EvaluationContext } from '../../instance/internal-api/EvaluationContext.ts';
import type { EngineXPathNode } from '../../integration/xpath/adapter/kind.ts';
import type { EngineXPathEvaluator } from '../../integration/xpath/EngineXPathEvaluator.ts';
import type { ItemsetDefinition } from '../../parse/body/control/ItemsetDefinition.ts';
import { createComputedExpression } from './createComputedExpression.ts';
import type { ReactiveScope } from './scope.ts';
import { createTextRange } from './text/createTextRange.ts';
import type { RankControl } from '../../instance/RankControl.ts';
import type { SelectControl } from '../../instance/SelectControl.ts';
import type { TranslationContext } from '../../instance/internal-api/TranslationContext.ts';
import { TextChunk } from '../../instance/text/TextChunk.ts';
import { TextRange } from '../../instance/text/TextRange.ts';

type ItemsetControl = SelectControl | RankControl;

Check warning on line 19 in packages/xforms-engine/src/lib/reactivity/createBaseItemset.ts

View workflow job for this annotation

GitHub Actions / Lint (global) (22.12.0)

Union type ItemsetControl constituents must be sorted

Check warning on line 19 in packages/xforms-engine/src/lib/reactivity/createBaseItemset.ts

View workflow job for this annotation

GitHub Actions / Lint (global) (22.12.0)

Union type ItemsetControl constituents must be sorted

class ItemsetItemEvaluationContext implements EvaluationContext {
readonly isAttached: Accessor<boolean>;
readonly scope: ReactiveScope;
readonly evaluator: EngineXPathEvaluator;
readonly contextReference: Accessor<string>;
readonly getActiveLanguage: Accessor<ActiveLanguage>;

constructor(control: ItemsetControl, readonly contextNode: EngineXPathNode) {
this.isAttached = control.isAttached;
this.scope = control.scope;
this.evaluator = control.evaluator;
this.contextReference = control.contextReference;
this.getActiveLanguage = control.getActiveLanguage;
}
}

type DerivedItemLabel = ClientTextRange<'item-label', 'form-derived'>;

export const derivedItemLabel = (context: TranslationContext, value: string): DerivedItemLabel => {
const chunk = new TextChunk(context, 'literal', value);

return new TextRange('form-derived', 'item-label', [chunk]);
};

const createItemsetItemLabel = (
context: EvaluationContext,
definition: ItemsetDefinition,
itemValue: Accessor<string>
): Accessor<ClientTextRange<'item-label'>> => {
const { label } = definition;

if (label == null) {
return createMemo(() => derivedItemLabel(context, itemValue()));
}

return createTextRange(context, 'item-label', label);
};

interface ItemsetItem {
label(): ClientTextRange<'item-label'>;
value(): string;
}

const createItemsetItems = (control: ItemsetControl, itemset: ItemsetDefinition): Accessor<readonly ItemsetItem[]> => {
return control.scope.runTask(() => {
const itemNodes = createComputedExpression(control, itemset.nodes, { defaultValue: [] });
const itemsCache = new UpsertableMap<EngineXPathNode, ItemsetItem>();

return createMemo(() => {
return itemNodes().map((itemNode) => {
return itemsCache.upsert(itemNode, () => {
const context = new ItemsetItemEvaluationContext(control, itemNode);
const value = createComputedExpression(context, itemset.value, { defaultValue: '' });
const label = createItemsetItemLabel(context, itemset, value);

return { label, value };
});
});
});
});
};

export interface SourceValueItem {
readonly value: string;
readonly label: ClientTextRange<'item-label'>;
}

export const createItemset = (
control: ItemsetControl,
itemset: ItemsetDefinition,
): Accessor<readonly SourceValueItem[]> => {
return control.scope.runTask(() => {
const itemsetItems = createItemsetItems(control, itemset);

return createMemo(() => {
return itemsetItems().map((item) => {
return {
label: item.label(),
value: item.value(),
};
});
});
});
};
Loading

0 comments on commit 8568fe0

Please sign in to comment.