Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
46 changes: 46 additions & 0 deletions packages/core/src/types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,52 @@ import {
SPEC_DATA_PART_TYPE,
} from "./types";
import type { Spec, SpecStreamLine, StreamChunk } from "./types";
import { DynamicValueSchema } from "./types";
import { resolveActionParam } from "./props";
import type { PropResolutionContext } from "./props";

describe("DynamicValueSchema matches resolveActionParam's runtime contract", () => {
const ctx: PropResolutionContext = {
stateModel: { form: { text: "hi" } },
repeatBasePath: "/todos/2",
repeatIndex: 2,
} as PropResolutionContext;

const shapes: [string, unknown][] = [
["string literal", "hello"],
["number literal", 42],
["boolean literal", true],
["null", null],
["$state expression", { $state: "/form/text" }],
["$item expression", { $item: "field" }],
["$index expression", { $index: true }],
["array of literals", ["a", "b"]],
["array of expressions", [{ $state: "/form/text" }, { $index: true }]],
[
"object with nested expression values",
{ id: { $state: "/form/text" }, label: "static" },
],
];

it.each(shapes)("accepts and resolves %s", (_name, value) => {
expect(DynamicValueSchema.safeParse(value).success).toBe(true);
// The runtime resolver must not throw on the same shape.
expect(() => resolveActionParam(value, ctx)).not.toThrow();
});

it("resolves the widened shapes the way the runtime documents them", () => {
expect(resolveActionParam({ $index: true }, ctx)).toBe(2);
expect(resolveActionParam(["a", "b"], ctx)).toEqual(["a", "b"]);
expect(resolveActionParam({ id: { $state: "/form/text" } }, ctx)).toEqual({
id: "hi",
});
});

it("rejects values outside the contract", () => {
expect(DynamicValueSchema.safeParse(undefined).success).toBe(false);
expect(DynamicValueSchema.safeParse(() => {}).success).toBe(false);
});
});

describe("getByPath", () => {
it("gets nested values with JSON pointer paths", () => {
Expand Down
10 changes: 9 additions & 1 deletion packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,21 @@ export type DynamicBoolean = DynamicValue<boolean>;

/**
* Zod schema for dynamic values
*
* Mirrors what `resolveActionParam` (and `resolvePropValue` underneath it)
* accepts at runtime: literals, `$state` / `$item` / `$index` expressions,
* arrays, and plain objects whose values are themselves dynamic values.
*/
export const DynamicValueSchema = z.union([
export const DynamicValueSchema: z.ZodType<DynamicValue> = z.union([
z.string(),
z.number(),
z.boolean(),
z.null(),
z.object({ $state: z.string() }),
z.object({ $item: z.string() }),
z.object({ $index: z.literal(true) }),
z.lazy(() => z.array(DynamicValueSchema)),
z.lazy(() => z.record(z.string(), DynamicValueSchema)),
]);

export const DynamicStringSchema = z.union([
Expand Down