Skip to content

Commit

Permalink
Merge pull request #226 from getodk/features/engine/submission-serial…
Browse files Browse the repository at this point in the history
…ization

Initial engine support for submissions
  • Loading branch information
eyelidlessness authored Oct 21, 2024
2 parents c9a0ee3 + 8edf375 commit 9d83384
Show file tree
Hide file tree
Showing 63 changed files with 2,419 additions and 222 deletions.
8 changes: 8 additions & 0 deletions .changeset/yellow-tomatoes-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@getodk/xforms-engine': minor
'@getodk/scenario': minor
'@getodk/ui-solid': patch
'@getodk/common': patch
---

Initial engine support for preparing submissions
7 changes: 7 additions & 0 deletions packages/common/src/lib/type-assertions/assertNull.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export type AssertNull = (value: unknown) => asserts value is null;

export const assertNull: AssertNull = (value) => {
if (value !== null) {
throw new Error('Not null');
}
};
9 changes: 9 additions & 0 deletions packages/common/src/lib/type-assertions/assertUnknownArray.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
type UnknownArray = readonly unknown[];

type AssertUnknownArray = (value: unknown) => asserts value is UnknownArray;

export const assertUnknownArray: AssertUnknownArray = (value) => {
if (!Array.isArray(value)) {
throw new Error('Not an array');
}
};
8 changes: 5 additions & 3 deletions packages/common/src/test/assertions/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ export { instanceAssertion } from './instanceAssertion.ts';
export { typeofAssertion } from './typeofAssertion.ts';
export { ArbitraryConditionExpectExtension } from './vitest/ArbitraryConditionExpectExtension.ts';
export { AsymmetricTypedExpectExtension } from './vitest/AsymmetricTypedExpectExtension.ts';
export { InspectableComparisonError } from './vitest/InspectableComparisonError.ts';
export { StaticConditionExpectExtension } from './vitest/StaticConditionExpectExtension.ts';
export { SymmetricTypedExpectExtension } from './vitest/SymmetricTypedExpectExtension.ts';
export { AsyncAsymmetricTypedExpectExtension } from './vitest/AsyncAsymmetricTypedExpectExtension.ts';
export { extendExpect } from './vitest/extendExpect.ts';
export { InspectableComparisonError } from './vitest/InspectableComparisonError.ts';
export { InspectableStaticConditionError } from './vitest/InspectableStaticConditionError.ts';
export type {
CustomInspectable,
DeriveStaticVitestExpectExtension,
Inspectable,
} from './vitest/shared-extension-types.ts';
export { StaticConditionExpectExtension } from './vitest/StaticConditionExpectExtension.ts';
export { SymmetricTypedExpectExtension } from './vitest/SymmetricTypedExpectExtension.ts';
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { SyncExpectationResult } from 'vitest';
import type { AssertIs } from '../../../../types/assertions/AssertIs.ts';
import { expandAsyncExpectExtensionResult } from './expandAsyncExpectExtensionResult.ts';
import type { ExpectExtensionMethod, SimpleAssertionResult } from './shared-extension-types.ts';
import { validatedExtensionMethod } from './validatedExtensionMethod.ts';

