2025-12-09 15:21:16 +01:00
import { promises as fs } from "node:fs" ;
import path from "node:path" ;
import { fileURLToPath } from "node:url" ;
2026-01-31 21:21:09 +09:00
import { ErrorCodes , PROTOCOL_VERSION , ProtocolSchemas } from "../src/gateway/protocol/schema.js" ;
2025-12-09 15:21:16 +01:00
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 , ".." ) ;
2026-01-20 11:15:10 +00:00
const outPaths = [
2026-01-31 21:21:09 +09:00
path . join ( repoRoot , "apps" , "macos" , "Sources" , "OpenClawProtocol" , "GatewayModels.swift" ) ,
2026-01-20 11:15:10 +00:00
path . join (
repoRoot ,
"apps" ,
"shared" ,
2026-01-30 03:15:10 +01:00
"OpenClawKit" ,
2026-01-20 11:15:10 +00:00
"Sources" ,
2026-01-30 03:15:10 +01:00
"OpenClawProtocol" ,
2026-01-20 11:15:10 +00:00
"GatewayModels.swift" ,
) ,
] ;
2025-12-09 15:21:16 +01:00
2026-02-11 21:21:21 -08:00
const header = ` // Generated by scripts/protocol-gen-swift.ts — do not edit by hand \ n// swiftlint:disable file_length \ nimport Foundation \ n \ npublic let GATEWAY_PROTOCOL_VERSION = ${ PROTOCOL_VERSION } \ n \ npublic enum ErrorCode: String, Codable, Sendable { \ n ${ Object . values (
2026-01-31 21:21:09 +09:00
ErrorCodes ,
)
2025-12-09 15:21:16 +01:00
. map ( ( c ) = > ` case ${ camelCase ( c ) } = " ${ c } " ` )
. join ( "\n" ) } \ n } \ n ` ;
2025-12-09 15:35:06 +01:00
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" ,
] ) ;
2025-12-09 15:21:16 +01:00
function camelCase ( input : string ) {
return input
2025-12-09 15:35:06 +01:00
. replace ( /[^a-zA-Z0-9]+/g , " " )
. trim ( )
2025-12-09 15:21:16 +01:00
. toLowerCase ( )
2025-12-09 15:35:06 +01:00
. split ( /\s+/ )
2025-12-09 15:21:16 +01:00
. map ( ( p , i ) = > ( i === 0 ? p : p [ 0 ] . toUpperCase ( ) + p . slice ( 1 ) ) )
. join ( "" ) ;
}
2025-12-09 15:35:06 +01:00
function safeName ( name : string ) {
const cc = camelCase ( name . replace ( /-/g , "_" ) ) ;
2026-01-31 21:29:14 +09:00
if ( reserved . has ( cc ) ) {
return ` _ ${ cc } ` ;
}
2025-12-09 15:35:06 +01:00
return cc ;
}
// filled later once schemas are loaded
const schemaNameByObject = new Map < object , string > ( ) ;
2025-12-09 15:21:16 +01:00
function swiftType ( schema : JsonSchema , required : boolean ) : string {
const t = schema . type ;
const isOptional = ! required ;
let base : string ;
2025-12-09 15:35:06 +01:00
const named = schemaNameByObject . get ( schema as object ) ;
if ( named ) {
base = named ;
2026-01-31 21:29:14 +09:00
} else if ( t === "string" ) {
base = "String" ;
} else if ( t === "integer" ) {
base = "Int" ;
} else if ( t === "number" ) {
base = "Double" ;
} else if ( t === "boolean" ) {
base = "Bool" ;
} else if ( t === "array" ) {
2025-12-09 15:21:16 +01:00
base = ` [ ${ swiftType ( schema . items ? ? { type : "Any" } , true)}] ` ;
} else if ( schema . enum ) {
2025-12-09 15:35:06 +01:00
base = "String" ;
2025-12-09 15:21:16 +01:00
} 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 [ ] = [ ] ;
if ( Object . keys ( props ) . length === 0 ) {
2026-02-21 16:53:41 +01:00
return ` public struct ${ name } : Codable, Sendable {} \ n ` ;
2025-12-09 15:21:16 +01:00
}
2026-02-21 16:53:41 +01:00
lines . push ( ` public struct ${ name } : Codable, Sendable { ` ) ;
2025-12-09 15:35:06 +01:00
const codingKeys : string [ ] = [ ] ;
2025-12-09 15:21:16 +01:00
for ( const [ key , propSchema ] of Object . entries ( props ) ) {
2025-12-09 15:35:06 +01:00
const propName = safeName ( key ) ;
2025-12-09 15:21:16 +01:00
const propType = swiftType ( propSchema , required . has ( key ) ) ;
lines . push ( ` public let ${ propName } : ${ propType } ` ) ;
2025-12-09 15:35:06 +01:00
if ( propName !== key ) {
codingKeys . push ( ` case ${ propName } = " ${ key } " ` ) ;
} else {
codingKeys . push ( ` case ${ propName } ` ) ;
}
2025-12-09 15:21:16 +01:00
}
2026-01-31 21:21:09 +09:00
lines . push (
"\n public init(\n" +
Object . entries ( props )
. map ( ( [ key , prop ] ) = > {
const propName = safeName ( key ) ;
const req = required . has ( key ) ;
return ` ${ propName } : ${ swiftType ( prop , true ) } ${ req ? "" : "?" } ` ;
} )
. join ( ",\n" ) +
2026-02-21 16:53:41 +01:00
")\n" +
" {\n" +
2026-01-31 21:21:09 +09:00
Object . entries ( props )
. map ( ( [ key ] ) = > {
const propName = safeName ( key ) ;
return ` self. ${ propName } = ${ propName } ` ;
} )
. join ( "\n" ) +
2026-02-21 16:53:41 +01:00
"\n }\n\n" +
2026-01-31 21:21:09 +09:00
" private enum CodingKeys: String, CodingKey {\n" +
codingKeys . join ( "\n" ) +
"\n }\n}" ,
) ;
2025-12-09 15:21:16 +01:00
lines . push ( "" ) ;
return lines . join ( "\n" ) ;
}
function emitGatewayFrame ( ) : string {
2025-12-12 23:29:57 +00:00
const cases = [ "req" , "res" , "event" ] ;
2025-12-09 15:21:16 +01:00
const associated : Record < string , string > = {
req : "RequestFrame" ,
res : "ResponseFrame" ,
event : "EventFrame" ,
} ;
2025-12-09 15:35:06 +01:00
const caseLines = cases . map ( ( c ) = > ` case ${ safeName ( c ) } ( ${ associated [ c ] } ) ` ) ;
2025-12-09 15:21:16 +01:00
const initLines = `
2025-12-12 17:30:21 +00:00
private enum CodingKeys : String , CodingKey {
case type
}
2025-12-09 15:21:16 +01:00
public init ( from decoder : Decoder ) throws {
2025-12-12 17:30:21 +00:00
let typeContainer = try decoder . container ( keyedBy : CodingKeys.self )
let type = try typeContainer . decode ( String . self , forKey : .type )
2025-12-09 15:21:16 +01:00
switch type {
case "req" :
2026-02-21 16:53:41 +01:00
self = try . req ( RequestFrame ( from : decoder ) )
2025-12-09 15:21:16 +01:00
case "res" :
2026-02-21 16:53:41 +01:00
self = try . res ( ResponseFrame ( from : decoder ) )
2025-12-09 15:21:16 +01:00
case "event" :
2026-02-21 16:53:41 +01:00
self = try . event ( EventFrame ( from : decoder ) )
2025-12-09 15:21:16 +01:00
default :
2025-12-12 17:30:21 +00:00
let container = try decoder . singleValueContainer ( )
let raw = try container . decode ( [ String : AnyCodable ] . self )
2025-12-09 15:26:31 +01:00
self = . unknown ( type : type , raw : raw )
2025-12-09 15:21:16 +01:00
}
}
public func encode ( to encoder : Encoder ) throws {
switch self {
2026-02-21 16:53:41 +01:00
case let . req ( v ) :
try v . encode ( to : encoder )
case let . res ( v ) :
try v . encode ( to : encoder )
case let . event ( v ) :
try v . encode ( to : encoder )
case let . unknown ( _ , raw ) :
2025-12-09 15:26:31 +01:00
var container = encoder . singleValueContainer ( )
try container . encode ( raw )
2025-12-09 15:21:16 +01:00
}
}
` ;
return [
2026-01-04 14:39:21 -08:00
"public enum GatewayFrame: Codable, Sendable {" ,
2025-12-09 15:21:16 +01:00
. . . caseLines ,
2025-12-09 15:26:31 +01:00
" case unknown(type: String, raw: [String: AnyCodable])" ,
2026-02-21 16:53:41 +01:00
initLines . trimEnd ( ) ,
2025-12-09 15:21:16 +01:00
"}" ,
"" ,
] . join ( "\n" ) ;
}
async function generate() {
2026-01-31 21:21:09 +09:00
const definitions = Object . entries ( ProtocolSchemas ) as Array < [ string , JsonSchema ] > ;
2025-12-09 15:21:16 +01:00
2025-12-09 15:35:06 +01:00
for ( const [ name , schema ] of definitions ) {
schemaNameByObject . set ( schema as object , name ) ;
}
2025-12-09 15:21:16 +01:00
const parts : string [ ] = [ ] ;
parts . push ( header ) ;
// Value structs
for ( const [ name , schema ] of definitions ) {
2026-01-31 21:29:14 +09:00
if ( name === "GatewayFrame" ) {
continue ;
}
2025-12-09 15:21:16 +01:00
if ( schema . type === "object" ) {
parts . push ( emitStruct ( name , schema ) ) ;
}
}
// Frame enum must come after payload structs
parts . push ( emitGatewayFrame ( ) ) ;
const content = parts . join ( "\n" ) ;
2026-01-20 11:15:10 +00:00
for ( const outPath of outPaths ) {
await fs . mkdir ( path . dirname ( outPath ) , { recursive : true } ) ;
await fs . writeFile ( outPath , content ) ;
console . log ( ` wrote ${ outPath } ` ) ;
}
2025-12-09 15:21:16 +01:00
}
generate ( ) . catch ( ( err ) = > {
console . error ( err ) ;
process . exit ( 1 ) ;
} ) ;