Skip to content

Commit dc696d2

Browse files
authored
Zod-like type inference, but for JSONSchema (#490)
* Zod-like type inference for our JsonSchema variant, including Cell support * add type inference to recipe, lift, handler, .asCell, .asSchema and many more * added `schema` helper so we don't have to write `as const as JSONSchema` + allow `example` in schema * added `asStream` support
1 parent 26b7862 commit dc696d2

File tree

17 files changed

+1770
-90
lines changed

17 files changed

+1770
-90
lines changed

typescript/packages/common-builder/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export {
5858
unsafe_parentRecipe,
5959
type UnsafeBinding,
6060
} from "./types.ts";
61+
export { type Schema, schema } from "./schema-to-ts.ts";
6162

6263
// This should be a separate package, but for now it's easier to keep it here.
6364
export {

typescript/packages/common-builder/src/module.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
traverseValue,
1818
} from "./utils.ts";
1919
import { getTopFrame } from "./recipe.ts";
20+
import { Schema, SchemaWithoutCell } from "./schema-to-ts.ts";
2021
import { z } from "zod";
2122
import { zodToJsonSchema } from "zod-to-json-schema";
2223

@@ -45,11 +46,14 @@ export function createNodeFactory<T = any, R = any>(
4546
*
4647
* @returns A module node factory that also serializes as module.
4748
*/
48-
export function lift<T, R>(
49-
argumentSchema: JSONSchema,
50-
resultSchema: JSONSchema,
51-
implementation: (input: T) => R,
52-
): ModuleFactory<T, R>;
49+
export function lift<
50+
T extends JSONSchema = JSONSchema,
51+
R extends JSONSchema = JSONSchema,
52+
>(
53+
argumentSchema: T,
54+
resultSchema: R,
55+
implementation: (input: Schema<T>) => Schema<R>,
56+
): ModuleFactory<SchemaWithoutCell<T>, SchemaWithoutCell<R>>;
5357
export function lift<T extends z.ZodTypeAny, R extends z.ZodTypeAny>(
5458
argumentSchema: T,
5559
resultSchema: R,
@@ -97,6 +101,14 @@ export function byRef<T, R>(ref: string): ModuleFactory<T, R> {
97101
});
98102
}
99103

104+
export function handler<
105+
E extends JSONSchema = JSONSchema,
106+
T extends JSONSchema = JSONSchema,
107+
>(
108+
eventSchema: E,
109+
stateSchema: T,
110+
handler: (event: Schema<E>, props: Schema<T>) => any,
111+
): HandlerFactory<SchemaWithoutCell<T>, SchemaWithoutCell<E>>;
100112
export function handler<E, T>(
101113
eventSchema: JSONSchema,
102114
stateSchema: JSONSchema,

typescript/packages/common-builder/src/recipe.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
isOpaqueRef,
55
isShadowRef,
66
type JSONSchema,
7+
type JSONSchemaWritable,
78
makeOpaqueRef,
89
type Module,
910
type Node,
@@ -27,6 +28,7 @@ import {
2728
toJSONWithAliases,
2829
traverseValue,
2930
} from "./utils.ts";
31+
import { SchemaWithoutCell } from "./schema-to-ts.ts";
3032
import { z } from "zod";
3133
import { zodToJsonSchema } from "zod-to-json-schema";
3234

@@ -71,6 +73,21 @@ export function recipe<T, R>(
7173
resultSchema: JSONSchema,
7274
fn: (input: OpaqueRef<Required<T>>) => Opaque<R>,
7375
): RecipeFactory<T, R>;
76+
export function recipe<S extends JSONSchema>(
77+
argumentSchema: S,
78+
fn: (input: OpaqueRef<Required<SchemaWithoutCell<S>>>) => any,
79+
): RecipeFactory<SchemaWithoutCell<S>, ReturnType<typeof fn>>;
80+
export function recipe<S extends JSONSchema, R>(
81+
argumentSchema: S,
82+
fn: (input: OpaqueRef<Required<SchemaWithoutCell<S>>>) => Opaque<R>,
83+
): RecipeFactory<SchemaWithoutCell<S>, R>;
84+
export function recipe<S extends JSONSchema, RS extends JSONSchema>(
85+
argumentSchema: S,
86+
resultSchema: RS,
87+
fn: (
88+
input: OpaqueRef<Required<SchemaWithoutCell<S>>>,
89+
) => Opaque<SchemaWithoutCell<RS>>,
90+
): RecipeFactory<SchemaWithoutCell<S>, SchemaWithoutCell<RS>>;
7491
export function recipe<T, R>(
7592
argumentSchema: string | JSONSchema | z.ZodTypeAny,
7693
resultSchema:
@@ -246,15 +263,15 @@ function factoryFromRecipe<T, R>(
246263
if (external) setValueAtPath(initial, paths.get(cell)!, external);
247264
});
248265

249-
let argumentSchema: JSONSchema;
266+
let argumentSchema: JSONSchemaWritable;
250267

251268
if (typeof argumentSchemaArg === "string") {
252269
// TODO(seefeld): initial is likely not needed anymore
253-
// TODO(seefeld): But we need a new one for the result
270+
// TODO(seefeld): But we need a derived schema for the result
254271
argumentSchema = createJsonSchema(defaults, {});
255272
argumentSchema.description = argumentSchemaArg;
256273

257-
delete argumentSchema.properties?.[UI]; // TODO(seefeld): This should be a schema for views
274+
delete (argumentSchema.properties as any)?.[UI]; // TODO(seefeld): This should be a schema for views
258275
if (argumentSchema.properties?.internal?.properties) {
259276
for (
260277
const key of Object.keys(
@@ -269,15 +286,15 @@ function factoryFromRecipe<T, R>(
269286
} else if (argumentSchemaArg instanceof z.ZodType) {
270287
argumentSchema = zodToJsonSchema(argumentSchemaArg) as JSONSchema;
271288
} else {
272-
argumentSchema = argumentSchemaArg as unknown as JSONSchema;
289+
argumentSchema = argumentSchemaArg;
273290
}
274291

275292
const resultSchema: JSONSchema = resultSchemaArg instanceof z.ZodType
276293
? (zodToJsonSchema(resultSchemaArg) as JSONSchema)
277294
: (resultSchemaArg ?? ({} as JSONSchema));
278295

279296
const serializedNodes = Array.from(nodes).map((node) => {
280-
const module = toJSONWithAliases(node.module, paths) as Module;
297+
const module = toJSONWithAliases(node.module, paths) as unknown as Module;
281298
const inputs = toJSONWithAliases(node.inputs, paths)!;
282299
const outputs = toJSONWithAliases(node.outputs, paths)!;
283300
return { module, inputs, outputs } satisfies Node;

0 commit comments

Comments
 (0)