mirror of
https://fastgit.cc/https://github.com/anomalyco/opencode
synced 2026-04-21 05:10:58 +08:00
feat(effect-zod): add tuple support; migrate config/plugin to Effect Schema (#23178)
This commit is contained in:
@@ -31,7 +31,7 @@ export const TuiInfo = z
|
||||
$schema: z.string().optional(),
|
||||
theme: z.string().optional(),
|
||||
keybinds: KeybindOverride.optional(),
|
||||
plugin: ConfigPlugin.Spec.array().optional(),
|
||||
plugin: ConfigPlugin.Spec.zod.array().optional(),
|
||||
plugin_enabled: z.record(z.string(), z.boolean()).optional(),
|
||||
})
|
||||
.extend(TuiOptions.shape)
|
||||
|
||||
@@ -113,7 +113,7 @@ export const Info = z
|
||||
"Enable or disable snapshot tracking. When false, filesystem snapshots are not recorded and undoing or reverting will not undo/redo file changes. Defaults to true.",
|
||||
),
|
||||
// User-facing plugin config is stored as Specs; provenance gets attached later while configs are merged.
|
||||
plugin: ConfigPlugin.Spec.array().optional(),
|
||||
plugin: ConfigPlugin.Spec.zod.array().optional(),
|
||||
share: z
|
||||
.enum(["manual", "auto", "disabled"])
|
||||
.optional()
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import { Glob } from "@opencode-ai/shared/util/glob"
|
||||
import z from "zod"
|
||||
import { Schema } from "effect"
|
||||
import { pathToFileURL } from "url"
|
||||
import { isPathPluginSpec, parsePluginSpecifier, resolvePathPluginTarget } from "@/plugin/shared"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { withStatics } from "@/util/schema"
|
||||
import path from "path"
|
||||
|
||||
const Options = z.record(z.string(), z.unknown())
|
||||
export type Options = z.infer<typeof Options>
|
||||
export const Options = Schema.Record(Schema.String, Schema.Unknown).pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
export type Options = Schema.Schema.Type<typeof Options>
|
||||
|
||||
// Spec is the user-config value: either just a plugin identifier, or the identifier plus inline options.
|
||||
// It answers "what should we load?" but says nothing about where that value came from.
|
||||
export const Spec = z.union([z.string(), z.tuple([z.string(), Options])])
|
||||
export type Spec = z.infer<typeof Spec>
|
||||
export const Spec = Schema.Union([
|
||||
Schema.String,
|
||||
Schema.mutable(Schema.Tuple([Schema.String, Options])),
|
||||
]).pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
export type Spec = Schema.Schema.Type<typeof Spec>
|
||||
|
||||
export type Scope = "global" | "local"
|
||||
|
||||
|
||||
@@ -119,9 +119,16 @@ function object(ast: SchemaAST.Objects): z.ZodTypeAny {
|
||||
}
|
||||
|
||||
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]))
|
||||
// Pure variadic arrays: { elements: [], rest: [item] }
|
||||
if (ast.elements.length === 0) {
|
||||
if (ast.rest.length !== 1) return fail(ast)
|
||||
return z.array(walk(ast.rest[0]))
|
||||
}
|
||||
// Fixed-length tuples: { elements: [a, b, ...], rest: [] }
|
||||
// Tuples with a variadic tail (...rest) are not yet supported.
|
||||
if (ast.rest.length > 0) return fail(ast)
|
||||
const items = ast.elements.map(walk)
|
||||
return z.tuple(items as [z.ZodTypeAny, ...Array<z.ZodTypeAny>])
|
||||
}
|
||||
|
||||
function decl(ast: SchemaAST.Declaration): z.ZodTypeAny {
|
||||
|
||||
@@ -61,8 +61,32 @@ describe("util.effect-zod", () => {
|
||||
})
|
||||
})
|
||||
|
||||
test("throws for unsupported tuple schemas", () => {
|
||||
expect(() => zod(Schema.Tuple([Schema.String, Schema.Number]))).toThrow("unsupported effect schema")
|
||||
describe("Tuples", () => {
|
||||
test("fixed-length tuple parses matching array", () => {
|
||||
const out = zod(Schema.Tuple([Schema.String, Schema.Number]))
|
||||
expect(out.parse(["a", 1])).toEqual(["a", 1])
|
||||
expect(out.safeParse(["a"]).success).toBe(false)
|
||||
expect(out.safeParse(["a", "b"]).success).toBe(false)
|
||||
})
|
||||
|
||||
test("single-element tuple parses a one-element array", () => {
|
||||
const out = zod(Schema.Tuple([Schema.Boolean]))
|
||||
expect(out.parse([true])).toEqual([true])
|
||||
expect(out.safeParse([true, false]).success).toBe(false)
|
||||
})
|
||||
|
||||
test("tuple inside a union picks the right branch", () => {
|
||||
const out = zod(Schema.Union([Schema.String, Schema.Tuple([Schema.String, Schema.Number])]))
|
||||
expect(out.parse("hello")).toBe("hello")
|
||||
expect(out.parse(["foo", 42])).toEqual(["foo", 42])
|
||||
expect(out.safeParse(["foo"]).success).toBe(false)
|
||||
})
|
||||
|
||||
test("plain arrays still work (no element positions)", () => {
|
||||
const out = zod(Schema.Array(Schema.String))
|
||||
expect(out.parse(["a", "b", "c"])).toEqual(["a", "b", "c"])
|
||||
expect(out.parse([])).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
test("string literal unions produce z.enum with enum in JSON Schema", () => {
|
||||
|
||||
Reference in New Issue
Block a user