Protocol: add TypeBox-driven Swift generator

This commit is contained in:
Peter Steinberger
2025-12-09 15:21:16 +01:00
parent cf5769753a
commit b0c196cf82
5 changed files with 630 additions and 902 deletions

View File

@@ -0,0 +1,41 @@
import Foundation
/// Lightweight `Codable` wrapper that round-trips heterogeneous JSON payloads.
/// Marked `@unchecked Sendable` because it can hold reference types.
struct AnyCodable: Codable, @unchecked Sendable {
let value: Any
init(_ value: Any) { self.value = value }
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let intVal = try? container.decode(Int.self) { self.value = intVal; return }
if let doubleVal = try? container.decode(Double.self) { self.value = doubleVal; return }
if let boolVal = try? container.decode(Bool.self) { self.value = boolVal; return }
if let stringVal = try? container.decode(String.self) { self.value = stringVal; return }
if container.decodeNil() { self.value = NSNull(); return }
if let dict = try? container.decode([String: AnyCodable].self) { self.value = dict; return }
if let array = try? container.decode([AnyCodable].self) { self.value = array; return }
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "Unsupported type")
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self.value {
case let intVal as Int: try container.encode(intVal)
case let doubleVal as Double: try container.encode(doubleVal)
case let boolVal as Bool: try container.encode(boolVal)
case let stringVal as String: try container.encode(stringVal)
case is NSNull: try container.encodeNil()
case let dict as [String: AnyCodable]: try container.encode(dict)
case let array as [AnyCodable]: try container.encode(array)
default:
let context = EncodingError.Context(
codingPath: encoder.codingPath,
debugDescription: "Unsupported type")
throw EncodingError.invalidValue(self.value, context)
}
}
}

View File

