201 lines
6.1 KiB
TypeScript
201 lines
6.1 KiB
TypeScript
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 outPath = path.join(
|
|
repoRoot,
|
|
"apps",
|
|
"macos",
|
|
"Sources",
|
|
"ClawdisProtocol",
|
|
"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 {\n${Object.values(ErrorCodes)
|
|
.map((c) => ` case ${camelCase(c)} = "${c}"`)
|
|
.join("\n")}\n}\n`;
|
|
|
|
function camelCase(input: string) {
|
|
return input
|
|
.toLowerCase()
|
|
.split("_")
|
|
.map((p, i) => (i === 0 ? p : p[0].toUpperCase() + p.slice(1)))
|
|
.join("");
|
|
}
|
|
|
|
function swiftType(schema: JsonSchema, required: boolean): string {
|
|
const t = schema.type;
|
|
const isOptional = !required;
|
|
let base: string;
|
|
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 = schema.enum.map((v) => `\"${v}\"`).join(" | ");
|
|
base = "String"; // simplify enums to String; custom enums could be added if needed
|
|
} 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 {`);
|
|
if (Object.keys(props).length === 0) {
|
|
lines.push("}\n");
|
|
return lines.join("\n");
|
|
}
|
|
for (const [key, propSchema] of Object.entries(props)) {
|
|
const propName = key === "description" ? "desc" : key;
|
|
const propType = swiftType(propSchema, required.has(key));
|
|
lines.push(` public let ${propName}: ${propType}`);
|
|
}
|
|
lines.push("\n public init(\n" +
|
|
Object.entries(props)
|
|
.map(([key, prop]) => {
|
|
const propName = key === "description" ? "desc" : key;
|
|
const req = required.has(key);
|
|
return ` ${propName}: ${swiftType(prop, true)}${req ? "" : "?"}`;
|
|
})
|
|
.join(",\n") +
|
|
"\n ) {\n" +
|
|
Object.entries(props)
|
|
.map(([key]) => {
|
|
const propName = key === "description" ? "desc" : key;
|
|
return ` self.${propName} = ${propName}`;
|
|
})
|
|
.join("\n") +
|
|
"\n }\n}");
|
|
lines.push("");
|
|
return lines.join("\n");
|
|
}
|
|
|
|
function emitGatewayFrame(): string {
|
|
const cases = [
|
|
"hello",
|
|
"hello-ok",
|
|
"hello-error",
|
|
"req",
|
|
"res",
|
|
"event",
|
|
];
|
|
const associated: Record<string, string> = {
|
|
hello: "Hello",
|
|
"hello-ok": "HelloOk",
|
|
"hello-error": "HelloError",
|
|
req: "RequestFrame",
|
|
res: "ResponseFrame",
|
|
event: "EventFrame",
|
|
};
|
|
const caseLines = cases.map((c) => ` case ${camelCase(c)}(${associated[c]})`);
|
|
const initLines = `
|
|
public init(from decoder: Decoder) throws {
|
|
let container = try decoder.singleValueContainer()
|
|
let raw = try container.decode([String: AnyCodable].self)
|
|
guard let type = raw["type"]?.value as? String else {
|
|
throw DecodingError.dataCorruptedError(in: container, debugDescription: "missing type")
|
|
}
|
|
switch type {
|
|
case "hello":
|
|
self = .hello(try decodePayload(Hello.self, from: raw))
|
|
case "hello-ok":
|
|
self = .helloOk(try decodePayload(HelloOk.self, from: raw))
|
|
case "hello-error":
|
|
self = .helloError(try decodePayload(HelloError.self, from: raw))
|
|
case "req":
|
|
self = .req(try decodePayload(RequestFrame.self, from: raw))
|
|
case "res":
|
|
self = .res(try decodePayload(ResponseFrame.self, from: raw))
|
|
case "event":
|
|
self = .event(try decodePayload(EventFrame.self, from: raw))
|
|
default:
|
|
throw DecodingError.dataCorruptedError(in: container, debugDescription: "unknown type \(type)")
|
|
}
|
|
}
|
|
|
|
public func encode(to encoder: Encoder) throws {
|
|
switch self {
|
|
case .hello(let v): try v.encode(to: encoder)
|
|
case .helloOk(let v): try v.encode(to: encoder)
|
|
case .helloError(let v): try v.encode(to: encoder)
|
|
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)
|
|
}
|
|
}
|
|
`;
|
|
|
|
const helper = `
|
|
private func decodePayload<T: Decodable>(_ type: T.Type, from raw: [String: AnyCodable]) throws -> T {
|
|
let data = try JSONSerialization.data(withJSONObject: raw)
|
|
let decoder = JSONDecoder()
|
|
return try decoder.decode(T.self, from: data)
|
|
}
|
|
`;
|
|
|
|
return [
|
|
"public enum GatewayFrame: Codable {",
|
|
...caseLines,
|
|
initLines,
|
|
helper,
|
|
"}",
|
|
"",
|
|
].join("\n");
|
|
}
|
|
|
|
async function generate() {
|
|
const definitions = Object.entries(ProtocolSchemas) as Array<
|
|
[string, JsonSchema]
|
|
>;
|
|
|
|
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");
|
|
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);
|
|
});
|