Mac: build GatewayProtocol target and typed presence handling

This commit is contained in:
Peter Steinberger
2025-12-09 15:35:06 +01:00
parent a7737912b0
commit 336c9d6caa
6 changed files with 262 additions and 101 deletions

View File

@@ -31,27 +31,67 @@ const header = `// Generated by scripts/protocol-gen-swift.ts — do not edit by
.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("_")
.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;
if (t === "string") 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 = schema.enum.map((v) => `\"${v}\"`).join(" | ");
base = "String"; // simplify enums to String; custom enums could be added if needed
base = "String";
} else if (schema.patternProperties) {
base = "[String: AnyCodable]";
} else if (t === "object") {
@@ -71,15 +111,21 @@ function emitStruct(name: string, schema: JsonSchema): string {
lines.push("}\n");
return lines.join("\n");
}
const codingKeys: string[] = [];
for (const [key, propSchema] of Object.entries(props)) {
const propName = key === "description" ? "desc" : key;
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 = key === "description" ? "desc" : key;
const propName = safeName(key);
const req = required.has(key);
return ` ${propName}: ${swiftType(prop, true)}${req ? "" : "?"}`;
})
@@ -87,24 +133,20 @@ function emitStruct(name: string, schema: JsonSchema): string {
"\n ) {\n" +
Object.entries(props)
.map(([key]) => {
const propName = key === "description" ? "desc" : 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 = [
"hello",
"hello-ok",
"hello-error",
"req",
"res",
"event",
];
const cases = ["hello", "hello-ok", "hello-error", "req", "res", "event"];
const associated: Record<string, string> = {
hello: "Hello",
"hello-ok": "HelloOk",
@@ -113,7 +155,7 @@ function emitGatewayFrame(): string {
res: "ResponseFrame",
event: "EventFrame",
};
const caseLines = cases.map((c) => ` case ${camelCase(c)}(${associated[c]})`);
const caseLines = cases.map((c) => ` case ${safeName(c)}(${associated[c]})`);
const initLines = `
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
@@ -123,17 +165,17 @@ function emitGatewayFrame(): string {
}
switch type {
case "hello":
self = .hello(try decodePayload(Hello.self, from: raw))
self = .hello(try Self.decodePayload(Hello.self, from: raw))
case "hello-ok":
self = .helloOk(try decodePayload(HelloOk.self, from: raw))
self = .helloOk(try Self.decodePayload(HelloOk.self, from: raw))
case "hello-error":
self = .helloError(try decodePayload(HelloError.self, from: raw))
self = .helloError(try Self.decodePayload(HelloError.self, from: raw))
case "req":
self = .req(try decodePayload(RequestFrame.self, from: raw))
self = .req(try Self.decodePayload(RequestFrame.self, from: raw))
case "res":
self = .res(try decodePayload(ResponseFrame.self, from: raw))
self = .res(try Self.decodePayload(ResponseFrame.self, from: raw))
case "event":
self = .event(try decodePayload(EventFrame.self, from: raw))
self = .event(try Self.decodePayload(EventFrame.self, from: raw))
default:
self = .unknown(type: type, raw: raw)
}
@@ -155,7 +197,7 @@ function emitGatewayFrame(): string {
`;
const helper = `
private func decodePayload<T: Decodable>(_ type: T.Type, from raw: [String: AnyCodable]) throws -> T {
private static 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)
@@ -178,6 +220,10 @@ async function generate() {
[string, JsonSchema]
>;
for (const [name, schema] of definitions) {
schemaNameByObject.set(schema as object, name);
}
const parts: string[] = [];
parts.push(header);