@@ -0,0 +1,387 @@
// Generated by scripts/protocol-gen-swift.ts do not edit by hand
import Foundation
public let GATEWAY_PROTOCOL_VERSION = 1
public enum ErrorCode: String, Codable {
case notLinked = "NOT_LINKED"
case agentTimeout = "AGENT_TIMEOUT"
case invalidRequest = "INVALID_REQUEST"
case unavailable = "UNAVAILABLE"
}
public struct Hello: Codable {
public let type: String
public let minProtocol: Int
public let maxProtocol: Int
public let client: [String: AnyCodable]
public let caps: [String]?
public let auth: [String: AnyCodable]?
public let locale: String?
public let userAgent: String?
public init(
type: String,
minProtocol: Int,
maxProtocol: Int,
client: [String: AnyCodable],
caps: [String]?,
auth: [String: AnyCodable]?,
locale: String?,
userAgent: String?
) {
self.type = type
self.minProtocol = minProtocol
self.maxProtocol = maxProtocol
self.client = client
self.caps = caps
self.auth = auth
self.locale = locale
self.userAgent = userAgent
}
}
public struct HelloOk: Codable {
public let type: String
public let protocol: Int
public let server: [String: AnyCodable]
public let features: [String: AnyCodable]
public let snapshot: [String: AnyCodable]
public let policy: [String: AnyCodable]
public init(
type: String,
protocol: Int,
server: [String: AnyCodable],
features: [String: AnyCodable],
snapshot: [String: AnyCodable],
policy: [String: AnyCodable]
) {
self.type = type
self.protocol = protocol
self.server = server
self.features = features
self.snapshot = snapshot
self.policy = policy
}
}
public struct HelloError: Codable {
public let type: String
public let reason: String
public let expectedProtocol: Int?
public let minClient: String?
public init(
type: String,
reason: String,
expectedProtocol: Int?,
minClient: String?
) {
self.type = type
self.reason = reason
self.expectedProtocol = expectedProtocol
self.minClient = minClient
}
}
public struct RequestFrame: Codable {
public let type: String
public let id: String
public let method: String
public let params: AnyCodable?
public init(
type: String,
id: String,
method: String,
params: AnyCodable?
) {
self.type = type
self.id = id
self.method = method
self.params = params
}
}
public struct ResponseFrame: Codable {
public let type: String
public let id: String
public let ok: Bool
public let payload: AnyCodable?
public let error: [String: AnyCodable]?
public init(
type: String,
id: String,
ok: Bool,
payload: AnyCodable?,
error: [String: AnyCodable]?
) {
self.type = type
self.id = id
self.ok = ok
self.payload = payload
self.error = error
}
}
public struct EventFrame: Codable {
public let type: String
public let event: String
public let payload: AnyCodable?
public let seq: Int?
public let stateVersion: [String: AnyCodable]?
public init(
type: String,
event: String,
payload: AnyCodable?,
seq: Int?,
stateVersion: [String: AnyCodable]?
) {
self.type = type
self.event = event
self.payload = payload
self.seq = seq
self.stateVersion = stateVersion
}
}
public struct PresenceEntry: Codable {
public let host: String?
public let ip: String?
public let version: String?
public let mode: String?
public let lastInputSeconds: Int?
public let reason: String?
public let tags: [String]?
public let text: String?
public let ts: Int
public let instanceId: String?
public init(
host: String?,
ip: String?,
version: String?,
mode: String?,
lastInputSeconds: Int?,
reason: String?,
tags: [String]?,
text: String?,
ts: Int,
instanceId: String?
) {
self.host = host
self.ip = ip
self.version = version
self.mode = mode
self.lastInputSeconds = lastInputSeconds
self.reason = reason
self.tags = tags
self.text = text
self.ts = ts
self.instanceId = instanceId
}
}
public struct StateVersion: Codable {
public let presence: Int
public let health: Int
public init(
presence: Int,
health: Int
) {
self.presence = presence
self.health = health
}
}
public struct Snapshot: Codable {
public let presence: [[String: AnyCodable]]
public let health: AnyCodable
public let stateVersion: [String: AnyCodable]
public let uptimeMs: Int
public init(
presence: [[String: AnyCodable]],
health: AnyCodable,
stateVersion: [String: AnyCodable],
uptimeMs: Int
) {
self.presence = presence
self.health = health
self.stateVersion = stateVersion
self.uptimeMs = uptimeMs
}
}
public struct ErrorShape: Codable {
public let code: String
public let message: String
public let details: AnyCodable?
public let retryable: Bool?
public let retryAfterMs: Int?
public init(
code: String,
message: String,
details: AnyCodable?,
retryable: Bool?,
retryAfterMs: Int?
) {
self.code = code
self.message = message
self.details = details
self.retryable = retryable
self.retryAfterMs = retryAfterMs
}
}
public struct AgentEvent: Codable {
public let runId: String
public let seq: Int
public let stream: String
public let ts: Int
public let data: [String: AnyCodable]
public init(
runId: String,
seq: Int,
stream: String,
ts: Int,
data: [String: AnyCodable]
) {
self.runId = runId
self.seq = seq
self.stream = stream
self.ts = ts
self.data = data
}
}
public struct SendParams: Codable {
public let to: String
public let message: String
public let mediaUrl: String?
public let provider: String?
public let idempotencyKey: String
public init(
to: String,
message: String,
mediaUrl: String?,
provider: String?,
idempotencyKey: String
) {
self.to = to
self.message = message
self.mediaUrl = mediaUrl
self.provider = provider
self.idempotencyKey = idempotencyKey
}
}
public struct AgentParams: Codable {
public let message: String
public let to: String?
public let sessionId: String?
public let thinking: String?
public let deliver: Bool?
public let timeout: Int?
public let idempotencyKey: String
public init(
message: String,
to: String?,
sessionId: String?,
thinking: String?,
deliver: Bool?,
timeout: Int?,
idempotencyKey: String
) {
self.message = message
self.to = to
self.sessionId = sessionId
self.thinking = thinking
self.deliver = deliver
self.timeout = timeout
self.idempotencyKey = idempotencyKey
}
}
public struct TickEvent: Codable {
public let ts: Int
public init(
ts: Int
) {
self.ts = ts
}
}
public struct ShutdownEvent: Codable {
public let reason: String
public let restartExpectedMs: Int?
public init(
reason: String,
restartExpectedMs: Int?
) {
self.reason = reason
self.restartExpectedMs = restartExpectedMs
}
}
public enum GatewayFrame: Codable {
case hello(Hello)
case hello-ok(HelloOk)
case hello-error(HelloError)
case req(RequestFrame)
case res(ResponseFrame)
case event(EventFrame)
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)
}
}
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)
}
}

