mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-04-18 22:54:41 +00:00
feat(schema): scaffold effect-to-zod bridge (#17273)
This commit is contained in:
92
packages/opencode/src/util/effect-zod.ts
Normal file
92
packages/opencode/src/util/effect-zod.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { Schema, SchemaAST } from "effect"
|
||||||
|
import z from "zod"
|
||||||
|
|
||||||
|
export function zod<S extends Schema.Top>(schema: S): z.ZodType<Schema.Schema.Type<S>> {
|
||||||
|
return walk(schema.ast) as z.ZodType<Schema.Schema.Type<S>>
|
||||||
|
}
|
||||||
|
|
||||||
|
function walk(ast: SchemaAST.AST): z.ZodTypeAny {
|
||||||
|
const out = body(ast)
|
||||||
|
const desc = SchemaAST.resolveDescription(ast)
|
||||||
|
const ref = SchemaAST.resolveIdentifier(ast)
|
||||||
|
const next = desc ? out.describe(desc) : out
|
||||||
|
return ref ? next.meta({ ref }) : next
|
||||||
|
}
|
||||||
|
|
||||||
|
function body(ast: SchemaAST.AST): z.ZodTypeAny {
|
||||||
|
if (SchemaAST.isOptional(ast)) return opt(ast)
|
||||||
|
|
||||||
|
switch (ast._tag) {
|
||||||
|
case "String":
|
||||||
|
return z.string()
|
||||||
|
case "Number":
|
||||||
|
return z.number()
|
||||||
|
case "Boolean":
|
||||||
|
return z.boolean()
|
||||||
|
case "Null":
|
||||||
|
return z.null()
|
||||||
|
case "Undefined":
|
||||||
|
return z.undefined()
|
||||||
|
case "Any":
|
||||||
|
case "Unknown":
|
||||||
|
return z.unknown()
|
||||||
|
case "Never":
|
||||||
|
return z.never()
|
||||||
|
case "Literal":
|
||||||
|
return z.literal(ast.literal)
|
||||||
|
case "Union":
|
||||||
|
return union(ast)
|
||||||
|
case "Objects":
|
||||||
|
return object(ast)
|
||||||
|
case "Arrays":
|
||||||
|
return array(ast)
|
||||||
|
case "Declaration":
|
||||||
|
return decl(ast)
|
||||||
|
default:
|
||||||
|
return fail(ast)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function opt(ast: SchemaAST.AST): z.ZodTypeAny {
|
||||||
|
if (ast._tag !== "Union") return fail(ast)
|
||||||
|
const items = ast.types.filter((item) => item._tag !== "Undefined")
|
||||||
|
if (items.length === 1) return walk(items[0]).optional()
|
||||||
|
if (items.length > 1)
|
||||||
|
return z.union(items.map(walk) as [z.ZodTypeAny, z.ZodTypeAny, ...Array<z.ZodTypeAny>]).optional()
|
||||||
|
return z.undefined().optional()
|
||||||
|
}
|
||||||
|
|
||||||
|
function union(ast: SchemaAST.Union): z.ZodTypeAny {
|
||||||
|
const items = ast.types.map(walk)
|
||||||
|
if (items.length === 1) return items[0]
|
||||||
|
if (items.length < 2) return fail(ast)
|
||||||
|
return z.union(items as [z.ZodTypeAny, z.ZodTypeAny, ...Array<z.ZodTypeAny>])
|
||||||
|
}
|
||||||
|
|
||||||
|
function object(ast: SchemaAST.Objects): z.ZodTypeAny {
|
||||||
|
if (ast.propertySignatures.length === 0 && ast.indexSignatures.length === 1) {
|
||||||
|
const sig = ast.indexSignatures[0]
|
||||||
|
if (sig.parameter._tag !== "String") return fail(ast)
|
||||||
|
return z.record(z.string(), walk(sig.type))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ast.indexSignatures.length > 0) return fail(ast)
|
||||||
|
|
||||||
|
return z.object(Object.fromEntries(ast.propertySignatures.map((sig) => [String(sig.name), walk(sig.type)])))
|
||||||
|
}
|
||||||
|
|
||||||
|
function array(ast: SchemaAST.Arrays): z.ZodTypeAny {
|
||||||
|
if (ast.elements.length > 0) return fail(ast)
|
||||||
|
if (ast.rest.length !== 1) return fail(ast)
|
||||||
|
return z.array(walk(ast.rest[0]))
|
||||||
|
}
|
||||||
|
|
||||||
|
function decl(ast: SchemaAST.Declaration): z.ZodTypeAny {
|
||||||
|
if (ast.typeParameters.length !== 1) return fail(ast)
|
||||||
|
return walk(ast.typeParameters[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
function fail(ast: SchemaAST.AST): never {
|
||||||
|
const ref = SchemaAST.resolveIdentifier(ast)
|
||||||
|
throw new Error(`unsupported effect schema: ${ref ?? ast._tag}`)
|
||||||
|
}
|
||||||
61
packages/opencode/test/util/effect-zod.test.ts
Normal file
61
packages/opencode/test/util/effect-zod.test.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { Schema } from "effect"
|
||||||
|
|
||||||
|
import { zod } from "../../src/util/effect-zod"
|
||||||
|
|
||||||
|
describe("util.effect-zod", () => {
|
||||||
|
test("converts class schemas for route dto shapes", () => {
|
||||||
|
class Method extends Schema.Class<Method>("ProviderAuthMethod")({
|
||||||
|
type: Schema.Union([Schema.Literal("oauth"), Schema.Literal("api")]),
|
||||||
|
label: Schema.String,
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
const out = zod(Method)
|
||||||
|
|
||||||
|
expect(out.meta()?.ref).toBe("ProviderAuthMethod")
|
||||||
|
expect(
|
||||||
|
out.parse({
|
||||||
|
type: "oauth",
|
||||||
|
label: "OAuth",
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
type: "oauth",
|
||||||
|
label: "OAuth",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("converts structs with optional fields, arrays, and records", () => {
|
||||||
|
const out = zod(
|
||||||
|
Schema.Struct({
|
||||||
|
foo: Schema.optional(Schema.String),
|
||||||
|
bar: Schema.Array(Schema.Number),
|
||||||
|
baz: Schema.Record(Schema.String, Schema.Boolean),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
out.parse({
|
||||||
|
bar: [1, 2],
|
||||||
|
baz: { ok: true },
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
bar: [1, 2],
|
||||||
|
baz: { ok: true },
|
||||||
|
})
|
||||||
|
expect(
|
||||||
|
out.parse({
|
||||||
|
foo: "hi",
|
||||||
|
bar: [1],
|
||||||
|
baz: { ok: false },
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
foo: "hi",
|
||||||
|
bar: [1],
|
||||||
|
baz: { ok: false },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("throws for unsupported tuple schemas", () => {
|
||||||
|
expect(() => zod(Schema.Tuple([Schema.String, Schema.Number]))).toThrow("unsupported effect schema")
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user