Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
768
scripts/protocol-gen-swift.ts
Normal file
768
scripts/protocol-gen-swift.ts
Normal file
@@ -0,0 +1,768 @@
|
||||
// Protocol Gen Swift script supports OpenClaw repository automation.
|
||||
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 "../packages/gateway-protocol/src/schema.js";
|
||||
|
||||
type JsonSchema = {
|
||||
type?: string | string[];
|
||||
const?: boolean | number | string | null;
|
||||
properties?: Record<string, JsonSchema>;
|
||||
required?: string[];
|
||||
items?: JsonSchema;
|
||||
enum?: string[];
|
||||
patternProperties?: Record<string, JsonSchema>;
|
||||
anyOf?: JsonSchema[];
|
||||
oneOf?: JsonSchema[];
|
||||
additionalProperties?: boolean | JsonSchema;
|
||||
};
|
||||
|
||||
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = path.resolve(scriptDir, "..");
|
||||
const outPaths = [
|
||||
path.join(
|
||||
repoRoot,
|
||||
"apps",
|
||||
"shared",
|
||||
"OpenClawKit",
|
||||
"Sources",
|
||||
"OpenClawProtocol",
|
||||
"GatewayModels.swift",
|
||||
),
|
||||
];
|
||||
|
||||
const STRICT_LITERAL_STRUCTS = new Set([
|
||||
"PluginsSessionActionSuccessResult",
|
||||
"PluginsSessionActionFailureResult",
|
||||
]);
|
||||
|
||||
const DEFAULTED_OPTIONAL_INIT_PARAM_ENTRIES: readonly [string, readonly string[]][] = [
|
||||
["CrestodianChatResult", ["sensitive"]],
|
||||
["SendParams", ["buffer", "filename", "contentType"]],
|
||||
["SessionOperationEvent", ["agentId"]],
|
||||
["SessionsCompactionListParams", ["agentId"]],
|
||||
["SessionsCompactionGetParams", ["agentId"]],
|
||||
["SessionsCompactionBranchParams", ["agentId"]],
|
||||
["SessionsCompactionRestoreParams", ["agentId"]],
|
||||
["SessionsSendParams", ["agentId"]],
|
||||
["SessionsMessagesSubscribeParams", ["agentId"]],
|
||||
["SessionsMessagesUnsubscribeParams", ["agentId"]],
|
||||
["SessionsAbortParams", ["agentId"]],
|
||||
["SessionsListParams", ["archived"]],
|
||||
["SessionsPatchParams", ["agentId", "archived", "pinned"]],
|
||||
["SessionsResetParams", ["agentId"]],
|
||||
[
|
||||
"SessionsDeleteParams",
|
||||
["agentId", "expectedSessionId", "expectedLifecycleRevision", "expectedSessionUpdatedAt"],
|
||||
],
|
||||
["SessionsCompactParams", ["agentId"]],
|
||||
["SessionsResolveParams", ["allowMissing"]],
|
||||
["SessionsUsageParams", ["agentId", "agentScope"]],
|
||||
["ChatHistoryParams", ["agentId", "offset"]],
|
||||
["ChatSendParams", ["agentId"]],
|
||||
["ChatAbortParams", ["agentId", "preserveSideRuns"]],
|
||||
["ChatInjectParams", ["agentId"]],
|
||||
["ChatDeltaEvent", ["agentId"]],
|
||||
["ChatFinalEvent", ["agentId"]],
|
||||
["ChatAbortedEvent", ["agentId"]],
|
||||
["ChatErrorEvent", ["agentId"]],
|
||||
["ArtifactsListParams", ["agentId"]],
|
||||
["ArtifactsGetParams", ["agentId"]],
|
||||
["ArtifactsDownloadParams", ["agentId"]],
|
||||
["ChatAbortParams", ["agentId", "preserveSideRuns"]],
|
||||
["ChatAbortedEvent", ["agentId"]],
|
||||
["ChatDeltaEvent", ["agentId"]],
|
||||
["ChatErrorEvent", ["agentId"]],
|
||||
["ChatFinalEvent", ["agentId"]],
|
||||
["ChatHistoryParams", ["agentId", "offset"]],
|
||||
["ChatInjectParams", ["agentId"]],
|
||||
["ChatSendParams", ["agentId"]],
|
||||
["MessageActionParams", ["inboundTurnKind", "requesterAccountId"]],
|
||||
["CronListParams", ["compact"]],
|
||||
["CronRunLogEntry", ["errorReason", "failureNotificationDelivery"]],
|
||||
["ExecApprovalRequestParams", ["requireDeliveryRoute", "suppressDelivery"]],
|
||||
["AgentSummary", ["thinkingLevels", "thinkingOptions", "thinkingDefault"]],
|
||||
["ModelChoice", ["available"]],
|
||||
];
|
||||
|
||||
const DEFAULTED_OPTIONAL_INIT_PARAMS: Record<string, Set<string>> = Object.fromEntries(
|
||||
DEFAULTED_OPTIONAL_INIT_PARAM_ENTRIES.map(([name, params]) => [name, new Set(params)]),
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function swiftStoredPropertyName(structName: string, key: string): string {
|
||||
if (structName === "ChatSendParams" && key === "fastMode") {
|
||||
return "fastmodevalue";
|
||||
}
|
||||
if (structName === "ChatSendParams" && key === "fast_seconds") {
|
||||
return "fastseconds";
|
||||
}
|
||||
return safeName(key);
|
||||
}
|
||||
|
||||
function swiftInitializerName(structName: string, key: string): string {
|
||||
if (structName === "ChatSendParams" && key === "fastMode") {
|
||||
return "fastmodevalue";
|
||||
}
|
||||
if (structName === "ChatSendParams" && key === "fast_seconds") {
|
||||
return "fastseconds";
|
||||
}
|
||||
return safeName(key);
|
||||
}
|
||||
|
||||
function swiftCompatibilityPropertyLines(structName: string, key: string): string[] {
|
||||
if (structName === "ChatSendParams" && key === "fastMode") {
|
||||
return [" public var fastmode: Bool? { fastmodevalue?.value as? Bool }"];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// filled later once schemas are loaded
|
||||
const schemaNameByObject = new Map<object, string>();
|
||||
const schemaNameBySignature = new Map<string, string>();
|
||||
const duplicateSchemaSignatures = new Set<string>();
|
||||
|
||||
function stableJson(value: unknown): unknown {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(stableJson);
|
||||
}
|
||||
if (value && typeof value === "object") {
|
||||
const record = value as Record<string, unknown>;
|
||||
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 stringEnumCases(schema: JsonSchema): string[] | undefined {
|
||||
if (schema.type === "string" && schema.enum) {
|
||||
return schema.enum.every((value) => typeof value === "string") ? schema.enum : undefined;
|
||||
}
|
||||
|
||||
const variants = schema.oneOf ?? schema.anyOf;
|
||||
if (!variants?.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const cases = variants
|
||||
.map((variant) => literalSchemaValue(variant))
|
||||
.filter((value): value is string => typeof value === "string");
|
||||
return cases.length === variants.length ? cases : undefined;
|
||||
}
|
||||
|
||||
function swiftInitializerParam(params: {
|
||||
structName: string;
|
||||
key: string;
|
||||
name: string;
|
||||
schema: JsonSchema;
|
||||
required: boolean;
|
||||
allowStructuralNamed?: boolean;
|
||||
}): string {
|
||||
const type = swiftType(params.schema, true, params.allowStructuralNamed ?? true);
|
||||
if (params.required) {
|
||||
return `${params.name}: ${type}`;
|
||||
}
|
||||
const defaultNil =
|
||||
params.key === "agentId" ||
|
||||
(DEFAULTED_OPTIONAL_INIT_PARAMS[params.structName]?.has(params.key) ?? false);
|
||||
return `${params.name}: ${type}?${defaultNil ? " = nil" : ""}`;
|
||||
}
|
||||
|
||||
function emitEnum(name: string, schema: JsonSchema): string {
|
||||
const cases = stringEnumCases(schema) ?? [];
|
||||
return [
|
||||
`public enum ${name}: String, Codable, Sendable {`,
|
||||
...cases.map((value) => ` case ${safeName(value)} = "${value}"`),
|
||||
"}",
|
||||
"",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function stringLiteralUnionValues(schema: JsonSchema): string[] | undefined {
|
||||
const branches = schema.oneOf ?? schema.anyOf;
|
||||
if (!branches || branches.length < 2) {
|
||||
return undefined;
|
||||
}
|
||||
const values = branches.map((branch) => literalSchemaValue(branch));
|
||||
if (values.some((value) => typeof value !== "string")) {
|
||||
return undefined;
|
||||
}
|
||||
const stringValues = values as string[];
|
||||
return new Set(stringValues).size === stringValues.length ? stringValues : undefined;
|
||||
}
|
||||
|
||||
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 ` ${swiftInitializerParam({
|
||||
structName: name,
|
||||
key,
|
||||
name: propName,
|
||||
schema: prop,
|
||||
required: 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 = swiftStoredPropertyName(name, key);
|
||||
const propType = swiftType(propSchema, required.has(key), true);
|
||||
lines.push(` public let ${propName}: ${propType}`);
|
||||
lines.push(...swiftCompatibilityPropertyLines(name, key));
|
||||
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 = swiftInitializerName(name, key);
|
||||
const req = required.has(key);
|
||||
return ` ${swiftInitializerParam({
|
||||
structName: name,
|
||||
key,
|
||||
name: propName,
|
||||
schema: prop,
|
||||
required: req,
|
||||
})}`;
|
||||
})
|
||||
.join(",\n") +
|
||||
")\n" +
|
||||
" {\n" +
|
||||
Object.entries(props)
|
||||
.map(([key]) => {
|
||||
const propName = swiftStoredPropertyName(name, key);
|
||||
const paramName = swiftInitializerName(name, key);
|
||||
return ` self.${propName} = ${paramName}`;
|
||||
})
|
||||
.join("\n") +
|
||||
"\n }" +
|
||||
emitStructCompatibilityInitializer(name, props, required) +
|
||||
"\n\n" +
|
||||
" private enum CodingKeys: String, CodingKey {\n" +
|
||||
codingKeys.join("\n") +
|
||||
"\n }\n}",
|
||||
);
|
||||
lines.push("");
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function emitStructCompatibilityInitializer(
|
||||
name: string,
|
||||
props: Record<string, JsonSchema>,
|
||||
required: Set<string>,
|
||||
): string {
|
||||
if (name !== "ChatSendParams" || !props.fastMode) {
|
||||
return "";
|
||||
}
|
||||
const legacyKeys = Object.keys(props).filter(
|
||||
(key) => key !== "fastAutoOnSeconds" && key !== "fast_seconds",
|
||||
);
|
||||
const initializerParams = legacyKeys.map((key) => {
|
||||
const prop = props[key];
|
||||
if (!prop) {
|
||||
throw new Error(`missing ${name}.${key} schema`);
|
||||
}
|
||||
const propName = swiftInitializerName(name, key);
|
||||
if (key === "fastMode") {
|
||||
return " fastmode: Bool?";
|
||||
}
|
||||
return ` ${swiftInitializerParam({
|
||||
structName: name,
|
||||
key,
|
||||
name: propName,
|
||||
schema: prop,
|
||||
required: required.has(key),
|
||||
})}`;
|
||||
});
|
||||
const delegatedArgs = Object.keys(props).map((key) => {
|
||||
const propName = swiftInitializerName(name, key);
|
||||
if (key === "fastMode") {
|
||||
return " fastmodevalue: fastmode.map { AnyCodable($0) }";
|
||||
}
|
||||
if (key === "fastAutoOnSeconds" || key === "fast_seconds") {
|
||||
return ` ${propName}: nil`;
|
||||
}
|
||||
return ` ${propName}: ${propName}`;
|
||||
});
|
||||
return (
|
||||
"\n\n public init(\n" +
|
||||
initializerParams.join(",\n") +
|
||||
")\n" +
|
||||
" {\n" +
|
||||
" self.init(\n" +
|
||||
delegatedArgs.join(",\n") +
|
||||
")\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;
|
||||
}
|
||||
const coversAllBoolCases =
|
||||
literalType === "Bool" &&
|
||||
resolvedCases.some((entry) => entry.literal === true) &&
|
||||
resolvedCases.some((entry) => entry.literal === false);
|
||||
const unknownDiscriminatorLines = coversAllBoolCases
|
||||
? []
|
||||
: [
|
||||
" default:",
|
||||
" throw DecodingError.dataCorruptedError(",
|
||||
" forKey: .discriminator,",
|
||||
" in: container,",
|
||||
` debugDescription: "Unknown ${name} discriminator value"`,
|
||||
" )",
|
||||
];
|
||||
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))`,
|
||||
),
|
||||
...unknownDiscriminatorLines,
|
||||
" }",
|
||||
" }",
|
||||
"",
|
||||
" 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<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 = 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 (stringEnumCases(schema)) {
|
||||
parts.push(emitEnum(name, schema));
|
||||
continue;
|
||||
}
|
||||
const literalUnionValues = stringLiteralUnionValues(schema);
|
||||
if (literalUnionValues) {
|
||||
parts.push(emitEnum(name, { enum: literalUnionValues }));
|
||||
}
|
||||
}
|
||||
|
||||
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: unknown) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user