Spaces:
Paused
Paused
| import { promises as fs } from "node:fs"; | |
| import path from "node:path"; | |
| import { fileURLToPath } from "node:url"; | |
| import { ErrorCodes, PROTOCOL_VERSION, ProtocolSchemas } from "../src/gateway/protocol/schema.js"; | |
| type JsonSchema = { | |
| type?: string | string[]; | |
| properties?: Record<string, JsonSchema>; | |
| required?: string[]; | |
| items?: JsonSchema; | |
| enum?: string[]; | |
| patternProperties?: Record<string, JsonSchema>; | |
| }; | |
| const __dirname = path.dirname(fileURLToPath(import.meta.url)); | |
| const repoRoot = path.resolve(__dirname, ".."); | |
| const outPaths = [ | |
| path.join(repoRoot, "apps", "macos", "Sources", "OpenClawProtocol", "GatewayModels.swift"), | |
| path.join( | |
| repoRoot, | |
| "apps", | |
| "shared", | |
| "OpenClawKit", | |
| "Sources", | |
| "OpenClawProtocol", | |
| "GatewayModels.swift", | |
| ), | |
| ]; | |
| const header = `// Generated by scripts/protocol-gen-swift.ts — do not edit by hand\nimport Foundation\n\npublic let GATEWAY_PROTOCOL_VERSION = ${PROTOCOL_VERSION}\n\npublic enum ErrorCode: String, Codable, Sendable {\n${Object.values( | |
| ErrorCodes, | |
| ) | |
| .map((c) => ` case ${camelCase(c)} = "${c}"`) | |
| .join("\n")}\n}\n`; | |
| const reserved = new Set([ | |
| "associatedtype", | |
| "class", | |
| "deinit", | |
| "enum", | |
| "extension", | |
| "fileprivate", | |
| "func", | |
| "import", | |
| "init", | |
| "inout", | |
| "internal", | |
| "let", | |
| "open", | |
| "operator", | |
| "private", | |
| "precedencegroup", | |
| "protocol", | |
| "public", | |
| "rethrows", | |
| "static", | |
| "struct", | |
| "subscript", | |
| "typealias", | |
| "var", | |
| ]); | |
| function camelCase(input: string) { | |
| return input | |
| .replace(/[^a-zA-Z0-9]+/g, " ") | |
| .trim() | |
| .toLowerCase() | |
| .split(/\s+/) | |
| .map((p, i) => (i === 0 ? p : p[0].toUpperCase() + p.slice(1))) | |
| .join(""); | |
| } | |
| function safeName(name: string) { | |
| const cc = camelCase(name.replace(/-/g, "_")); | |
| if (reserved.has(cc)) { | |
| return `_${cc}`; | |
| } | |
| return cc; | |
| } | |
| // filled later once schemas are loaded | |
| const schemaNameByObject = new Map<object, string>(); | |
| function swiftType(schema: JsonSchema, required: boolean): string { | |
| const t = schema.type; | |
| const isOptional = !required; | |
| let base: string; | |
| const named = schemaNameByObject.get(schema as object); | |
| if (named) { | |
| base = named; | |
| } else if (t === "string") { | |
| base = "String"; | |
| } else if (t === "integer") { | |
| base = "Int"; | |
| } else if (t === "number") { | |
| base = "Double"; | |
| } else if (t === "boolean") { | |
| base = "Bool"; | |
| } else if (t === "array") { | |
| base = `[${swiftType(schema.items ?? { type: "Any" }, true)}]`; | |
| } else if (schema.enum) { | |
| base = "String"; | |
| } else if (schema.patternProperties) { | |
| base = "[String: AnyCodable]"; | |
| } else if (t === "object") { | |
| base = "[String: AnyCodable]"; | |
| } else { | |
| base = "AnyCodable"; | |
| } | |
| return isOptional ? `${base}?` : base; | |
| } | |
| function emitStruct(name: string, schema: JsonSchema): string { | |
| const props = schema.properties ?? {}; | |
| const required = new Set(schema.required ?? []); | |
| const lines: string[] = []; | |
| lines.push(`public struct ${name}: Codable, Sendable {`); | |
| if (Object.keys(props).length === 0) { | |
| lines.push("}\n"); | |
| return lines.join("\n"); | |
| } | |
| const codingKeys: string[] = []; | |
| for (const [key, propSchema] of Object.entries(props)) { | |
| const propName = safeName(key); | |
| const propType = swiftType(propSchema, required.has(key)); | |
| lines.push(` public let ${propName}: ${propType}`); | |
| if (propName !== key) { | |
| codingKeys.push(` case ${propName} = "${key}"`); | |
| } else { | |
| codingKeys.push(` case ${propName}`); | |
| } | |
| } | |
| lines.push( | |
| "\n public init(\n" + | |
| Object.entries(props) | |
| .map(([key, prop]) => { | |
| const propName = safeName(key); | |
| const req = required.has(key); | |
| return ` ${propName}: ${swiftType(prop, true)}${req ? "" : "?"}`; | |
| }) | |
| .join(",\n") + | |
| "\n ) {\n" + | |
| Object.entries(props) | |
| .map(([key]) => { | |
| const propName = safeName(key); | |
| return ` self.${propName} = ${propName}`; | |
| }) | |
| .join("\n") + | |
| "\n }\n" + | |
| " private enum CodingKeys: String, CodingKey {\n" + | |
| codingKeys.join("\n") + | |
| "\n }\n}", | |
| ); | |
| lines.push(""); | |
| return lines.join("\n"); | |
| } | |
| function emitGatewayFrame(): string { | |
| const cases = ["req", "res", "event"]; | |
| const associated: Record<string, string> = { | |
| req: "RequestFrame", | |
| res: "ResponseFrame", | |
| event: "EventFrame", | |
| }; | |
| const caseLines = cases.map((c) => ` case ${safeName(c)}(${associated[c]})`); | |
| const initLines = ` | |
| private enum CodingKeys: String, CodingKey { | |
| case type | |
| } | |
| public init(from decoder: Decoder) throws { | |
| let typeContainer = try decoder.container(keyedBy: CodingKeys.self) | |
| let type = try typeContainer.decode(String.self, forKey: .type) | |
| switch type { | |
| case "req": | |
| self = .req(try RequestFrame(from: decoder)) | |
| case "res": | |
| self = .res(try ResponseFrame(from: decoder)) | |
| case "event": | |
| self = .event(try EventFrame(from: decoder)) | |
| default: | |
| let container = try decoder.singleValueContainer() | |
| let raw = try container.decode([String: AnyCodable].self) | |
| self = .unknown(type: type, raw: raw) | |
| } | |
| } | |
| public func encode(to encoder: Encoder) throws { | |
| switch self { | |
| case .req(let v): try v.encode(to: encoder) | |
| case .res(let v): try v.encode(to: encoder) | |
| case .event(let v): try v.encode(to: encoder) | |
| case .unknown(_, let raw): | |
| var container = encoder.singleValueContainer() | |
| try container.encode(raw) | |
| } | |
| } | |
| `; | |
| return [ | |
| "public enum GatewayFrame: Codable, Sendable {", | |
| ...caseLines, | |
| " case unknown(type: String, raw: [String: AnyCodable])", | |
| initLines, | |
| "}", | |
| "", | |
| ].join("\n"); | |
| } | |
| async function generate() { | |
| const definitions = Object.entries(ProtocolSchemas) as Array<[string, JsonSchema]>; | |
| for (const [name, schema] of definitions) { | |
| schemaNameByObject.set(schema as object, name); | |
| } | |
| const parts: string[] = []; | |
| parts.push(header); | |
| // Value structs | |
| for (const [name, schema] of definitions) { | |
| if (name === "GatewayFrame") { | |
| continue; | |
| } | |
| if (schema.type === "object") { | |
| parts.push(emitStruct(name, schema)); | |
| } | |
| } | |
| // Frame enum must come after payload structs | |
| parts.push(emitGatewayFrame()); | |
| const content = parts.join("\n"); | |
| for (const outPath of outPaths) { | |
| await fs.mkdir(path.dirname(outPath), { recursive: true }); | |
| await fs.writeFile(outPath, content); | |
| console.log(`wrote ${outPath}`); | |
| } | |
| } | |
| generate().catch((err) => { | |
| console.error(err); | |
| process.exit(1); | |
| }); | |