Skip to content

Commit 01f431f

Browse files
kitlangtonleohenon
authored andcommitted
Generate config schema from Effect Schema (anomalyco#26939)
1 parent 2e1f096 commit 01f431f

1 file changed

Lines changed: 19 additions & 59 deletions

File tree

packages/opencode/script/schema.ts

Lines changed: 19 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,11 @@
11
#!/usr/bin/env bun
22

3-
import { z } from "zod"
43
import { Config } from "@/config/config"
5-
import { zodObject } from "@opencode-ai/core/effect-zod"
6-
import { TuiJsonSchema } from "../src/cli/cmd/tui/config/tui-json-schema"
74
import { Schema } from "effect"
5+
import { TuiJsonSchema } from "../src/cli/cmd/tui/config/tui-json-schema"
86

97
type JsonSchema = Record<string, unknown>
10-
11-
function generate(schema: z.ZodType) {
12-
const result = z.toJSONSchema(schema, {
13-
io: "input", // Generate input shape (treats optional().default() as not required)
14-
/**
15-
* We'll use the `default` values of the field as the only value in `examples`.
16-
* This will ensure no docs are needed to be read, as the configuration is
17-
* self-documenting.
18-
*
19-
* See https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.9.5
20-
*/
21-
override(ctx) {
22-
const schema = ctx.jsonSchema
23-
24-
// Preserve strictness: set additionalProperties: false for objects
25-
if (
26-
schema &&
27-
typeof schema === "object" &&
28-
schema.type === "object" &&
29-
schema.additionalProperties === undefined
30-
) {
31-
schema.additionalProperties = false
32-
}
33-
34-
// Add examples and default descriptions for string fields with defaults
35-
if (schema && typeof schema === "object" && "type" in schema && schema.type === "string" && schema?.default) {
36-
if (!schema.examples) {
37-
schema.examples = [schema.default]
38-
}
39-
40-
schema.description = [schema.description || "", `default: \`${formatDefault(schema.default)}\``]
41-
.filter(Boolean)
42-
.join("\n\n")
43-
.trim()
44-
}
45-
},
46-
}) as Record<string, unknown> & {
47-
allowComments?: boolean
48-
allowTrailingCommas?: boolean
49-
}
50-
51-
// used for json lsps since config supports jsonc
52-
result.allowComments = true
53-
result.allowTrailingCommas = true
54-
55-
return result
56-
}
57-
58-
function formatDefault(value: unknown) {
59-
if (typeof value !== "object" || value === null) return String(value)
60-
return JSON.stringify(value)
61-
}
8+
const MODEL_REF = "https://models.dev/model-schema.json#/$defs/Model"
629

6310
function generateEffect(schema: Schema.Top) {
6411
const document = Schema.toJsonSchemaDocument(schema)
@@ -68,9 +15,11 @@ function generateEffect(schema: Schema.Top) {
6815
$defs: document.definitions,
6916
})
7017
if (!isRecord(normalized)) throw new Error("schema generator produced a non-object schema")
71-
normalized.allowComments = true
72-
normalized.allowTrailingCommas = true
73-
return normalized
18+
const restored = restoreModelRefs(normalized)
19+
if (!isRecord(restored)) throw new Error("schema generator produced a non-object schema")
20+
restored.allowComments = true
21+
restored.allowTrailingCommas = true
22+
return restored
7423
}
7524

7625
function normalize(value: unknown): unknown {
@@ -100,6 +49,17 @@ function normalize(value: unknown): unknown {
10049
return schema
10150
}
10251

52+
function restoreModelRefs(value: unknown, key?: string): unknown {
53+
if (Array.isArray(value)) return value.map((item) => restoreModelRefs(item))
54+
if (!isRecord(value)) return value
55+
56+
const schema = Object.fromEntries(Object.entries(value).map(([name, item]) => [name, restoreModelRefs(item, name)]))
57+
if ((key === "model" || key === "small_model") && schema.type === "string") {
58+
return { ...schema, $ref: MODEL_REF }
59+
}
60+
return schema
61+
}
62+
10363
function isRecord(value: unknown): value is JsonSchema {
10464
return typeof value === "object" && value !== null && !Array.isArray(value)
10565
}
@@ -108,7 +68,7 @@ const configFile = process.argv[2]
10868
const tuiFile = process.argv[3]
10969

11070
console.log(configFile)
111-
await Bun.write(configFile, JSON.stringify(generate(zodObject(Config.Info).strict().meta({ ref: "Config" })), null, 2))
71+
await Bun.write(configFile, JSON.stringify(generateEffect(Config.Info), null, 2))
11272

11373
if (tuiFile) {
11474
console.log(tuiFile)

0 commit comments

Comments
 (0)