Add ez-assistant and kerberos service folders
This commit is contained in:
245
docker-compose/ez-assistant/scripts/protocol-gen-swift.ts
Normal file
245
docker-compose/ez-assistant/scripts/protocol-gen-swift.ts
Normal file
@@ -0,0 +1,245 @@
|
||||
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",
|
||||
"MoltbotProtocol",
|
||||
"GatewayModels.swift",
|
||||
),
|
||||
path.join(
|
||||
repoRoot,
|
||||
"apps",
|
||||
"shared",
|
||||
"MoltbotKit",
|
||||
"Sources",
|
||||
"MoltbotProtocol",
|
||||
"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);
|
||||
});
|
||||
Reference in New Issue
Block a user