Skip to content

Commit 1007630

Browse files
authored
Migrate runtime validators to Effect Schema (#26975)
1 parent 9e8274d commit 1007630

4 files changed

Lines changed: 176 additions & 161 deletions

File tree

packages/opencode/src/cli/cmd/tui/context/editor-zed.ts

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,36 @@
11
import { Database } from "bun:sqlite"
22
import os from "node:os"
33
import path from "node:path"
4-
import z from "zod"
4+
import { Option, Schema } from "effect"
55
import { Filesystem } from "@/util/filesystem"
66
import type { EditorSelection } from "./editor"
77

8-
const ZedEditorRowSchema = z.object({
9-
item_kind: z.string(),
10-
editor_id: z.number().nullable(),
11-
workspace_id: z.number(),
12-
workspace_paths: z.string().nullable(),
13-
timestamp: z.string(),
14-
buffer_path: z.string().nullable(),
8+
const ZedEditorRowSchema = Schema.Struct({
9+
item_kind: Schema.String,
10+
editor_id: Schema.NullOr(Schema.Number),
11+
workspace_id: Schema.Number,
12+
workspace_paths: Schema.NullOr(Schema.String),
13+
timestamp: Schema.String,
14+
buffer_path: Schema.NullOr(Schema.String),
1515
})
1616

17-
const ZedSelectionRowSchema = z.object({
18-
selection_start: z.number().nullable(),
19-
selection_end: z.number().nullable(),
17+
const ZedSelectionRowSchema = Schema.Struct({
18+
selection_start: Schema.NullOr(Schema.Number),
19+
selection_end: Schema.NullOr(Schema.Number),
2020
})
2121

22-
const ZedEditorContentsSchema = z.object({
23-
contents: z.string().nullable(),
22+
const ZedEditorContentsSchema = Schema.Struct({
23+
contents: Schema.NullOr(Schema.String),
2424
})
2525

26+
const decodeZedEditorRow = Schema.decodeUnknownOption(ZedEditorRowSchema)
27+
const decodeZedSelectionRow = Schema.decodeUnknownOption(ZedSelectionRowSchema)
28+
const decodeZedEditorContents = Schema.decodeUnknownOption(ZedEditorContentsSchema)
29+
2630
const utf8 = new TextEncoder()
2731

28-
type ZedEditorRow = z.infer<typeof ZedEditorRowSchema>
32+
type ZedEditorRow = Schema.Schema.Type<typeof ZedEditorRowSchema>
2933
type ZedActiveEditorRow = ZedEditorRow & { item_kind: "Editor"; editor_id: number }
30-
type ZedSelectionRow = z.infer<typeof ZedSelectionRowSchema>
3134

3235
export type ZedSelectionResult =
3336
| { type: "selection"; selection: EditorSelection }
@@ -107,8 +110,8 @@ function queryZedActiveEditor(dbPath: string, cwd: string) {
107110
.all()
108111

109112
const rows = raw.flatMap((row) => {
110-
const parsed = ZedEditorRowSchema.safeParse(row)
111-
return parsed.success ? [parsed.data] : []
113+
const parsed = decodeZedEditorRow(row)
114+
return Option.isSome(parsed) ? [parsed.value] : []
112115
})
113116

114117
if (raw.length > 0 && rows.length === 0) return { type: "unavailable" as const }
@@ -143,8 +146,8 @@ function queryZedEditorSelections(dbPath: string, row: ZedActiveEditorRow) {
143146
.all({ $editorID: row.editor_id, $workspaceID: row.workspace_id })
144147

145148
const selections = raw.flatMap((selection) => {
146-
const parsed = ZedSelectionRowSchema.safeParse(selection)
147-
return parsed.success ? [parsed.data] : []
149+
const parsed = decodeZedSelectionRow(selection)
150+
return Option.isSome(parsed) ? [parsed.value] : []
148151
})
149152

150153
if (raw.length > 0 && selections.length === 0) return { type: "unavailable" as const }
@@ -160,7 +163,7 @@ function queryZedEditorContents(dbPath: string, row: ZedActiveEditorRow) {
160163
let db: Database | undefined
161164
try {
162165
db = new Database(dbPath, { readonly: true })
163-
const parsed = ZedEditorContentsSchema.safeParse(
166+
const parsed = decodeZedEditorContents(
164167
db
165168
.query(
166169
`select contents
@@ -169,8 +172,8 @@ function queryZedEditorContents(dbPath: string, row: ZedActiveEditorRow) {
169172
)
170173
.get({ $editorID: row.editor_id, $workspaceID: row.workspace_id }),
171174
)
172-
if (!parsed.success) return { type: "unavailable" as const }
173-
return { type: "contents" as const, contents: parsed.data.contents }
175+
if (Option.isNone(parsed)) return { type: "unavailable" as const }
176+
return { type: "contents" as const, contents: parsed.value.contents }
174177
} catch {
175178
return { type: "unavailable" as const }
176179
} finally {

packages/opencode/src/cli/cmd/tui/context/editor.ts

Lines changed: 85 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -3,92 +3,102 @@ import os from "node:os"
33
import path from "node:path"
44
import { onCleanup, onMount } from "solid-js"
55
import { createStore } from "solid-js/store"
6-
import z from "zod"
6+
import { Option, Schema, SchemaGetter } from "effect"
77
import { isRecord } from "@/util/record"
88
import { createSimpleContext } from "./helper"
99
import { resolveZedDbPath, resolveZedSelection } from "./editor-zed"
1010

1111
const MCP_PROTOCOL_VERSION = "2025-11-25"
1212

13-
const JsonRpcMessageSchema = z.object({
14-
id: z.union([z.number(), z.string(), z.null()]).optional(),
15-
method: z.string().optional(),
16-
params: z.unknown().optional(),
17-
result: z.unknown().optional(),
18-
error: z
19-
.object({
20-
code: z.number().optional(),
21-
message: z.string().optional(),
22-
})
23-
.optional(),
13+
const JsonRpcMessageSchema = Schema.Struct({
14+
id: Schema.optional(Schema.Union([Schema.Number, Schema.String, Schema.Null])),
15+
method: Schema.optional(Schema.String),
16+
params: Schema.optional(Schema.Unknown),
17+
result: Schema.optional(Schema.Unknown),
18+
error: Schema.optional(
19+
Schema.Struct({
20+
code: Schema.optional(Schema.Number),
21+
message: Schema.optional(Schema.String),
22+
}),
23+
),
2424
})
2525

26-
const PositionSchema = z.object({
27-
line: z.number(),
28-
character: z.number(),
26+
const PositionSchema = Schema.Struct({
27+
line: Schema.Number,
28+
character: Schema.Number,
2929
})
3030

31-
const EditorSelectionRangeSchema = z.object({
32-
text: z.string(),
33-
selection: z.object({
31+
const EditorSelectionRangeSchema = Schema.Struct({
32+
text: Schema.String,
33+
selection: Schema.Struct({
3434
start: PositionSchema,
3535
end: PositionSchema,
3636
}),
3737
})
3838

39-
const EditorSelectionSchema = z
40-
.union([
41-
z.object({
42-
filePath: z.string(),
43-
source: z.enum(["websocket", "zed"]).optional(),
44-
ranges: z.array(EditorSelectionRangeSchema).min(1),
45-
}),
46-
z.object({
47-
text: z.string(),
48-
filePath: z.string(),
49-
source: z.enum(["websocket", "zed"]).optional(),
50-
selection: z.object({
51-
start: PositionSchema,
52-
end: PositionSchema,
53-
}),
39+
const EditorSelectionRangesSchema = Schema.Struct({
40+
filePath: Schema.String,
41+
source: Schema.optional(Schema.Literals(["websocket", "zed"])),
42+
ranges: Schema.mutable(Schema.Array(EditorSelectionRangeSchema).check(Schema.isMinLength(1))),
43+
})
44+
45+
const EditorSelectionSchema = Schema.Union([
46+
EditorSelectionRangesSchema,
47+
Schema.Struct({
48+
text: Schema.String,
49+
filePath: Schema.String,
50+
source: Schema.optional(Schema.Literals(["websocket", "zed"])),
51+
selection: Schema.Struct({
52+
start: PositionSchema,
53+
end: PositionSchema,
5454
}),
55-
])
56-
.transform((value) =>
57-
"ranges" in value
58-
? value
59-
: {
60-
filePath: value.filePath,
61-
source: value.source,
62-
ranges: [
63-
{
64-
text: value.text,
65-
selection: value.selection,
66-
},
67-
],
68-
},
69-
)
70-
71-
const EditorMentionSchema = z.object({
72-
filePath: z.string(),
73-
lineStart: z.number(),
74-
lineEnd: z.number(),
55+
}),
56+
]).pipe(
57+
Schema.decodeTo(EditorSelectionRangesSchema, {
58+
decode: SchemaGetter.transform((value) =>
59+
"ranges" in value
60+
? value
61+
: {
62+
filePath: value.filePath,
63+
source: value.source,
64+
ranges: [
65+
{
66+
text: value.text,
67+
selection: value.selection,
68+
},
69+
],
70+
},
71+
),
72+
encode: SchemaGetter.passthrough({ strict: false }),
73+
}),
74+
)
75+
76+
const EditorMentionSchema = Schema.Struct({
77+
filePath: Schema.String,
78+
lineStart: Schema.Number,
79+
lineEnd: Schema.Number,
7580
})
7681

77-
const EditorServerInfoSchema = z.object({
78-
protocolVersion: z.string().optional(),
79-
serverInfo: z
80-
.object({
81-
name: z.string().optional(),
82-
version: z.string().optional(),
83-
})
84-
.optional(),
82+
const EditorServerInfoSchema = Schema.Struct({
83+
protocolVersion: Schema.optional(Schema.String),
84+
serverInfo: Schema.optional(
85+
Schema.Struct({
86+
name: Schema.optional(Schema.String),
87+
version: Schema.optional(Schema.String),
88+
}),
89+
),
8590
})
8691

87-
type JsonRpcMessage = z.infer<typeof JsonRpcMessageSchema>
88-
export type EditorSelection = z.infer<typeof EditorSelectionSchema>
89-
export type EditorMention = z.infer<typeof EditorMentionSchema>
92+
const decodeJsonRpcMessage = Schema.decodeUnknownOption(JsonRpcMessageSchema)
93+
const decodeEditorSelection = Schema.decodeUnknownOption(EditorSelectionSchema)
94+
const decodeEditorMention = Schema.decodeUnknownOption(EditorMentionSchema)
95+
const decodeEditorServerInfo = Schema.decodeUnknownOption(EditorServerInfoSchema)
96+
97+
type JsonRpcMessage = Schema.Schema.Type<typeof JsonRpcMessageSchema>
98+
export type EditorSelection = Schema.Schema.Type<typeof EditorSelectionSchema>
99+
export type EditorMention = Schema.Schema.Type<typeof EditorMentionSchema>
90100
export type EditorLabelState = "pending" | "sent" | "none"
91-
type EditorServerInfo = z.infer<typeof EditorServerInfoSchema>
101+
type EditorServerInfo = Schema.Schema.Type<typeof EditorServerInfoSchema>
92102

93103
type EditorConnection = {
94104
url: string
@@ -214,16 +224,15 @@ export const { use: useEditorContext, provider: EditorContextProvider } = create
214224
const message = parseMessage(event.data)
215225
if (!message) return
216226

217-
const selection =
218-
message.method === "selection_changed" ? EditorSelectionSchema.safeParse(message.params) : undefined
219-
if (selection?.success) {
220-
setSelection({ ...selection.data, source: "websocket" })
227+
const selection = message.method === "selection_changed" ? decodeEditorSelection(message.params) : Option.none()
228+
if (Option.isSome(selection)) {
229+
setSelection({ ...selection.value, source: "websocket" })
221230
return
222231
}
223232

224-
const mention = message.method === "at_mentioned" ? EditorMentionSchema.safeParse(message.params) : undefined
225-
if (mention?.success) {
226-
mentionListeners.forEach((listener) => listener(mention.data))
233+
const mention = message.method === "at_mentioned" ? decodeEditorMention(message.params) : Option.none()
234+
if (Option.isSome(mention)) {
235+
mentionListeners.forEach((listener) => listener(mention.value))
227236
return
228237
}
229238

@@ -235,9 +244,9 @@ export const { use: useEditorContext, provider: EditorContextProvider } = create
235244
pending.delete(message.id)
236245
if (message.error) return
237246

238-
const initialize = method === "initialize" ? EditorServerInfoSchema.safeParse(message.result) : undefined
239-
if (initialize?.success) {
240-
setStore("server", initialize.data)
247+
const initialize = method === "initialize" ? decodeEditorServerInfo(message.result) : Option.none()
248+
if (Option.isSome(initialize)) {
249+
setStore("server", initialize.value)
241250
send({ method: "notifications/initialized" })
242251
return
243252
}
@@ -447,7 +456,7 @@ function parseMessage(value: unknown) {
447456
if (typeof value !== "string") return
448457

449458
try {
450-
return JsonRpcMessageSchema.parse(JSON.parse(value))
459+
return Option.getOrUndefined(decodeJsonRpcMessage(JSON.parse(value)))
451460
} catch {
452461
return
453462
}

packages/opencode/src/mcp/auth.ts

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,35 @@
11
import path from "path"
2-
import z from "zod"
32
import { Global } from "@opencode-ai/core/global"
4-
import { Effect, Layer, Context } from "effect"
3+
import { Effect, Layer, Context, Option, Schema } from "effect"
54
import { AppFileSystem } from "@opencode-ai/core/filesystem"
65

7-
export const Tokens = z.object({
8-
accessToken: z.string(),
9-
refreshToken: z.string().optional(),
10-
expiresAt: z.number().optional(),
11-
scope: z.string().optional(),
6+
export const Tokens = Schema.Struct({
7+
accessToken: Schema.mutableKey(Schema.String),
8+
refreshToken: Schema.mutableKey(Schema.optional(Schema.String)),
9+
expiresAt: Schema.mutableKey(Schema.optional(Schema.Number)),
10+
scope: Schema.mutableKey(Schema.optional(Schema.String)),
1211
})
13-
export type Tokens = z.infer<typeof Tokens>
12+
export type Tokens = Schema.Schema.Type<typeof Tokens>
1413

15-
export const ClientInfo = z.object({
16-
clientId: z.string(),
17-
clientSecret: z.string().optional(),
18-
clientIdIssuedAt: z.number().optional(),
19-
clientSecretExpiresAt: z.number().optional(),
14+
export const ClientInfo = Schema.Struct({
15+
clientId: Schema.mutableKey(Schema.String),
16+
clientSecret: Schema.mutableKey(Schema.optional(Schema.String)),
17+
clientIdIssuedAt: Schema.mutableKey(Schema.optional(Schema.Number)),
18+
clientSecretExpiresAt: Schema.mutableKey(Schema.optional(Schema.Number)),
2019
})
21-
export type ClientInfo = z.infer<typeof ClientInfo>
22-
23-
export const Entry = z.object({
24-
tokens: Tokens.optional(),
25-
clientInfo: ClientInfo.optional(),
26-
codeVerifier: z.string().optional(),
27-
oauthState: z.string().optional(),
28-
serverUrl: z.string().optional(),
20+
export type ClientInfo = Schema.Schema.Type<typeof ClientInfo>
21+
22+
export const Entry = Schema.Struct({
23+
tokens: Schema.mutableKey(Schema.optional(Tokens)),
24+
clientInfo: Schema.mutableKey(Schema.optional(ClientInfo)),
25+
codeVerifier: Schema.mutableKey(Schema.optional(Schema.String)),
26+
oauthState: Schema.mutableKey(Schema.optional(Schema.String)),
27+
serverUrl: Schema.mutableKey(Schema.optional(Schema.String)),
2928
})
30-
export type Entry = z.infer<typeof Entry>
29+
export type Entry = Schema.Schema.Type<typeof Entry>
30+
31+
const decodeAuthData = Schema.decodeUnknownOption(Schema.Record(Schema.String, Entry))
32+
type AuthData = Record<string, Entry>
3133

3234
const filepath = path.join(Global.Path.data, "mcp-auth.json")
3335

@@ -56,8 +58,8 @@ export const layer = Layer.effect(
5658

5759
const all = Effect.fn("McpAuth.all")(function* () {
5860
return yield* fs.readJson(filepath).pipe(
59-
Effect.map((data) => data as Record<string, Entry>),
60-
Effect.catch(() => Effect.succeed({} as Record<string, Entry>)),
61+
Effect.map((data): AuthData => Option.getOrElse(decodeAuthData(data), () => ({}) as AuthData) as AuthData),
62+
Effect.catch(() => Effect.succeed({} as AuthData)),
6163
)
6264
})
6365

@@ -93,7 +95,7 @@ export const layer = Layer.effect(
9395
yield* set(mcpName, entry, serverUrl)
9496
})
9597

96-
const clearField = <K extends keyof Entry>(field: K, spanName: string) =>
98+
const clearField = (field: keyof Entry, spanName: string) =>
9799
Effect.fn(`McpAuth.${spanName}`)(function* (mcpName: string) {
98100
const entry = yield* get(mcpName)
99101
if (entry) {

0 commit comments

Comments
 (0)