View File

@@ -1,901 +0,0 @@
// This file was generated from JSON Schema using quicktype, do not modify it directly.
// To parse the JSON, add this file to your project and do:
//
// let clawdisGateway = try ClawdisGateway(json)
import Foundation
// MARK: - ClawdisGateway
/// Handshake, request/response, and event frames for the Gateway WebSocket.
struct ClawdisGateway: Codable {
let auth: Auth?
let caps: [String]?
let client: Client?
let locale: String?
let maxProtocol, minProtocol: Int?
let type: TypeEnum
let userAgent: String?
let features: Features?
let policy: Policy?
let clawdisGatewayProtocol: Int?
let server: Server?
let snapshot: Snapshot?
let expectedProtocol: Int?
let minClient, reason, id, method: String?
let params: JSONAny?
let error: Error?
let ok: Bool?
let payload: JSONAny?
let event: String?
let seq: Int?
let stateVersion: ClawdisGatewayStateVersion?
enum CodingKeys: String, CodingKey {
case auth, caps, client, locale, maxProtocol, minProtocol, type, userAgent, features, policy
case clawdisGatewayProtocol = "protocol"
case server, snapshot, expectedProtocol, minClient, reason, id, method, params, error, ok, payload, event, seq,
stateVersion
}
}
// MARK: ClawdisGateway convenience initializers and mutators
extension ClawdisGateway {
init(data: Data) throws {
self = try newJSONDecoder().decode(ClawdisGateway.self, from: data)
}
init(_ json: String, using encoding: String.Encoding = .utf8) throws {
guard let data = json.data(using: encoding) else {
throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil)
}
try self.init(data: data)
}
init(fromURL url: URL) throws {
try self.init(data: Data(contentsOf: url))
}
func with(
auth: Auth?? = nil,
caps: [String]?? = nil,
client: Client?? = nil,
locale: String?? = nil,
maxProtocol: Int?? = nil,
minProtocol: Int?? = nil,
type: TypeEnum? = nil,
userAgent: String?? = nil,
features: Features?? = nil,
policy: Policy?? = nil,
clawdisGatewayProtocol: Int?? = nil,
server: Server?? = nil,
snapshot: Snapshot?? = nil,
expectedProtocol: Int?? = nil,
minClient: String?? = nil,
reason: String?? = nil,
id: String?? = nil,
method: String?? = nil,
params: JSONAny?? = nil,
error: Error?? = nil,
ok: Bool?? = nil,
payload: JSONAny?? = nil,
event: String?? = nil,
seq: Int?? = nil,
stateVersion: ClawdisGatewayStateVersion?? = nil) -> ClawdisGateway
{
ClawdisGateway(
auth: auth ?? self.auth,
caps: caps ?? self.caps,
client: client ?? self.client,
locale: locale ?? self.locale,
maxProtocol: maxProtocol ?? self.maxProtocol,
minProtocol: minProtocol ?? self.minProtocol,
type: type ?? self.type,
userAgent: userAgent ?? self.userAgent,
features: features ?? self.features,
policy: policy ?? self.policy,
clawdisGatewayProtocol: clawdisGatewayProtocol ?? self.clawdisGatewayProtocol,
server: server ?? self.server,
snapshot: snapshot ?? self.snapshot,
expectedProtocol: expectedProtocol ?? self.expectedProtocol,
minClient: minClient ?? self.minClient,
reason: reason ?? self.reason,
id: id ?? self.id,
method: method ?? self.method,
params: params ?? self.params,
error: error ?? self.error,
ok: ok ?? self.ok,
payload: payload ?? self.payload,
event: event ?? self.event,
seq: seq ?? self.seq,
stateVersion: stateVersion ?? self.stateVersion)
}
func jsonData() throws -> Data {
try newJSONEncoder().encode(self)
}
func jsonString(encoding: String.Encoding = .utf8) throws -> String? {
try String(data: self.jsonData(), encoding: encoding)
}
}
// MARK: - Auth
struct Auth: Codable {
let token: String?
}
// MARK: Auth convenience initializers and mutators
extension Auth {
init(data: Data) throws {
self = try newJSONDecoder().decode(Auth.self, from: data)
}
init(_ json: String, using encoding: String.Encoding = .utf8) throws {
guard let data = json.data(using: encoding) else {
throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil)
}
try self.init(data: data)
}
init(fromURL url: URL) throws {
try self.init(data: Data(contentsOf: url))
}
func with(
token: String?? = nil) -> Auth
{
Auth(
token: token ?? self.token)
}
func jsonData() throws -> Data {
try newJSONEncoder().encode(self)
}
func jsonString(encoding: String.Encoding = .utf8) throws -> String? {
try String(data: self.jsonData(), encoding: encoding)
}
}
// MARK: - Client
struct Client: Codable {
let instanceID: String?
let mode, name, platform, version: String
enum CodingKeys: String, CodingKey {
case instanceID = "instanceId"
case mode, name, platform, version
}
}
// MARK: Client convenience initializers and mutators
extension Client {
init(data: Data) throws {
self = try newJSONDecoder().decode(Client.self, from: data)
}
init(_ json: String, using encoding: String.Encoding = .utf8) throws {
guard let data = json.data(using: encoding) else {
throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil)
}
try self.init(data: data)
}
init(fromURL url: URL) throws {
try self.init(data: Data(contentsOf: url))
}
func with(
instanceID: String?? = nil,
mode: String? = nil,
name: String? = nil,
platform: String? = nil,
version: String? = nil) -> Client
{
Client(
instanceID: instanceID ?? self.instanceID,
mode: mode ?? self.mode,
name: name ?? self.name,
platform: platform ?? self.platform,
version: version ?? self.version)
}
func jsonData() throws -> Data {
try newJSONEncoder().encode(self)
}
func jsonString(encoding: String.Encoding = .utf8) throws -> String? {
try String(data: self.jsonData(), encoding: encoding)
}
}
// MARK: - Error
struct Error: Codable {
let code: String
let details: JSONAny?
let message: String
let retryable: Bool?
let retryAfterMS: Int?
enum CodingKeys: String, CodingKey {
case code, details, message, retryable
case retryAfterMS = "retryAfterMs"
}
}
// MARK: Error convenience initializers and mutators
extension Error {
init(data: Data) throws {
self = try newJSONDecoder().decode(Error.self, from: data)
}
init(_ json: String, using encoding: String.Encoding = .utf8) throws {
guard let data = json.data(using: encoding) else {
throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil)
}
try self.init(data: data)
}
init(fromURL url: URL) throws {
try self.init(data: Data(contentsOf: url))
}
func with(
code: String? = nil,
details: JSONAny?? = nil,
message: String? = nil,
retryable: Bool?? = nil,
retryAfterMS: Int?? = nil) -> Error
{
Error(
code: code ?? self.code,
details: details ?? self.details,
message: message ?? self.message,
retryable: retryable ?? self.retryable,
retryAfterMS: retryAfterMS ?? self.retryAfterMS)
}
func jsonData() throws -> Data {
try newJSONEncoder().encode(self)
}
func jsonString(encoding: String.Encoding = .utf8) throws -> String? {
try String(data: self.jsonData(), encoding: encoding)
}
}
// MARK: - Features
struct Features: Codable {
let events, methods: [String]
}
// MARK: Features convenience initializers and mutators
extension Features {
init(data: Data) throws {
self = try newJSONDecoder().decode(Features.self, from: data)
}
init(_ json: String, using encoding: String.Encoding = .utf8) throws {
guard let data = json.data(using: encoding) else {
throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil)
}
try self.init(data: data)
}
init(fromURL url: URL) throws {
try self.init(data: Data(contentsOf: url))
}
func with(
events: [String]? = nil,
methods: [String]? = nil) -> Features
{
Features(
events: events ?? self.events,
methods: methods ?? self.methods)
}
func jsonData() throws -> Data {
try newJSONEncoder().encode(self)
}
func jsonString(encoding: String.Encoding = .utf8) throws -> String? {
try String(data: self.jsonData(), encoding: encoding)
}
}
// MARK: - Policy
struct Policy: Codable {
let maxBufferedBytes, maxPayload, tickIntervalMS: Int
enum CodingKeys: String, CodingKey {
case maxBufferedBytes, maxPayload
case tickIntervalMS = "tickIntervalMs"
}
}
// MARK: Policy convenience initializers and mutators
extension Policy {
init(data: Data) throws {
self = try newJSONDecoder().decode(Policy.self, from: data)
}
init(_ json: String, using encoding: String.Encoding = .utf8) throws {
guard let data = json.data(using: encoding) else {
throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil)
}
try self.init(data: data)
}
init(fromURL url: URL) throws {
try self.init(data: Data(contentsOf: url))
}
func with(
maxBufferedBytes: Int? = nil,
maxPayload: Int? = nil,
tickIntervalMS: Int? = nil) -> Policy
{
Policy(
maxBufferedBytes: maxBufferedBytes ?? self.maxBufferedBytes,
maxPayload: maxPayload ?? self.maxPayload,
tickIntervalMS: tickIntervalMS ?? self.tickIntervalMS)
}
func jsonData() throws -> Data {
try newJSONEncoder().encode(self)
}
func jsonString(encoding: String.Encoding = .utf8) throws -> String? {
try String(data: self.jsonData(), encoding: encoding)
}
}
// MARK: - Server
struct Server: Codable {
let commit: String?
let connID: String
let host: String?
let version: String
enum CodingKeys: String, CodingKey {
case commit
case connID = "connId"
case host, version
}
}
// MARK: Server convenience initializers and mutators
extension Server {
init(data: Data) throws {
self = try newJSONDecoder().decode(Server.self, from: data)
}
init(_ json: String, using encoding: String.Encoding = .utf8) throws {
guard let data = json.data(using: encoding) else {
throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil)
}
try self.init(data: data)
}
init(fromURL url: URL) throws {
try self.init(data: Data(contentsOf: url))
}
func with(
commit: String?? = nil,
connID: String? = nil,
host: String?? = nil,
version: String? = nil) -> Server
{
Server(
commit: commit ?? self.commit,
connID: connID ?? self.connID,
host: host ?? self.host,
version: version ?? self.version)
}
func jsonData() throws -> Data {
try newJSONEncoder().encode(self)
}
func jsonString(encoding: String.Encoding = .utf8) throws -> String? {
try String(data: self.jsonData(), encoding: encoding)
}
}
// MARK: - Snapshot
struct Snapshot: Codable {
let health: JSONAny
let presence: [Presence]
let stateVersion: SnapshotStateVersion
let uptimeMS: Int
enum CodingKeys: String, CodingKey {
case health, presence, stateVersion
case uptimeMS = "uptimeMs"
}
}
// MARK: Snapshot convenience initializers and mutators
extension Snapshot {
init(data: Data) throws {
self = try newJSONDecoder().decode(Snapshot.self, from: data)
}
init(_ json: String, using encoding: String.Encoding = .utf8) throws {
guard let data = json.data(using: encoding) else {
throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil)
}
try self.init(data: data)
}
init(fromURL url: URL) throws {
try self.init(data: Data(contentsOf: url))
}
func with(
health: JSONAny? = nil,
presence: [Presence]? = nil,
stateVersion: SnapshotStateVersion? = nil,
uptimeMS: Int? = nil) -> Snapshot
{
Snapshot(
health: health ?? self.health,
presence: presence ?? self.presence,
stateVersion: stateVersion ?? self.stateVersion,
uptimeMS: uptimeMS ?? self.uptimeMS)
}
func jsonData() throws -> Data {
try newJSONEncoder().encode(self)
}
func jsonString(encoding: String.Encoding = .utf8) throws -> String? {
try String(data: self.jsonData(), encoding: encoding)
}
}
// MARK: - Presence
struct Presence: Codable {
let host, instanceID, ip: String?
let lastInputSeconds: Int?
let mode, reason: String?
let tags: [String]?
let text: String?
let ts: Int
let version: String?
enum CodingKeys: String, CodingKey {
case host
case instanceID = "instanceId"
case ip, lastInputSeconds, mode, reason, tags, text, ts, version
}
}
// MARK: Presence convenience initializers and mutators
extension Presence {
init(data: Data) throws {
self = try newJSONDecoder().decode(Presence.self, from: data)
}
init(_ json: String, using encoding: String.Encoding = .utf8) throws {
guard let data = json.data(using: encoding) else {
throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil)
}
try self.init(data: data)
}
init(fromURL url: URL) throws {
try self.init(data: Data(contentsOf: url))
}
func with(
host: String?? = nil,
instanceID: String?? = nil,
ip: String?? = nil,
lastInputSeconds: Int?? = nil,
mode: String?? = nil,
reason: String?? = nil,
tags: [String]?? = nil,
text: String?? = nil,
ts: Int? = nil,
version: String?? = nil) -> Presence
{
Presence(
host: host ?? self.host,
instanceID: instanceID ?? self.instanceID,
ip: ip ?? self.ip,
lastInputSeconds: lastInputSeconds ?? self.lastInputSeconds,
mode: mode ?? self.mode,
reason: reason ?? self.reason,
tags: tags ?? self.tags,
text: text ?? self.text,
ts: ts ?? self.ts,
version: version ?? self.version)
}
func jsonData() throws -> Data {
try newJSONEncoder().encode(self)
}
func jsonString(encoding: String.Encoding = .utf8) throws -> String? {
try String(data: self.jsonData(), encoding: encoding)
}
}
// MARK: - SnapshotStateVersion
struct SnapshotStateVersion: Codable {
let health, presence: Int
}
// MARK: SnapshotStateVersion convenience initializers and mutators
extension SnapshotStateVersion {
init(data: Data) throws {
self = try newJSONDecoder().decode(SnapshotStateVersion.self, from: data)
}
init(_ json: String, using encoding: String.Encoding = .utf8) throws {
guard let data = json.data(using: encoding) else {
throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil)
}
try self.init(data: data)
}
init(fromURL url: URL) throws {
try self.init(data: Data(contentsOf: url))
}
func with(
health: Int? = nil,
presence: Int? = nil) -> SnapshotStateVersion
{
SnapshotStateVersion(
health: health ?? self.health,
presence: presence ?? self.presence)
}
func jsonData() throws -> Data {
try newJSONEncoder().encode(self)
}
func jsonString(encoding: String.Encoding = .utf8) throws -> String? {
try String(data: self.jsonData(), encoding: encoding)
}
}
// MARK: - ClawdisGatewayStateVersion
struct ClawdisGatewayStateVersion: Codable {
let health, presence: Int
}
// MARK: ClawdisGatewayStateVersion convenience initializers and mutators
extension ClawdisGatewayStateVersion {
init(data: Data) throws {
self = try newJSONDecoder().decode(ClawdisGatewayStateVersion.self, from: data)
}
init(_ json: String, using encoding: String.Encoding = .utf8) throws {
guard let data = json.data(using: encoding) else {
throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil)
}
try self.init(data: data)
}
init(fromURL url: URL) throws {
try self.init(data: Data(contentsOf: url))
}
func with(
health: Int? = nil,
presence: Int? = nil) -> ClawdisGatewayStateVersion
{
ClawdisGatewayStateVersion(
health: health ?? self.health,
presence: presence ?? self.presence)
}
func jsonData() throws -> Data {
try newJSONEncoder().encode(self)
}
func jsonString(encoding: String.Encoding = .utf8) throws -> String? {
try String(data: self.jsonData(), encoding: encoding)
}
}
enum TypeEnum: String, Codable {
case event
case hello
case helloError = "hello-error"
case helloOk = "hello-ok"
case req
case res
}
// MARK: - Helper functions for creating encoders and decoders
func newJSONDecoder() -> JSONDecoder {
let decoder = JSONDecoder()
if #available(iOS 10.0, OSX 10.12, tvOS 10.0, watchOS 3.0, *) {
decoder.dateDecodingStrategy = .iso8601
}
return decoder
}
func newJSONEncoder() -> JSONEncoder {
let encoder = JSONEncoder()
if #available(iOS 10.0, OSX 10.12, tvOS 10.0, watchOS 3.0, *) {
encoder.dateEncodingStrategy = .iso8601
}
return encoder
}
// MARK: - Encode/decode helpers
class JSONNull: Codable, Hashable {
static func == (lhs: JSONNull, rhs: JSONNull) -> Bool {
true
}
func hash(into hasher: inout Hasher) {
hasher.combine(0)
}
init() {}
required init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if !container.decodeNil() {
throw DecodingError.typeMismatch(
JSONNull.self,
DecodingError.Context(
codingPath: decoder.codingPath,
debugDescription: "Wrong type for JSONNull"))
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encodeNil()
}
}
class JSONCodingKey: CodingKey {
let key: String
required init?(intValue: Int) {
nil
}
required init?(stringValue: String) {
self.key = stringValue
}
var intValue: Int? {
nil
}
var stringValue: String {
self.key
}
}
class JSONAny: Codable {
let value: Any
static func decodingError(forCodingPath codingPath: [CodingKey]) -> DecodingError {
let context = DecodingError.Context(codingPath: codingPath, debugDescription: "Cannot decode JSONAny")
return DecodingError.typeMismatch(JSONAny.self, context)
}
static func encodingError(forValue value: Any, codingPath: [CodingKey]) -> EncodingError {
let context = EncodingError.Context(codingPath: codingPath, debugDescription: "Cannot encode JSONAny")
return EncodingError.invalidValue(value, context)
}
static func decode(from container: SingleValueDecodingContainer) throws -> Any {
if let value = try? container.decode(Bool.self) {
return value
}
if let value = try? container.decode(Int64.self) {
return value
}
if let value = try? container.decode(Double.self) {
return value
}
if let value = try? container.decode(String.self) {
return value
}
if container.decodeNil() {
return JSONNull()
}
throw self.decodingError(forCodingPath: container.codingPath)
}
static func decode(from container: inout UnkeyedDecodingContainer) throws -> Any {
if let value = try? container.decode(Bool.self) {
return value
}
if let value = try? container.decode(Int64.self) {
return value
}
if let value = try? container.decode(Double.self) {
return value
}
if let value = try? container.decode(String.self) {
return value
}
if let value = try? container.decodeNil() {
if value {
return JSONNull()
}
}
if var container = try? container.nestedUnkeyedContainer() {
return try self.decodeArray(from: &container)
}
if var container = try? container.nestedContainer(keyedBy: JSONCodingKey.self) {
return try self.decodeDictionary(from: &container)
}
throw self.decodingError(forCodingPath: container.codingPath)
}
static func decode(
from container: inout KeyedDecodingContainer<JSONCodingKey>,
forKey key: JSONCodingKey) throws -> Any
{
if let value = try? container.decode(Bool.self, forKey: key) {
return value
}
if let value = try? container.decode(Int64.self, forKey: key) {
return value
}
if let value = try? container.decode(Double.self, forKey: key) {
return value
}
if let value = try? container.decode(String.self, forKey: key) {
return value
}
if let value = try? container.decodeNil(forKey: key) {
if value {
return JSONNull()
}
}
if var container = try? container.nestedUnkeyedContainer(forKey: key) {
return try self.decodeArray(from: &container)
}
if var container = try? container.nestedContainer(keyedBy: JSONCodingKey.self, forKey: key) {
return try self.decodeDictionary(from: &container)
}
throw self.decodingError(forCodingPath: container.codingPath)
}
static func decodeArray(from container: inout UnkeyedDecodingContainer) throws -> [Any] {
var arr: [Any] = []
while !container.isAtEnd {
let value = try decode(from: &container)
arr.append(value)
}
return arr
}
static func decodeDictionary(from container: inout KeyedDecodingContainer<JSONCodingKey>) throws -> [String: Any] {
var dict = [String: Any]()
for key in container.allKeys {
let value = try decode(from: &container, forKey: key)
dict[key.stringValue] = value
}
return dict
}
static func encode(to container: inout UnkeyedEncodingContainer, array: [Any]) throws {
for value in array {
if let value = value as? Bool {
try container.encode(value)
} else if let value = value as? Int64 {
try container.encode(value)
} else if let value = value as? Double {
try container.encode(value)
} else if let value = value as? String {
try container.encode(value)
} else if value is JSONNull {
try container.encodeNil()
} else if let value = value as? [Any] {
var container = container.nestedUnkeyedContainer()
try self.encode(to: &container, array: value)
} else if let value = value as? [String: Any] {
var container = container.nestedContainer(keyedBy: JSONCodingKey.self)
try self.encode(to: &container, dictionary: value)
} else {
throw self.encodingError(forValue: value, codingPath: container.codingPath)
}
}
}
static func encode(to container: inout KeyedEncodingContainer<JSONCodingKey>, dictionary: [String: Any]) throws {
for (key, value) in dictionary {
let key = JSONCodingKey(stringValue: key)!
if let value = value as? Bool {
try container.encode(value, forKey: key)
} else if let value = value as? Int64 {
try container.encode(value, forKey: key)
} else if let value = value as? Double {
try container.encode(value, forKey: key)
} else if let value = value as? String {
try container.encode(value, forKey: key)
} else if value is JSONNull {
try container.encodeNil(forKey: key)
} else if let value = value as? [Any] {
var container = container.nestedUnkeyedContainer(forKey: key)
try self.encode(to: &container, array: value)
} else if let value = value as? [String: Any] {
var container = container.nestedContainer(keyedBy: JSONCodingKey.self, forKey: key)
try self.encode(to: &container, dictionary: value)
} else {
throw self.encodingError(forValue: value, codingPath: container.codingPath)
}
}
}
static func encode(to container: inout SingleValueEncodingContainer, value: Any) throws {
if let value = value as? Bool {
try container.encode(value)
} else if let value = value as? Int64 {
try container.encode(value)
} else if let value = value as? Double {
try container.encode(value)
} else if let value = value as? String {
try container.encode(value)
} else if value is JSONNull {
try container.encodeNil()
} else {
throw self.encodingError(forValue: value, codingPath: container.codingPath)
}
}
required init(from decoder: Decoder) throws {
if var arrayContainer = try? decoder.unkeyedContainer() {
self.value = try JSONAny.decodeArray(from: &arrayContainer)
} else if var container = try? decoder.container(keyedBy: JSONCodingKey.self) {
self.value = try JSONAny.decodeDictionary(from: &container)
} else {
let container = try decoder.singleValueContainer()
self.value = try JSONAny.decode(from: container)
}
}
func encode(to encoder: Encoder) throws {
if let arr = self.value as? [Any] {
var container = encoder.unkeyedContainer()
try JSONAny.encode(to: &container, array: arr)
} else if let dict = self.value as? [String: Any] {
var container = encoder.container(keyedBy: JSONCodingKey.self)
try JSONAny.encode(to: &container, dictionary: dict)
} else {
var container = encoder.singleValueContainer()
try JSONAny.encode(to: &container, value: self.value)
}
}
}

View File

@@ -20,7 +20,8 @@
"test": "vitest",
"test:coverage": "vitest run --coverage",
"protocol:gen": "tsx scripts/protocol-gen.ts",
"protocol:check": "pnpm protocol:gen && git diff --exit-code -- dist/protocol.schema.json apps/macos/Sources/ClawdisProtocol/Protocol.swift",
"protocol:gen:swift": "tsx scripts/protocol-gen-swift.ts",
"protocol:check": "pnpm protocol:gen && pnpm protocol:gen:swift && git diff --exit-code -- dist/protocol.schema.json apps/macos/Sources/ClawdisProtocol/GatewayModels.swift",
"webchat:bundle": "rolldown -c apps/macos/Sources/Clawdis/Resources/WebChat/rolldown.config.mjs"
},
"keywords": [],

View File

@@ -0,0 +1,200 @@
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);
});