Mac: build GatewayProtocol target and typed presence handling
This commit is contained in:
@@ -20,6 +20,13 @@ let package = Package(
|
|||||||
.package(url: "https://github.com/sparkle-project/Sparkle", from: "2.8.1"),
|
.package(url: "https://github.com/sparkle-project/Sparkle", from: "2.8.1"),
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
|
.target(
|
||||||
|
name: "ClawdisProtocol",
|
||||||
|
dependencies: [],
|
||||||
|
path: "Sources/ClawdisProtocol",
|
||||||
|
swiftSettings: [
|
||||||
|
.enableUpcomingFeature("StrictConcurrency"),
|
||||||
|
]),
|
||||||
.target(
|
.target(
|
||||||
name: "ClawdisIPC",
|
name: "ClawdisIPC",
|
||||||
dependencies: [],
|
dependencies: [],
|
||||||
@@ -30,6 +37,7 @@ let package = Package(
|
|||||||
name: "Clawdis",
|
name: "Clawdis",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
"ClawdisIPC",
|
"ClawdisIPC",
|
||||||
|
"ClawdisProtocol",
|
||||||
.product(name: "AsyncXPCConnection", package: "AsyncXPCConnection"),
|
.product(name: "AsyncXPCConnection", package: "AsyncXPCConnection"),
|
||||||
.product(name: "MenuBarExtraAccess", package: "MenuBarExtraAccess"),
|
.product(name: "MenuBarExtraAccess", package: "MenuBarExtraAccess"),
|
||||||
.product(name: "Subprocess", package: "swift-subprocess"),
|
.product(name: "Subprocess", package: "swift-subprocess"),
|
||||||
@@ -46,6 +54,7 @@ let package = Package(
|
|||||||
name: "ClawdisCLI",
|
name: "ClawdisCLI",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
"ClawdisIPC",
|
"ClawdisIPC",
|
||||||
|
"ClawdisProtocol",
|
||||||
.product(name: "AsyncXPCConnection", package: "AsyncXPCConnection"),
|
.product(name: "AsyncXPCConnection", package: "AsyncXPCConnection"),
|
||||||
],
|
],
|
||||||
swiftSettings: [
|
swiftSettings: [
|
||||||
|
|||||||
@@ -120,7 +120,8 @@ private actor GatewayChannelActor {
|
|||||||
}
|
}
|
||||||
switch frame {
|
switch frame {
|
||||||
case let .res(res):
|
case let .res(res):
|
||||||
if let id = res.id, let waiter = pending.removeValue(forKey: id) {
|
let id = res.id
|
||||||
|
if let waiter = pending.removeValue(forKey: id) {
|
||||||
waiter.resume(returning: .res(res))
|
waiter.resume(returning: .res(res))
|
||||||
}
|
}
|
||||||
case let .event(evt):
|
case let .event(evt):
|
||||||
@@ -182,12 +183,14 @@ private actor GatewayChannelActor {
|
|||||||
throw NSError(domain: "Gateway", code: 2, userInfo: [NSLocalizedDescriptionKey: "unexpected frame"])
|
throw NSError(domain: "Gateway", code: 2, userInfo: [NSLocalizedDescriptionKey: "unexpected frame"])
|
||||||
}
|
}
|
||||||
if res.ok == false {
|
if res.ok == false {
|
||||||
let msg = (res.error?.message) ?? "gateway error"
|
let msg = (res.error?["message"]?.value as? String) ?? "gateway error"
|
||||||
throw NSError(domain: "Gateway", code: 3, userInfo: [NSLocalizedDescriptionKey: msg])
|
throw NSError(domain: "Gateway", code: 3, userInfo: [NSLocalizedDescriptionKey: msg])
|
||||||
}
|
}
|
||||||
if let payload = res.payload?.value {
|
if let payload = res.payload?.value {
|
||||||
let payloadData = try JSONSerialization.data(withJSONObject: payload)
|
if JSONSerialization.isValidJSONObject(payload) {
|
||||||
return payloadData
|
let payloadData = try JSONSerialization.data(withJSONObject: payload)
|
||||||
|
return payloadData
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return Data()
|
return Data()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,8 +97,8 @@ final class InstancesStore: ObservableObject {
|
|||||||
let frame = note.object as? GatewayFrame else { return }
|
let frame = note.object as? GatewayFrame else { return }
|
||||||
switch frame {
|
switch frame {
|
||||||
case let .helloOk(hello):
|
case let .helloOk(hello):
|
||||||
let presence = hello.snapshot.presence
|
if JSONSerialization.isValidJSONObject(hello.snapshot.presence),
|
||||||
if let data = try? JSONEncoder().encode(presence) {
|
let data = try? JSONEncoder().encode(hello.snapshot.presence) {
|
||||||
Task { @MainActor [weak self] in self?.decodeAndApplyPresenceData(data) }
|
Task { @MainActor [weak self] in self?.decodeAndApplyPresenceData(data) }
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import Foundation
|
|||||||
|
|
||||||
/// Lightweight `Codable` wrapper that round-trips heterogeneous JSON payloads.
|
/// Lightweight `Codable` wrapper that round-trips heterogeneous JSON payloads.
|
||||||
/// Marked `@unchecked Sendable` because it can hold reference types.
|
/// Marked `@unchecked Sendable` because it can hold reference types.
|
||||||
struct AnyCodable: Codable, @unchecked Sendable {
|
public struct AnyCodable: Codable, @unchecked Sendable {
|
||||||
let value: Any
|
public let value: Any
|
||||||
|
|
||||||
init(_ value: Any) { self.value = value }
|
public init(_ value: Any) { self.value = value }
|
||||||
|
|
||||||
init(from decoder: Decoder) throws {
|
public init(from decoder: Decoder) throws {
|
||||||
let container = try decoder.singleValueContainer()
|
let container = try decoder.singleValueContainer()
|
||||||
if let intVal = try? container.decode(Int.self) { self.value = intVal; return }
|
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 doubleVal = try? container.decode(Double.self) { self.value = doubleVal; return }
|
||||||
@@ -21,7 +21,7 @@ struct AnyCodable: Codable, @unchecked Sendable {
|
|||||||
debugDescription: "Unsupported type")
|
debugDescription: "Unsupported type")
|
||||||
}
|
}
|
||||||
|
|
||||||
func encode(to encoder: Encoder) throws {
|
public func encode(to encoder: Encoder) throws {
|
||||||
var container = encoder.singleValueContainer()
|
var container = encoder.singleValueContainer()
|
||||||
switch self.value {
|
switch self.value {
|
||||||
case let intVal as Int: try container.encode(intVal)
|
case let intVal as Int: try container.encode(intVal)
|
||||||
|
|||||||
@@ -12,76 +12,100 @@ public enum ErrorCode: String, Codable {
|
|||||||
|
|
||||||
public struct Hello: Codable {
|
public struct Hello: Codable {
|
||||||
public let type: String
|
public let type: String
|
||||||
public let minProtocol: Int
|
public let minprotocol: Int
|
||||||
public let maxProtocol: Int
|
public let maxprotocol: Int
|
||||||
public let client: [String: AnyCodable]
|
public let client: [String: AnyCodable]
|
||||||
public let caps: [String]?
|
public let caps: [String]?
|
||||||
public let auth: [String: AnyCodable]?
|
public let auth: [String: AnyCodable]?
|
||||||
public let locale: String?
|
public let locale: String?
|
||||||
public let userAgent: String?
|
public let useragent: String?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
type: String,
|
type: String,
|
||||||
minProtocol: Int,
|
minprotocol: Int,
|
||||||
maxProtocol: Int,
|
maxprotocol: Int,
|
||||||
client: [String: AnyCodable],
|
client: [String: AnyCodable],
|
||||||
caps: [String]?,
|
caps: [String]?,
|
||||||
auth: [String: AnyCodable]?,
|
auth: [String: AnyCodable]?,
|
||||||
locale: String?,
|
locale: String?,
|
||||||
userAgent: String?
|
useragent: String?
|
||||||
) {
|
) {
|
||||||
self.type = type
|
self.type = type
|
||||||
self.minProtocol = minProtocol
|
self.minprotocol = minprotocol
|
||||||
self.maxProtocol = maxProtocol
|
self.maxprotocol = maxprotocol
|
||||||
self.client = client
|
self.client = client
|
||||||
self.caps = caps
|
self.caps = caps
|
||||||
self.auth = auth
|
self.auth = auth
|
||||||
self.locale = locale
|
self.locale = locale
|
||||||
self.userAgent = userAgent
|
self.useragent = useragent
|
||||||
|
}
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case type
|
||||||
|
case minprotocol = "minProtocol"
|
||||||
|
case maxprotocol = "maxProtocol"
|
||||||
|
case client
|
||||||
|
case caps
|
||||||
|
case auth
|
||||||
|
case locale
|
||||||
|
case useragent = "userAgent"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct HelloOk: Codable {
|
public struct HelloOk: Codable {
|
||||||
public let type: String
|
public let type: String
|
||||||
public let protocol: Int
|
public let _protocol: Int
|
||||||
public let server: [String: AnyCodable]
|
public let server: [String: AnyCodable]
|
||||||
public let features: [String: AnyCodable]
|
public let features: [String: AnyCodable]
|
||||||
public let snapshot: [String: AnyCodable]
|
public let snapshot: Snapshot
|
||||||
public let policy: [String: AnyCodable]
|
public let policy: [String: AnyCodable]
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
type: String,
|
type: String,
|
||||||
protocol: Int,
|
_protocol: Int,
|
||||||
server: [String: AnyCodable],
|
server: [String: AnyCodable],
|
||||||
features: [String: AnyCodable],
|
features: [String: AnyCodable],
|
||||||
snapshot: [String: AnyCodable],
|
snapshot: Snapshot,
|
||||||
policy: [String: AnyCodable]
|
policy: [String: AnyCodable]
|
||||||
) {
|
) {
|
||||||
self.type = type
|
self.type = type
|
||||||
self.protocol = protocol
|
self._protocol = _protocol
|
||||||
self.server = server
|
self.server = server
|
||||||
self.features = features
|
self.features = features
|
||||||
self.snapshot = snapshot
|
self.snapshot = snapshot
|
||||||
self.policy = policy
|
self.policy = policy
|
||||||
}
|
}
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case type
|
||||||
|
case _protocol = "protocol"
|
||||||
|
case server
|
||||||
|
case features
|
||||||
|
case snapshot
|
||||||
|
case policy
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct HelloError: Codable {
|
public struct HelloError: Codable {
|
||||||
public let type: String
|
public let type: String
|
||||||
public let reason: String
|
public let reason: String
|
||||||
public let expectedProtocol: Int?
|
public let expectedprotocol: Int?
|
||||||
public let minClient: String?
|
public let minclient: String?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
type: String,
|
type: String,
|
||||||
reason: String,
|
reason: String,
|
||||||
expectedProtocol: Int?,
|
expectedprotocol: Int?,
|
||||||
minClient: String?
|
minclient: String?
|
||||||
) {
|
) {
|
||||||
self.type = type
|
self.type = type
|
||||||
self.reason = reason
|
self.reason = reason
|
||||||
self.expectedProtocol = expectedProtocol
|
self.expectedprotocol = expectedprotocol
|
||||||
self.minClient = minClient
|
self.minclient = minclient
|
||||||
|
}
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case type
|
||||||
|
case reason
|
||||||
|
case expectedprotocol = "expectedProtocol"
|
||||||
|
case minclient = "minClient"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,6 +126,12 @@ public struct RequestFrame: Codable {
|
|||||||
self.method = method
|
self.method = method
|
||||||
self.params = params
|
self.params = params
|
||||||
}
|
}
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case type
|
||||||
|
case id
|
||||||
|
case method
|
||||||
|
case params
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct ResponseFrame: Codable {
|
public struct ResponseFrame: Codable {
|
||||||
@@ -124,6 +154,13 @@ public struct ResponseFrame: Codable {
|
|||||||
self.payload = payload
|
self.payload = payload
|
||||||
self.error = error
|
self.error = error
|
||||||
}
|
}
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case type
|
||||||
|
case id
|
||||||
|
case ok
|
||||||
|
case payload
|
||||||
|
case error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct EventFrame: Codable {
|
public struct EventFrame: Codable {
|
||||||
@@ -131,20 +168,27 @@ public struct EventFrame: Codable {
|
|||||||
public let event: String
|
public let event: String
|
||||||
public let payload: AnyCodable?
|
public let payload: AnyCodable?
|
||||||
public let seq: Int?
|
public let seq: Int?
|
||||||
public let stateVersion: [String: AnyCodable]?
|
public let stateversion: [String: AnyCodable]?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
type: String,
|
type: String,
|
||||||
event: String,
|
event: String,
|
||||||
payload: AnyCodable?,
|
payload: AnyCodable?,
|
||||||
seq: Int?,
|
seq: Int?,
|
||||||
stateVersion: [String: AnyCodable]?
|
stateversion: [String: AnyCodable]?
|
||||||
) {
|
) {
|
||||||
self.type = type
|
self.type = type
|
||||||
self.event = event
|
self.event = event
|
||||||
self.payload = payload
|
self.payload = payload
|
||||||
self.seq = seq
|
self.seq = seq
|
||||||
self.stateVersion = stateVersion
|
self.stateversion = stateversion
|
||||||
|
}
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case type
|
||||||
|
case event
|
||||||
|
case payload
|
||||||
|
case seq
|
||||||
|
case stateversion = "stateVersion"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,35 +197,47 @@ public struct PresenceEntry: Codable {
|
|||||||
public let ip: String?
|
public let ip: String?
|
||||||
public let version: String?
|
public let version: String?
|
||||||
public let mode: String?
|
public let mode: String?
|
||||||
public let lastInputSeconds: Int?
|
public let lastinputseconds: Int?
|
||||||
public let reason: String?
|
public let reason: String?
|
||||||
public let tags: [String]?
|
public let tags: [String]?
|
||||||
public let text: String?
|
public let text: String?
|
||||||
public let ts: Int
|
public let ts: Int
|
||||||
public let instanceId: String?
|
public let instanceid: String?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
host: String?,
|
host: String?,
|
||||||
ip: String?,
|
ip: String?,
|
||||||
version: String?,
|
version: String?,
|
||||||
mode: String?,
|
mode: String?,
|
||||||
lastInputSeconds: Int?,
|
lastinputseconds: Int?,
|
||||||
reason: String?,
|
reason: String?,
|
||||||
tags: [String]?,
|
tags: [String]?,
|
||||||
text: String?,
|
text: String?,
|
||||||
ts: Int,
|
ts: Int,
|
||||||
instanceId: String?
|
instanceid: String?
|
||||||
) {
|
) {
|
||||||
self.host = host
|
self.host = host
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
self.version = version
|
self.version = version
|
||||||
self.mode = mode
|
self.mode = mode
|
||||||
self.lastInputSeconds = lastInputSeconds
|
self.lastinputseconds = lastinputseconds
|
||||||
self.reason = reason
|
self.reason = reason
|
||||||
self.tags = tags
|
self.tags = tags
|
||||||
self.text = text
|
self.text = text
|
||||||
self.ts = ts
|
self.ts = ts
|
||||||
self.instanceId = instanceId
|
self.instanceid = instanceid
|
||||||
|
}
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case host
|
||||||
|
case ip
|
||||||
|
case version
|
||||||
|
case mode
|
||||||
|
case lastinputseconds = "lastInputSeconds"
|
||||||
|
case reason
|
||||||
|
case tags
|
||||||
|
case text
|
||||||
|
case ts
|
||||||
|
case instanceid = "instanceId"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,24 +252,34 @@ public struct StateVersion: Codable {
|
|||||||
self.presence = presence
|
self.presence = presence
|
||||||
self.health = health
|
self.health = health
|
||||||
}
|
}
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case presence
|
||||||
|
case health
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct Snapshot: Codable {
|
public struct Snapshot: Codable {
|
||||||
public let presence: [[String: AnyCodable]]
|
public let presence: [PresenceEntry]
|
||||||
public let health: AnyCodable
|
public let health: AnyCodable
|
||||||
public let stateVersion: [String: AnyCodable]
|
public let stateversion: StateVersion
|
||||||
public let uptimeMs: Int
|
public let uptimems: Int
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
presence: [[String: AnyCodable]],
|
presence: [PresenceEntry],
|
||||||
health: AnyCodable,
|
health: AnyCodable,
|
||||||
stateVersion: [String: AnyCodable],
|
stateversion: StateVersion,
|
||||||
uptimeMs: Int
|
uptimems: Int
|
||||||
) {
|
) {
|
||||||
self.presence = presence
|
self.presence = presence
|
||||||
self.health = health
|
self.health = health
|
||||||
self.stateVersion = stateVersion
|
self.stateversion = stateversion
|
||||||
self.uptimeMs = uptimeMs
|
self.uptimems = uptimems
|
||||||
|
}
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case presence
|
||||||
|
case health
|
||||||
|
case stateversion = "stateVersion"
|
||||||
|
case uptimems = "uptimeMs"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,92 +288,122 @@ public struct ErrorShape: Codable {
|
|||||||
public let message: String
|
public let message: String
|
||||||
public let details: AnyCodable?
|
public let details: AnyCodable?
|
||||||
public let retryable: Bool?
|
public let retryable: Bool?
|
||||||
public let retryAfterMs: Int?
|
public let retryafterms: Int?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
code: String,
|
code: String,
|
||||||
message: String,
|
message: String,
|
||||||
details: AnyCodable?,
|
details: AnyCodable?,
|
||||||
retryable: Bool?,
|
retryable: Bool?,
|
||||||
retryAfterMs: Int?
|
retryafterms: Int?
|
||||||
) {
|
) {
|
||||||
self.code = code
|
self.code = code
|
||||||
self.message = message
|
self.message = message
|
||||||
self.details = details
|
self.details = details
|
||||||
self.retryable = retryable
|
self.retryable = retryable
|
||||||
self.retryAfterMs = retryAfterMs
|
self.retryafterms = retryafterms
|
||||||
|
}
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case code
|
||||||
|
case message
|
||||||
|
case details
|
||||||
|
case retryable
|
||||||
|
case retryafterms = "retryAfterMs"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct AgentEvent: Codable {
|
public struct AgentEvent: Codable {
|
||||||
public let runId: String
|
public let runid: String
|
||||||
public let seq: Int
|
public let seq: Int
|
||||||
public let stream: String
|
public let stream: String
|
||||||
public let ts: Int
|
public let ts: Int
|
||||||
public let data: [String: AnyCodable]
|
public let data: [String: AnyCodable]
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
runId: String,
|
runid: String,
|
||||||
seq: Int,
|
seq: Int,
|
||||||
stream: String,
|
stream: String,
|
||||||
ts: Int,
|
ts: Int,
|
||||||
data: [String: AnyCodable]
|
data: [String: AnyCodable]
|
||||||
) {
|
) {
|
||||||
self.runId = runId
|
self.runid = runid
|
||||||
self.seq = seq
|
self.seq = seq
|
||||||
self.stream = stream
|
self.stream = stream
|
||||||
self.ts = ts
|
self.ts = ts
|
||||||
self.data = data
|
self.data = data
|
||||||
}
|
}
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case runid = "runId"
|
||||||
|
case seq
|
||||||
|
case stream
|
||||||
|
case ts
|
||||||
|
case data
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct SendParams: Codable {
|
public struct SendParams: Codable {
|
||||||
public let to: String
|
public let to: String
|
||||||
public let message: String
|
public let message: String
|
||||||
public let mediaUrl: String?
|
public let mediaurl: String?
|
||||||
public let provider: String?
|
public let provider: String?
|
||||||
public let idempotencyKey: String
|
public let idempotencykey: String
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
to: String,
|
to: String,
|
||||||
message: String,
|
message: String,
|
||||||
mediaUrl: String?,
|
mediaurl: String?,
|
||||||
provider: String?,
|
provider: String?,
|
||||||
idempotencyKey: String
|
idempotencykey: String
|
||||||
) {
|
) {
|
||||||
self.to = to
|
self.to = to
|
||||||
self.message = message
|
self.message = message
|
||||||
self.mediaUrl = mediaUrl
|
self.mediaurl = mediaurl
|
||||||
self.provider = provider
|
self.provider = provider
|
||||||
self.idempotencyKey = idempotencyKey
|
self.idempotencykey = idempotencykey
|
||||||
|
}
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case to
|
||||||
|
case message
|
||||||
|
case mediaurl = "mediaUrl"
|
||||||
|
case provider
|
||||||
|
case idempotencykey = "idempotencyKey"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct AgentParams: Codable {
|
public struct AgentParams: Codable {
|
||||||
public let message: String
|
public let message: String
|
||||||
public let to: String?
|
public let to: String?
|
||||||
public let sessionId: String?
|
public let sessionid: String?
|
||||||
public let thinking: String?
|
public let thinking: String?
|
||||||
public let deliver: Bool?
|
public let deliver: Bool?
|
||||||
public let timeout: Int?
|
public let timeout: Int?
|
||||||
public let idempotencyKey: String
|
public let idempotencykey: String
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
message: String,
|
message: String,
|
||||||
to: String?,
|
to: String?,
|
||||||
sessionId: String?,
|
sessionid: String?,
|
||||||
thinking: String?,
|
thinking: String?,
|
||||||
deliver: Bool?,
|
deliver: Bool?,
|
||||||
timeout: Int?,
|
timeout: Int?,
|
||||||
idempotencyKey: String
|
idempotencykey: String
|
||||||
) {
|
) {
|
||||||
self.message = message
|
self.message = message
|
||||||
self.to = to
|
self.to = to
|
||||||
self.sessionId = sessionId
|
self.sessionid = sessionid
|
||||||
self.thinking = thinking
|
self.thinking = thinking
|
||||||
self.deliver = deliver
|
self.deliver = deliver
|
||||||
self.timeout = timeout
|
self.timeout = timeout
|
||||||
self.idempotencyKey = idempotencyKey
|
self.idempotencykey = idempotencykey
|
||||||
|
}
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case message
|
||||||
|
case to
|
||||||
|
case sessionid = "sessionId"
|
||||||
|
case thinking
|
||||||
|
case deliver
|
||||||
|
case timeout
|
||||||
|
case idempotencykey = "idempotencyKey"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,25 +415,32 @@ public struct TickEvent: Codable {
|
|||||||
) {
|
) {
|
||||||
self.ts = ts
|
self.ts = ts
|
||||||
}
|
}
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case ts
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct ShutdownEvent: Codable {
|
public struct ShutdownEvent: Codable {
|
||||||
public let reason: String
|
public let reason: String
|
||||||
public let restartExpectedMs: Int?
|
public let restartexpectedms: Int?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
reason: String,
|
reason: String,
|
||||||
restartExpectedMs: Int?
|
restartexpectedms: Int?
|
||||||
) {
|
) {
|
||||||
self.reason = reason
|
self.reason = reason
|
||||||
self.restartExpectedMs = restartExpectedMs
|
self.restartexpectedms = restartexpectedms
|
||||||
|
}
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case reason
|
||||||
|
case restartexpectedms = "restartExpectedMs"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum GatewayFrame: Codable {
|
public enum GatewayFrame: Codable {
|
||||||
case hello(Hello)
|
case hello(Hello)
|
||||||
case hello-ok(HelloOk)
|
case helloOk(HelloOk)
|
||||||
case hello-error(HelloError)
|
case helloError(HelloError)
|
||||||
case req(RequestFrame)
|
case req(RequestFrame)
|
||||||
case res(ResponseFrame)
|
case res(ResponseFrame)
|
||||||
case event(EventFrame)
|
case event(EventFrame)
|
||||||
@@ -351,17 +454,17 @@ public enum GatewayFrame: Codable {
|
|||||||
}
|
}
|
||||||
switch type {
|
switch type {
|
||||||
case "hello":
|
case "hello":
|
||||||
self = .hello(try decodePayload(Hello.self, from: raw))
|
self = .hello(try Self.decodePayload(Hello.self, from: raw))
|
||||||
case "hello-ok":
|
case "hello-ok":
|
||||||
self = .helloOk(try decodePayload(HelloOk.self, from: raw))
|
self = .helloOk(try Self.decodePayload(HelloOk.self, from: raw))
|
||||||
case "hello-error":
|
case "hello-error":
|
||||||
self = .helloError(try decodePayload(HelloError.self, from: raw))
|
self = .helloError(try Self.decodePayload(HelloError.self, from: raw))
|
||||||
case "req":
|
case "req":
|
||||||
self = .req(try decodePayload(RequestFrame.self, from: raw))
|
self = .req(try Self.decodePayload(RequestFrame.self, from: raw))
|
||||||
case "res":
|
case "res":
|
||||||
self = .res(try decodePayload(ResponseFrame.self, from: raw))
|
self = .res(try Self.decodePayload(ResponseFrame.self, from: raw))
|
||||||
case "event":
|
case "event":
|
||||||
self = .event(try decodePayload(EventFrame.self, from: raw))
|
self = .event(try Self.decodePayload(EventFrame.self, from: raw))
|
||||||
default:
|
default:
|
||||||
self = .unknown(type: type, raw: raw)
|
self = .unknown(type: type, raw: raw)
|
||||||
}
|
}
|
||||||
@@ -382,7 +485,7 @@ public enum GatewayFrame: Codable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private func decodePayload<T: Decodable>(_ type: T.Type, from raw: [String: AnyCodable]) throws -> T {
|
private static func decodePayload<T: Decodable>(_ type: T.Type, from raw: [String: AnyCodable]) throws -> T {
|
||||||
let data = try JSONSerialization.data(withJSONObject: raw)
|
let data = try JSONSerialization.data(withJSONObject: raw)
|
||||||
let decoder = JSONDecoder()
|
let decoder = JSONDecoder()
|
||||||
return try decoder.decode(T.self, from: data)
|
return try decoder.decode(T.self, from: data)
|
||||||
|
|||||||
@@ -31,27 +31,67 @@ const header = `// Generated by scripts/protocol-gen-swift.ts — do not edit by
|
|||||||
.map((c) => ` case ${camelCase(c)} = "${c}"`)
|
.map((c) => ` case ${camelCase(c)} = "${c}"`)
|
||||||
.join("\n")}\n}\n`;
|
.join("\n")}\n}\n`;
|
||||||
|
|
||||||
|
const reserved = new Set([
|
||||||
|
"associatedtype",
|
||||||
|
"class",
|
||||||
|
"deinit",
|
||||||
|
"enum",
|
||||||
|
"extension",
|
||||||
|
"fileprivate",
|
||||||
|
"func",
|
||||||
|
"import",
|
||||||
|
"init",
|
||||||
|
"inout",
|
||||||
|
"internal",
|
||||||
|
"let",
|
||||||
|
"open",
|
||||||
|
"operator",
|
||||||
|
"private",
|
||||||
|
"precedencegroup",
|
||||||
|
"protocol",
|
||||||
|
"public",
|
||||||
|
"rethrows",
|
||||||
|
"static",
|
||||||
|
"struct",
|
||||||
|
"subscript",
|
||||||
|
"typealias",
|
||||||
|
"var",
|
||||||
|
]);
|
||||||
|
|
||||||
function camelCase(input: string) {
|
function camelCase(input: string) {
|
||||||
return input
|
return input
|
||||||
|
.replace(/[^a-zA-Z0-9]+/g, " ")
|
||||||
|
.trim()
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.split("_")
|
.split(/\s+/)
|
||||||
.map((p, i) => (i === 0 ? p : p[0].toUpperCase() + p.slice(1)))
|
.map((p, i) => (i === 0 ? p : p[0].toUpperCase() + p.slice(1)))
|
||||||
.join("");
|
.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function safeName(name: string) {
|
||||||
|
const cc = camelCase(name.replace(/-/g, "_"));
|
||||||
|
if (reserved.has(cc)) return `_${cc}`;
|
||||||
|
return cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// filled later once schemas are loaded
|
||||||
|
const schemaNameByObject = new Map<object, string>();
|
||||||
|
|
||||||
function swiftType(schema: JsonSchema, required: boolean): string {
|
function swiftType(schema: JsonSchema, required: boolean): string {
|
||||||
const t = schema.type;
|
const t = schema.type;
|
||||||
const isOptional = !required;
|
const isOptional = !required;
|
||||||
let base: string;
|
let base: string;
|
||||||
if (t === "string") base = "String";
|
const named = schemaNameByObject.get(schema as object);
|
||||||
|
if (named) {
|
||||||
|
base = named;
|
||||||
|
} else if (t === "string") base = "String";
|
||||||
else if (t === "integer") base = "Int";
|
else if (t === "integer") base = "Int";
|
||||||
else if (t === "number") base = "Double";
|
else if (t === "number") base = "Double";
|
||||||
else if (t === "boolean") base = "Bool";
|
else if (t === "boolean") base = "Bool";
|
||||||
else if (t === "array") {
|
else if (t === "array") {
|
||||||
base = `[${swiftType(schema.items ?? { type: "Any" }, true)}]`;
|
base = `[${swiftType(schema.items ?? { type: "Any" }, true)}]`;
|
||||||
} else if (schema.enum) {
|
} else if (schema.enum) {
|
||||||
base = schema.enum.map((v) => `\"${v}\"`).join(" | ");
|
base = "String";
|
||||||
base = "String"; // simplify enums to String; custom enums could be added if needed
|
|
||||||
} else if (schema.patternProperties) {
|
} else if (schema.patternProperties) {
|
||||||
base = "[String: AnyCodable]";
|
base = "[String: AnyCodable]";
|
||||||
} else if (t === "object") {
|
} else if (t === "object") {
|
||||||
@@ -71,15 +111,21 @@ function emitStruct(name: string, schema: JsonSchema): string {
|
|||||||
lines.push("}\n");
|
lines.push("}\n");
|
||||||
return lines.join("\n");
|
return lines.join("\n");
|
||||||
}
|
}
|
||||||
|
const codingKeys: string[] = [];
|
||||||
for (const [key, propSchema] of Object.entries(props)) {
|
for (const [key, propSchema] of Object.entries(props)) {
|
||||||
const propName = key === "description" ? "desc" : key;
|
const propName = safeName(key);
|
||||||
const propType = swiftType(propSchema, required.has(key));
|
const propType = swiftType(propSchema, required.has(key));
|
||||||
lines.push(` public let ${propName}: ${propType}`);
|
lines.push(` public let ${propName}: ${propType}`);
|
||||||
|
if (propName !== key) {
|
||||||
|
codingKeys.push(` case ${propName} = "${key}"`);
|
||||||
|
} else {
|
||||||
|
codingKeys.push(` case ${propName}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
lines.push("\n public init(\n" +
|
lines.push("\n public init(\n" +
|
||||||
Object.entries(props)
|
Object.entries(props)
|
||||||
.map(([key, prop]) => {
|
.map(([key, prop]) => {
|
||||||
const propName = key === "description" ? "desc" : key;
|
const propName = safeName(key);
|
||||||
const req = required.has(key);
|
const req = required.has(key);
|
||||||
return ` ${propName}: ${swiftType(prop, true)}${req ? "" : "?"}`;
|
return ` ${propName}: ${swiftType(prop, true)}${req ? "" : "?"}`;
|
||||||
})
|
})
|
||||||
@@ -87,24 +133,20 @@ function emitStruct(name: string, schema: JsonSchema): string {
|
|||||||
"\n ) {\n" +
|
"\n ) {\n" +
|
||||||
Object.entries(props)
|
Object.entries(props)
|
||||||
.map(([key]) => {
|
.map(([key]) => {
|
||||||
const propName = key === "description" ? "desc" : key;
|
const propName = safeName(key);
|
||||||
return ` self.${propName} = ${propName}`;
|
return ` self.${propName} = ${propName}`;
|
||||||
})
|
})
|
||||||
.join("\n") +
|
.join("\n") +
|
||||||
|
"\n }\n" +
|
||||||
|
" private enum CodingKeys: String, CodingKey {\n" +
|
||||||
|
codingKeys.join("\n") +
|
||||||
"\n }\n}");
|
"\n }\n}");
|
||||||
lines.push("");
|
lines.push("");
|
||||||
return lines.join("\n");
|
return lines.join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
function emitGatewayFrame(): string {
|
function emitGatewayFrame(): string {
|
||||||
const cases = [
|
const cases = ["hello", "hello-ok", "hello-error", "req", "res", "event"];
|
||||||
"hello",
|
|
||||||
"hello-ok",
|
|
||||||
"hello-error",
|
|
||||||
"req",
|
|
||||||
"res",
|
|
||||||
"event",
|
|
||||||
];
|
|
||||||
const associated: Record<string, string> = {
|
const associated: Record<string, string> = {
|
||||||
hello: "Hello",
|
hello: "Hello",
|
||||||
"hello-ok": "HelloOk",
|
"hello-ok": "HelloOk",
|
||||||
@@ -113,7 +155,7 @@ function emitGatewayFrame(): string {
|
|||||||
res: "ResponseFrame",
|
res: "ResponseFrame",
|
||||||
event: "EventFrame",
|
event: "EventFrame",
|
||||||
};
|
};
|
||||||
const caseLines = cases.map((c) => ` case ${camelCase(c)}(${associated[c]})`);
|
const caseLines = cases.map((c) => ` case ${safeName(c)}(${associated[c]})`);
|
||||||
const initLines = `
|
const initLines = `
|
||||||
public init(from decoder: Decoder) throws {
|
public init(from decoder: Decoder) throws {
|
||||||
let container = try decoder.singleValueContainer()
|
let container = try decoder.singleValueContainer()
|
||||||
@@ -123,17 +165,17 @@ function emitGatewayFrame(): string {
|
|||||||
}
|
}
|
||||||
switch type {
|
switch type {
|
||||||
case "hello":
|
case "hello":
|
||||||
self = .hello(try decodePayload(Hello.self, from: raw))
|
self = .hello(try Self.decodePayload(Hello.self, from: raw))
|
||||||
case "hello-ok":
|
case "hello-ok":
|
||||||
self = .helloOk(try decodePayload(HelloOk.self, from: raw))
|
self = .helloOk(try Self.decodePayload(HelloOk.self, from: raw))
|
||||||
case "hello-error":
|
case "hello-error":
|
||||||
self = .helloError(try decodePayload(HelloError.self, from: raw))
|
self = .helloError(try Self.decodePayload(HelloError.self, from: raw))
|
||||||
case "req":
|
case "req":
|
||||||
self = .req(try decodePayload(RequestFrame.self, from: raw))
|
self = .req(try Self.decodePayload(RequestFrame.self, from: raw))
|
||||||
case "res":
|
case "res":
|
||||||
self = .res(try decodePayload(ResponseFrame.self, from: raw))
|
self = .res(try Self.decodePayload(ResponseFrame.self, from: raw))
|
||||||
case "event":
|
case "event":
|
||||||
self = .event(try decodePayload(EventFrame.self, from: raw))
|
self = .event(try Self.decodePayload(EventFrame.self, from: raw))
|
||||||
default:
|
default:
|
||||||
self = .unknown(type: type, raw: raw)
|
self = .unknown(type: type, raw: raw)
|
||||||
}
|
}
|
||||||
@@ -155,7 +197,7 @@ function emitGatewayFrame(): string {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const helper = `
|
const helper = `
|
||||||
private func decodePayload<T: Decodable>(_ type: T.Type, from raw: [String: AnyCodable]) throws -> T {
|
private static func decodePayload<T: Decodable>(_ type: T.Type, from raw: [String: AnyCodable]) throws -> T {
|
||||||
let data = try JSONSerialization.data(withJSONObject: raw)
|
let data = try JSONSerialization.data(withJSONObject: raw)
|
||||||
let decoder = JSONDecoder()
|
let decoder = JSONDecoder()
|
||||||
return try decoder.decode(T.self, from: data)
|
return try decoder.decode(T.self, from: data)
|
||||||
@@ -178,6 +220,10 @@ async function generate() {
|
|||||||
[string, JsonSchema]
|
[string, JsonSchema]
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
for (const [name, schema] of definitions) {
|
||||||
|
schemaNameByObject.set(schema as object, name);
|
||||||
|
}
|
||||||
|
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
parts.push(header);
|
parts.push(header);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user