| 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; |
| } |
|
|
| |
| 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); |
|
|
| |
| for (const [name, schema] of definitions) { |
| if (name === "GatewayFrame") continue; |
| if (schema.type === "object") { |
| parts.push(emitStruct(name, schema)); |
| } |
| } |
|
|
| |
| 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); |
| }); |
|
|