/**
* Generalizes definition of a Vitest `expect` API extension where the assertion
* expects differing types for its `actual` and `expected` parameters, and:
*
* - Automatically perfoms runtime validation of those parameters, helping to
* ensure that the extensions' static types are consistent with the runtime
* values passed in a given test's assertions
*
* - Expands simplified assertion result types to the full interface expected by
* Vitest
*
* - Facilitates deriving and defining corresponding static types on the base
* `expect` type
*/
export class AsyncAsymmetricTypedExpectExtension<
Actual = unknown,
Expected = Actual,
Result extends SimpleAssertionResult = SimpleAssertionResult,
> {
readonly extensionMethod: ExpectExtensionMethod<unknown, unknown, Promise<SyncExpectationResult>>;

constructor(
readonly validateActualArgument: AssertIs<Actual>,
readonly validateExpectedArgument: AssertIs<Expected>,
extensionMethod: ExpectExtensionMethod<Actual, Expected, Promise<Result>>
) {
const validatedMethod = validatedExtensionMethod(
validateActualArgument,
validateExpectedArgument,
extensionMethod
);

this.extensionMethod = expandAsyncExpectExtensionResult(validatedMethod);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { SyncExpectationResult } from 'vitest';
import type { expandSimpleExpectExtensionResult } from './expandSimpleExpectExtensionResult.ts';
import { isErrorLike } from './isErrorLike.ts';
import type { ExpectExtensionMethod, SimpleAssertionResult } from './shared-extension-types.ts';

/**
* Asynchronous counterpart to {@link expandSimpleExpectExtensionResult}
*/
export const expandAsyncExpectExtensionResult = <Actual, Expected>(
simpleMethod: ExpectExtensionMethod<Actual, Expected, Promise<SimpleAssertionResult>>
): ExpectExtensionMethod<Actual, Expected, Promise<SyncExpectationResult>> => {
return async (actual, expected) => {
const simpleResult = await simpleMethod(actual, expected);

const pass = simpleResult === true;

if (pass) {
return {
pass,
/**
* @todo It was previously assumed that it would never occur that an
* assertion would pass, and that Vitest would then produce a message
* for that. In hindsight, it makes sense that this case occurs in
* negated assertions (e.g.
* `expect(...).not.toPassSomeCustomAssertion`). It seems
* {@link SimpleAssertionResult} is not a good way to model the
* generalization, and that we may want a more uniform `AssertionResult`
* type which always includes both `pass` and `message` capabilities.
* This is should probably be addressed before we merge the big JR port
* PR, but is being temporarily put aside to focus on porting tests in
* bulk in anticipation of a scope change/hopefully-temporary
* interruption of momentum.
*/
message: () => {
throw new Error('Unsupported `SimpleAssertionResult` runtime value');
},
};
}

let message: () => string;

if (isErrorLike(simpleResult)) {
message = () => simpleResult.message;
} else {
message = () => simpleResult;
}

return {
pass,
message,
};
};
};
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
import type { SyncExpectationResult } from 'vitest';
import type {
ErrorLike,
ExpectExtensionMethod,
SimpleAssertionResult,
} from './shared-extension-types.ts';

const isErrorLike = (result: SimpleAssertionResult): result is ErrorLike => {
return typeof result === 'object' && typeof result.message === 'string';
};
import { isErrorLike } from './isErrorLike.ts';
import type { ExpectExtensionMethod, SimpleAssertionResult } from './shared-extension-types.ts';

/**
* Where Vitest assertion extends may be defined to return a
Expand Down
5 changes: 5 additions & 0 deletions packages/common/src/test/assertions/vitest/isErrorLike.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { ErrorLike, SimpleAssertionResult } from './shared-extension-types.ts';

export const isErrorLike = (result: SimpleAssertionResult): result is ErrorLike => {
return typeof result === 'object' && typeof result.message === 'string';
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { JSONValue } from '../../../../types/JSONValue.ts';
import type { Primitive } from '../../../../types/Primitive.ts';
import type { ArbitraryConditionExpectExtension } from './ArbitraryConditionExpectExtension.ts';
import type { AsymmetricTypedExpectExtension } from './AsymmetricTypedExpectExtension.ts';
import type { AsyncAsymmetricTypedExpectExtension } from './AsyncAsymmetricTypedExpectExtension.ts';
import type { StaticConditionExpectExtension } from './StaticConditionExpectExtension.ts';
import type { SymmetricTypedExpectExtension } from './SymmetricTypedExpectExtension.ts';

Expand Down Expand Up @@ -37,14 +38,26 @@ export type ExpectExtensionMethod<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type TypedExpectExtension<Actual = any, Expected = Actual> =
| AsymmetricTypedExpectExtension<Actual, Expected>
| AsyncAsymmetricTypedExpectExtension<Actual, Expected>
| SymmetricTypedExpectExtension<Expected>;

export type UntypedExpectExtensionFunction = ExpectExtensionMethod<
type AsyncUntypedExpectExtensionFunction = ExpectExtensionMethod<
unknown,
unknown,
Promise<SyncExpectationResult>
>;

type SyncUntypedExpectExtensionFunction = ExpectExtensionMethod<
unknown,
unknown,
SyncExpectationResult
>;

// prettier-ignore
export type UntypedExpectExtensionFunction =
| AsyncUntypedExpectExtensionFunction
| SyncUntypedExpectExtensionFunction;

export interface UntypedExpectExtensionObject {
readonly extensionMethod: UntypedExpectExtensionFunction;
}
Expand All @@ -54,7 +67,10 @@ export type UntypedExpectExtension =
| UntypedExpectExtensionFunction
| UntypedExpectExtensionObject;

export type ExpectExtension = TypedExpectExtension | UntypedExpectExtension;
// prettier-ignore
export type ExpectExtension =
| TypedExpectExtension
| UntypedExpectExtension;

export type ExpectExtensionRecord<MethodName extends string> = {
[K in MethodName]: ExpectExtension;
Expand All @@ -68,7 +84,10 @@ export type DeriveStaticVitestExpectExtension<
> = {
[K in keyof Implementation]:
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Implementation[K] extends ArbitraryConditionExpectExtension<any>
Implementation[K] extends AsyncAsymmetricTypedExpectExtension<any, infer Expected>
? (expected: Expected) => Promise<VitestParameterizedReturn>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
: Implementation[K] extends ArbitraryConditionExpectExtension<any>
? () => VitestParameterizedReturn
// eslint-disable-next-line @typescript-eslint/no-explicit-any
: Implementation[K] extends StaticConditionExpectExtension<any, any>
Expand Down
42 changes: 2 additions & 40 deletions packages/scenario/src/answer/ComparableAnswer.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
import type { SimpleAssertionResult } from '@getodk/common/test/assertions/vitest/shared-extension-types.ts';
import type { JSONValue } from '@getodk/common/types/JSONValue.ts';
import { ComparableAssertableValue } from '../comparable/ComparableAssertableValue.ts';
import type { Scenario } from '../jr/Scenario.ts';

interface OptionalBooleanComparable {
// Expressed here so it can be overridden as either a `readonly` property or
// as a `get` accessor
readonly booleanValue?: boolean;
}

/**
* Provides a common interface for comparing "answer" values of arbitrary data
* types, where the answer may be obtained from:
Expand All @@ -21,36 +14,5 @@ interface OptionalBooleanComparable {
* {@link https://vitest.dev/guide/extending-matchers.html | extended}
* assertions/matchers.
*/
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging -- see OptionalBooleanComparable.booleanValue
export abstract class ComparableAnswer implements OptionalBooleanComparable {
abstract get stringValue(): string;

// To be overridden
equals(
// @ts-expect-error -- part of the interface to be overridden
// eslint-disable-next-line @typescript-eslint/no-unused-vars
answer: ComparableAnswer
): SimpleAssertionResult | null {
return null;
}

/**
* Note: we currently return {@link stringValue} here, but this probably
* won't last as we expand support for other data types. This is why the
* return type is currently `unknown`.
*/
getValue(): unknown {
return this.stringValue;
}

inspectValue(): JSONValue {
return this.stringValue;
}

toString(): string {
return this.stringValue;
}
}

// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging -- see OptionalBooleanComparable.booleanValue
export interface ComparableAnswer extends OptionalBooleanComparable {}
export abstract class ComparableAnswer extends ComparableAssertableValue {}
Loading

0 comments on commit 9d83384

Please sign in to comment.