import { promises as fs } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { ErrorCodes, MIN_CLIENT_PROTOCOL_VERSION, PROTOCOL_VERSION, ProtocolSchemas, } from "../src/gateway/protocol/schema.js"; type JsonSchema = { type?: string | string[]; const?: boolean | number | string | null; properties?: Record; required?: string[]; items?: JsonSchema; enum?: string[]; patternProperties?: Record; anyOf?: JsonSchema[]; oneOf?: JsonSchema[]; additionalProperties?: boolean | JsonSchema; }; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.resolve(__dirname, ".."); const outPaths = [ path.join( repoRoot, "apps", "shared", "OpenClawKit", "Sources", "OpenClawProtocol", "GatewayModels.swift", ), ]; const STRICT_LITERAL_STRUCTS = new Set([ "PluginsSessionActionSuccessResult", "PluginsSessionActionFailureResult", ]); const header = `// Generated by scripts/protocol-gen-swift.ts — do not edit by hand\n// swiftlint:disable file_length\nimport Foundation\n\npublic let GATEWAY_PROTOCOL_VERSION = ${PROTOCOL_VERSION}\npublic let GATEWAY_MIN_PROTOCOL_VERSION = ${MIN_CLIENT_PROTOCOL_VERSION}\n\nprivate struct GatewayAnyCodingKey: CodingKey, Hashable {\n let stringValue: String\n let intValue: Int?\n\n init?(stringValue: String) {\n self.stringValue = stringValue\n self.intValue = nil\n }\n\n init?(intValue: Int) {\n self.stringValue = String(intValue)\n self.intValue = intValue\n }\n}\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 (/^\d/.test(cc)) { return `_${cc}`; } if (reserved.has(cc)) { return `_${cc}`; } return cc; } // filled later once schemas are loaded const schemaNameByObject = new Map(); const schemaNameBySignature = new Map(); const duplicateSchemaSignatures = new Set(); function stableJson(value: unknown): unknown { if (Array.isArray(value)) { return value.map(stableJson); } if (value && typeof value === "object") { const record = value as Record; return Object.fromEntries( Object.keys(record) .toSorted() .map((key) => [key, stableJson(record[key])]), ); } return value; } function schemaSignature(schema: JsonSchema): string { return JSON.stringify(stableJson(schema)); } function registerNamedSchema(name: string, schema: JsonSchema): void { schemaNameByObject.set(schema as object, name); const signature = schemaSignature(schema); if (duplicateSchemaSignatures.has(signature)) { return; } if (schemaNameBySignature.has(signature)) { schemaNameBySignature.delete(signature); duplicateSchemaSignatures.add(signature); return; } schemaNameBySignature.set(signature, name); } function namedSchema(schema: JsonSchema, allowStructuralFallback = false): string | undefined { return ( schemaNameByObject.get(schema as object) ?? (allowStructuralFallback ? schemaNameBySignature.get(schemaSignature(schema)) : undefined) ); } function swiftType(schema: JsonSchema, required: boolean, allowStructuralNamed = false): string { const t = schema.type; const isOptional = !required; let base: string; const named = namedSchema(schema, allowStructuralNamed); 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, 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 emitEnum(name: string, schema: JsonSchema): string { const cases = schema.enum ?? []; return [ `public enum ${name}: String, Codable, Sendable {`, ...cases.map((value) => ` case ${safeName(value)} = "${value}"`), "}", "", ].join("\n"); } function emitStruct(name: string, schema: JsonSchema): string { const props = schema.properties ?? {}; const required = new Set(schema.required ?? []); const literalProps = Object.entries(props) .map(([key, propSchema]) => ({ key, propSchema, literal: literalSchemaValue(propSchema), })) .filter( ( entry, ): entry is { key: string; propSchema: JsonSchema; literal: boolean | number | string | null; } => entry.literal !== undefined, ); const lines: string[] = []; if (Object.keys(props).length === 0) { return `public struct ${name}: Codable, Sendable {}\n`; } if (STRICT_LITERAL_STRUCTS.has(name) && literalProps.length > 0) { const literalPropByKey = new Map(literalProps.map((entry) => [entry.key, entry.literal])); lines.push(`public struct ${name}: Codable, Sendable {`); const codingKeys: string[] = []; for (const [key, propSchema] of Object.entries(props)) { const propName = safeName(key); const propType = swiftType(propSchema, required.has(key), true); lines.push(` public let ${propName}: ${propType}`); if (propName !== key) { codingKeys.push(` case ${propName} = "${key}"`); } else { codingKeys.push(` case ${propName}`); } } const initializerParams = Object.entries(props) .filter(([key]) => !literalPropByKey.has(key)) .map(([key, prop]) => { const propName = safeName(key); const req = required.has(key); return ` ${propName}: ${swiftType(prop, true, true)}${req ? "" : "?"}`; }); lines.push( "\n public init(\n" + (initializerParams.length > 0 ? initializerParams.join(",\n") : " ") + "\n )\n" + " {\n" + Object.entries(props) .map(([key]) => { const propName = safeName(key); if (literalPropByKey.has(key)) { return ` self.${propName} = ${swiftLiteralSource(literalPropByKey.get(key)!)}`; } return ` self.${propName} = ${propName}`; }) .join("\n") + "\n }\n\n" + " private enum CodingKeys: String, CodingKey {\n" + codingKeys.join("\n") + "\n }\n\n" + " public init(from decoder: Decoder) throws {\n" + (schema.additionalProperties === false ? ` let rawContainer = try decoder.container(keyedBy: GatewayAnyCodingKey.self)\n let unexpectedKeys = rawContainer.allKeys\n .map(\\.stringValue)\n .filter { !Set([${Object.keys( props, ) .map((key) => JSON.stringify(key)) .join( ", ", )}]).contains($0) }\n if !unexpectedKeys.isEmpty {\n throw DecodingError.dataCorrupted(\n .init(\n codingPath: rawContainer.codingPath,\n debugDescription: "Unexpected keys for ${name}: \\(unexpectedKeys.sorted().joined(separator: ", "))"\n )\n )\n }\n` : "") + " let container = try decoder.container(keyedBy: CodingKeys.self)\n" + Object.entries(props) .map(([key, propSchema]) => { const propName = safeName(key); const capitalizedPropName = propName.slice(0, 1).toUpperCase() + propName.slice(1); const literal = literalPropByKey.get(key); if (literal !== undefined) { const literalType = swiftType(propSchema, true, true); return ` let decoded${capitalizedPropName} = try container.decode(${literalType}.self, forKey: .${propName})\n guard decoded${capitalizedPropName} == ${swiftLiteralSource(literal)} else {\n throw DecodingError.dataCorruptedError(\n forKey: .${propName},\n in: container,\n debugDescription: "Expected ${key} to equal ${String(literal)}"\n )\n }\n self.${propName} = ${swiftLiteralSource(literal)}`; } if (required.has(key)) { return ` self.${propName} = try container.decode(${swiftType(propSchema, true, true)}.self, forKey: .${propName})`; } return ` self.${propName} = try container.decodeIfPresent(${swiftType(propSchema, true, true)}.self, forKey: .${propName})`; }) .join("\n") + "\n }\n\n" + " public func encode(to encoder: Encoder) throws {\n" + " var container = encoder.container(keyedBy: CodingKeys.self)\n" + Object.entries(props) .map(([key]) => { const propName = safeName(key); const literal = literalPropByKey.get(key); if (literal !== undefined) { return ` try container.encode(${swiftLiteralSource(literal)}, forKey: .${propName})`; } if (required.has(key)) { return ` try container.encode(${propName}, forKey: .${propName})`; } return ` try container.encodeIfPresent(${propName}, forKey: .${propName})`; }) .join("\n") + "\n }\n}", ); lines.push(""); return lines.join("\n"); } lines.push(`public struct ${name}: Codable, Sendable {`); const codingKeys: string[] = []; for (const [key, propSchema] of Object.entries(props)) { const propName = safeName(key); const propType = swiftType(propSchema, required.has(key), true); 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, true)}${req ? "" : "?"}`; }) .join(",\n") + ")\n" + " {\n" + Object.entries(props) .map(([key]) => { const propName = safeName(key); return ` self.${propName} = ${propName}`; }) .join("\n") + "\n }\n\n" + " private enum CodingKeys: String, CodingKey {\n" + codingKeys.join("\n") + "\n }\n}", ); lines.push(""); return lines.join("\n"); } function literalSchemaValue(schema: JsonSchema): boolean | number | string | null | undefined { if ("const" in schema) { return schema.const; } if (schema.enum?.length === 1) { return schema.enum[0] ?? undefined; } return undefined; } function swiftLiteralTypeName(value: boolean | number | string | null): string { if (typeof value === "boolean") { return "Bool"; } if (typeof value === "number") { return Number.isInteger(value) ? "Int" : "Double"; } if (value === null) { return "AnyCodable"; } return "String"; } function swiftLiteralSource(value: boolean | number | string | null): string { if (typeof value === "string") { return JSON.stringify(value); } if (value === null) { return "AnyCodable(nil)"; } return String(value); } function swiftUnionCaseName(value: boolean | number | string | null, fallback: string): string { if (typeof value === "boolean") { return value ? "success" : "failure"; } if (value === null) { return fallback; } return safeName(String(value)); } function emitDiscriminatedUnion(name: string, schema: JsonSchema): string | undefined { const branches = schema.oneOf ?? schema.anyOf; if (!branches || branches.length < 2) { return undefined; } const objectBranches = branches.filter((branch) => branch.type === "object"); if (objectBranches.length !== branches.length) { return undefined; } const discriminatorCandidates = Object.keys(objectBranches[0]?.properties ?? {}); for (const discriminator of discriminatorCandidates) { const cases = objectBranches.map((branch, index) => { const discriminatorSchema = branch.properties?.[discriminator]; const literal = discriminatorSchema ? literalSchemaValue(discriminatorSchema) : undefined; const branchName = namedSchema(branch, true); if (literal === undefined || !branchName) { return undefined; } return { branchName, caseName: swiftUnionCaseName(literal, `case${index + 1}`), literal, }; }); if (cases.some((entry) => !entry)) { continue; } const resolvedCases: Array<{ branchName: string; caseName: string; literal: boolean | number | string | null; }> = cases; const [firstCase] = resolvedCases; if (!firstCase) { continue; } const literalType = swiftLiteralTypeName(firstCase.literal); if ( resolvedCases.some((entry) => swiftLiteralTypeName(entry.literal) !== literalType) || new Set(resolvedCases.map((entry) => String(entry.literal))).size !== resolvedCases.length ) { continue; } return [ `public enum ${name}: Codable, Sendable {`, ...resolvedCases.map((entry) => ` case ${entry.caseName}(${entry.branchName})`), "", " private enum CodingKeys: String, CodingKey {", ` case discriminator = "${discriminator}"`, " }", "", " public init(from decoder: Decoder) throws {", " let container = try decoder.container(keyedBy: CodingKeys.self)", ` let discriminator = try container.decode(${literalType}.self, forKey: .discriminator)`, " switch discriminator {", ...resolvedCases.map( (entry) => ` case ${swiftLiteralSource(entry.literal)}: self = try .${entry.caseName}(${entry.branchName}(from: decoder))`, ), " default:", " throw DecodingError.dataCorruptedError(", " forKey: .discriminator,", " in: container,", ` debugDescription: "Unknown ${name} discriminator value"`, " )", " }", " }", "", " public func encode(to encoder: Encoder) throws {", " switch self {", ...resolvedCases.map( (entry) => ` case .${entry.caseName}(let value): try value.encode(to: encoder)`, ), " }", " }", "}", "", ].join("\n"); } return undefined; } function emitGatewayFrame(): string { const cases = ["req", "res", "event"]; const associated: Record = { 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 = try .req(RequestFrame(from: decoder)) case "res": self = try .res(ResponseFrame(from: decoder)) case "event": self = try .event(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 let .req(v): try v.encode(to: encoder) case let .res(v): try v.encode(to: encoder) case let .event(v): try v.encode(to: encoder) case let .unknown(_, raw): var container = encoder.singleValueContainer() try container.encode(raw) } } `; return [ "public enum GatewayFrame: Codable, Sendable {", ...caseLines, " case unknown(type: String, raw: [String: AnyCodable])", initLines.trimEnd(), "}", "", ].join("\n"); } async function generate() { const definitions = Object.entries(ProtocolSchemas) as Array<[string, JsonSchema]>; for (const [name, schema] of definitions) { registerNamedSchema(name, schema); } const parts: string[] = []; parts.push(header); // Named enums and value structs for (const [name, schema] of definitions) { if (name === "GatewayFrame") { continue; } if (schema.type === "string" && schema.enum) { parts.push(emitEnum(name, schema)); } } for (const [name, schema] of definitions) { if (name === "GatewayFrame") { continue; } if (schema.type === "object") { parts.push(emitStruct(name, schema)); } } for (const [name, schema] of definitions) { if (name === "GatewayFrame") { continue; } const union = emitDiscriminatedUnion(name, schema); if (union) { parts.push(union); } } // 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); });