Migrate runtime validators to Effect Schema (#26975)
This commit is contained in:
@@ -3,92 +3,102 @@ import os from "node:os"
|
||||
import path from "node:path"
|
||||
import { onCleanup, onMount } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import z from "zod"
|
||||
import { Option, Schema, SchemaGetter } from "effect"
|
||||
import { isRecord } from "@/util/record"
|
||||
import { createSimpleContext } from "./helper"
|
||||
import { resolveZedDbPath, resolveZedSelection } from "./editor-zed"
|
||||
|
||||
const MCP_PROTOCOL_VERSION = "2025-11-25"
|
||||
|
||||
const JsonRpcMessageSchema = z.object({
|
||||
id: z.union([z.number(), z.string(), z.null()]).optional(),
|
||||
method: z.string().optional(),
|
||||
params: z.unknown().optional(),
|
||||
result: z.unknown().optional(),
|
||||
error: z
|
||||
.object({
|
||||
code: z.number().optional(),
|
||||
message: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
const JsonRpcMessageSchema = Schema.Struct({
|
||||
id: Schema.optional(Schema.Union([Schema.Number, Schema.String, Schema.Null])),
|
||||
method: Schema.optional(Schema.String),
|
||||
params: Schema.optional(Schema.Unknown),
|
||||
result: Schema.optional(Schema.Unknown),
|
||||
error: Schema.optional(
|
||||
Schema.Struct({
|
||||
code: Schema.optional(Schema.Number),
|
||||
message: Schema.optional(Schema.String),
|
||||
}),
|
||||
),
|
||||
})
|
||||
|
||||
const PositionSchema = z.object({
|
||||
line: z.number(),
|
||||
character: z.number(),
|
||||
const PositionSchema = Schema.Struct({
|
||||
line: Schema.Number,
|
||||
character: Schema.Number,
|
||||
})
|
||||
|
||||
const EditorSelectionRangeSchema = z.object({
|
||||
text: z.string(),
|
||||
selection: z.object({
|
||||
const EditorSelectionRangeSchema = Schema.Struct({
|
||||
text: Schema.String,
|
||||
selection: Schema.Struct({
|
||||
start: PositionSchema,
|
||||
end: PositionSchema,
|
||||
}),
|
||||
})
|
||||
|
||||
const EditorSelectionSchema = z
|
||||
.union([
|
||||
z.object({
|
||||
filePath: z.string(),
|
||||
source: z.enum(["websocket", "zed"]).optional(),
|
||||
ranges: z.array(EditorSelectionRangeSchema).min(1),
|
||||
}),
|
||||
z.object({
|
||||
text: z.string(),
|
||||
filePath: z.string(),
|
||||
source: z.enum(["websocket", "zed"]).optional(),
|
||||
selection: z.object({
|
||||
start: PositionSchema,
|
||||
end: PositionSchema,
|
||||
}),
|
||||
}),
|
||||
])
|
||||
.transform((value) =>
|
||||
"ranges" in value
|
||||
? value
|
||||
: {
|
||||
filePath: value.filePath,
|
||||
source: value.source,
|
||||
ranges: [
|
||||
{
|
||||
text: value.text,
|
||||
selection: value.selection,
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
const EditorMentionSchema = z.object({
|
||||
filePath: z.string(),
|
||||
lineStart: z.number(),
|
||||
lineEnd: z.number(),
|
||||
const EditorSelectionRangesSchema = Schema.Struct({
|
||||
filePath: Schema.String,
|
||||
source: Schema.optional(Schema.Literals(["websocket", "zed"])),
|
||||
ranges: Schema.mutable(Schema.Array(EditorSelectionRangeSchema).check(Schema.isMinLength(1))),
|
||||
})
|
||||
|
||||
const EditorServerInfoSchema = z.object({
|
||||
protocolVersion: z.string().optional(),
|
||||
serverInfo: z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
version: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
const EditorSelectionSchema = Schema.Union([
|
||||
EditorSelectionRangesSchema,
|
||||
Schema.Struct({
|
||||
text: Schema.String,
|
||||
filePath: Schema.String,
|
||||
source: Schema.optional(Schema.Literals(["websocket", "zed"])),
|
||||
selection: Schema.Struct({
|
||||
start: PositionSchema,
|
||||
end: PositionSchema,
|
||||
}),
|
||||
}),
|
||||
]).pipe(
|
||||
Schema.decodeTo(EditorSelectionRangesSchema, {
|
||||
decode: SchemaGetter.transform((value) =>
|
||||
"ranges" in value
|
||||
? value
|
||||
: {
|
||||
filePath: value.filePath,
|
||||
source: value.source,
|
||||
ranges: [
|
||||
{
|
||||
text: value.text,
|
||||
selection: value.selection,
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
encode: SchemaGetter.passthrough({ strict: false }),
|
||||
}),
|
||||
)
|
||||
|
||||
const EditorMentionSchema = Schema.Struct({
|
||||
filePath: Schema.String,
|
||||
lineStart: Schema.Number,
|
||||
lineEnd: Schema.Number,
|
||||
})
|
||||
|
||||
type JsonRpcMessage = z.infer<typeof JsonRpcMessageSchema>
|
||||
export type EditorSelection = z.infer<typeof EditorSelectionSchema>
|
||||
export type EditorMention = z.infer<typeof EditorMentionSchema>
|
||||
const EditorServerInfoSchema = Schema.Struct({
|
||||
protocolVersion: Schema.optional(Schema.String),
|
||||
serverInfo: Schema.optional(
|
||||
Schema.Struct({
|
||||
name: Schema.optional(Schema.String),
|
||||
version: Schema.optional(Schema.String),
|
||||
}),
|
||||
),
|
||||
})
|
||||
|
||||
const decodeJsonRpcMessage = Schema.decodeUnknownOption(JsonRpcMessageSchema)
|
||||
const decodeEditorSelection = Schema.decodeUnknownOption(EditorSelectionSchema)
|
||||
const decodeEditorMention = Schema.decodeUnknownOption(EditorMentionSchema)
|
||||
const decodeEditorServerInfo = Schema.decodeUnknownOption(EditorServerInfoSchema)
|
||||
|
||||
type JsonRpcMessage = Schema.Schema.Type<typeof JsonRpcMessageSchema>
|
||||
export type EditorSelection = Schema.Schema.Type<typeof EditorSelectionSchema>
|
||||
export type EditorMention = Schema.Schema.Type<typeof EditorMentionSchema>
|
||||
export type EditorLabelState = "pending" | "sent" | "none"
|
||||
type EditorServerInfo = z.infer<typeof EditorServerInfoSchema>
|
||||
type EditorServerInfo = Schema.Schema.Type<typeof EditorServerInfoSchema>
|
||||
|
||||
type EditorConnection = {
|
||||
url: string
|
||||
@@ -214,16 +224,15 @@ export const { use: useEditorContext, provider: EditorContextProvider } = create
|
||||
const message = parseMessage(event.data)
|
||||
if (!message) return
|
||||
|
||||
const selection =
|
||||
message.method === "selection_changed" ? EditorSelectionSchema.safeParse(message.params) : undefined
|
||||
if (selection?.success) {
|
||||
setSelection({ ...selection.data, source: "websocket" })
|
||||
const selection = message.method === "selection_changed" ? decodeEditorSelection(message.params) : Option.none()
|
||||
if (Option.isSome(selection)) {
|
||||
setSelection({ ...selection.value, source: "websocket" })
|
||||
return
|
||||
}
|
||||
|
||||
const mention = message.method === "at_mentioned" ? EditorMentionSchema.safeParse(message.params) : undefined
|
||||
if (mention?.success) {
|
||||
mentionListeners.forEach((listener) => listener(mention.data))
|
||||
const mention = message.method === "at_mentioned" ? decodeEditorMention(message.params) : Option.none()
|
||||
if (Option.isSome(mention)) {
|
||||
mentionListeners.forEach((listener) => listener(mention.value))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -235,9 +244,9 @@ export const { use: useEditorContext, provider: EditorContextProvider } = create
|
||||
pending.delete(message.id)
|
||||
if (message.error) return
|
||||
|
||||
const initialize = method === "initialize" ? EditorServerInfoSchema.safeParse(message.result) : undefined
|
||||
if (initialize?.success) {
|
||||
setStore("server", initialize.data)
|
||||
const initialize = method === "initialize" ? decodeEditorServerInfo(message.result) : Option.none()
|
||||
if (Option.isSome(initialize)) {
|
||||
setStore("server", initialize.value)
|
||||
send({ method: "notifications/initialized" })
|
||||
return
|
||||
}
|
||||
@@ -447,7 +456,7 @@ function parseMessage(value: unknown) {
|
||||
if (typeof value !== "string") return
|
||||
|
||||
try {
|
||||
return JsonRpcMessageSchema.parse(JSON.parse(value))
|
||||
return Option.getOrUndefined(decodeJsonRpcMessage(JSON.parse(value)))
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user