Protocol: add TypeBox-driven Swift generator
This commit is contained in:
41
apps/macos/Sources/ClawdisProtocol/AnyCodable.swift
Normal file
41
apps/macos/Sources/ClawdisProtocol/AnyCodable.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
387
apps/macos/Sources/ClawdisProtocol/GatewayModels.swift
Normal file
387
apps/macos/Sources/ClawdisProtocol/GatewayModels.swift
Normal 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -20,7 +20,8 @@
|
|||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"test:coverage": "vitest run --coverage",
|
"test:coverage": "vitest run --coverage",
|
||||||
"protocol:gen": "tsx scripts/protocol-gen.ts",
|
"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"
|
"webchat:bundle": "rolldown -c apps/macos/Sources/Clawdis/Resources/WebChat/rolldown.config.mjs"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
|
|||||||
200
scripts/protocol-gen-swift.ts
Normal file
200
scripts/protocol-gen-swift.ts
Normal 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);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user