diff --git a/AblyChat.xcworkspace/contents.xcworkspacedata b/AblyChat.xcworkspace/contents.xcworkspacedata
index 56985fe1..806958d1 100644
--- a/AblyChat.xcworkspace/contents.xcworkspacedata
+++ b/AblyChat.xcworkspace/contents.xcworkspacedata
@@ -7,4 +7,7 @@
+
+
diff --git a/UTSChatAdapter/UTSChatAdapter.xcodeproj/project.pbxproj b/UTSChatAdapter/UTSChatAdapter.xcodeproj/project.pbxproj
new file mode 100644
index 00000000..6f22e97d
--- /dev/null
+++ b/UTSChatAdapter/UTSChatAdapter.xcodeproj/project.pbxproj
@@ -0,0 +1,323 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 77;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 84E1054E2CBEEC9800244C8F /* AblyChat in Frameworks */ = {isa = PBXBuildFile; productRef = 84E1054D2CBEEC9800244C8F /* AblyChat */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ 847DD5D92CBC34E5000F89AE /* CopyFiles */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = /usr/share/man/man1/;
+ dstSubfolderSpec = 0;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 1;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 847DD5DB2CBC34E5000F89AE /* UTSChatAdapter */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = UTSChatAdapter; sourceTree = BUILT_PRODUCTS_DIR; };
+/* End PBXFileReference section */
+
+/* Begin PBXFileSystemSynchronizedRootGroup section */
+ 847DD5DD2CBC34E5000F89AE /* UTSChatAdapter */ = {
+ isa = PBXFileSystemSynchronizedRootGroup;
+ path = UTSChatAdapter;
+ sourceTree = "";
+ };
+/* End PBXFileSystemSynchronizedRootGroup section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 847DD5D82CBC34E5000F89AE /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 84E1054E2CBEEC9800244C8F /* AblyChat in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 847DD5D22CBC34E5000F89AE = {
+ isa = PBXGroup;
+ children = (
+ 847DD5DD2CBC34E5000F89AE /* UTSChatAdapter */,
+ 847DD5ED2CBC3674000F89AE /* Frameworks */,
+ 847DD5DC2CBC34E5000F89AE /* Products */,
+ );
+ sourceTree = "";
+ };
+ 847DD5DC2CBC34E5000F89AE /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 847DD5DB2CBC34E5000F89AE /* UTSChatAdapter */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 847DD5ED2CBC3674000F89AE /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 847DD5DA2CBC34E5000F89AE /* UTSChatAdapter */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 847DD5E22CBC34E5000F89AE /* Build configuration list for PBXNativeTarget "UTSChatAdapter" */;
+ buildPhases = (
+ 847DD5D72CBC34E5000F89AE /* Sources */,
+ 847DD5D82CBC34E5000F89AE /* Frameworks */,
+ 847DD5D92CBC34E5000F89AE /* CopyFiles */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ fileSystemSynchronizedGroups = (
+ 847DD5DD2CBC34E5000F89AE /* UTSChatAdapter */,
+ );
+ name = UTSChatAdapter;
+ packageProductDependencies = (
+ 84E1054D2CBEEC9800244C8F /* AblyChat */,
+ );
+ productName = UTSChatAdapter;
+ productReference = 847DD5DB2CBC34E5000F89AE /* UTSChatAdapter */;
+ productType = "com.apple.product-type.tool";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 847DD5D32CBC34E5000F89AE /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ BuildIndependentTargetsInParallel = 1;
+ LastSwiftUpdateCheck = 1600;
+ LastUpgradeCheck = 1600;
+ TargetAttributes = {
+ 847DD5DA2CBC34E5000F89AE = {
+ CreatedOnToolsVersion = 16.0;
+ };
+ };
+ };
+ buildConfigurationList = 847DD5D62CBC34E5000F89AE /* Build configuration list for PBXProject "UTSChatAdapter" */;
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 847DD5D22CBC34E5000F89AE;
+ minimizedProjectReferenceProxies = 1;
+ packageReferences = (
+ 84E1054C2CBEEC9800244C8F /* XCLocalSwiftPackageReference "../../ably-chat-swift" */,
+ );
+ preferredProjectObjectVersion = 77;
+ productRefGroup = 847DD5DC2CBC34E5000F89AE /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 847DD5DA2CBC34E5000F89AE /* UTSChatAdapter */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 847DD5D72CBC34E5000F89AE /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin XCBuildConfiguration section */
+ 847DD5E02CBC34E5000F89AE /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MACOSX_DEPLOYMENT_TARGET = 15.0;
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_FAST_MATH = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = macosx;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ };
+ name = Debug;
+ };
+ 847DD5E12CBC34E5000F89AE /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MACOSX_DEPLOYMENT_TARGET = 15.0;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ MTL_FAST_MATH = YES;
+ SDKROOT = macosx;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ };
+ name = Release;
+ };
+ 847DD5E32CBC34E5000F89AE /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_STYLE = Automatic;
+ MACOSX_DEPLOYMENT_TARGET = 13;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 5.0;
+ };
+ name = Debug;
+ };
+ 847DD5E42CBC34E5000F89AE /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_STYLE = Automatic;
+ MACOSX_DEPLOYMENT_TARGET = 13;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 5.0;
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 847DD5D62CBC34E5000F89AE /* Build configuration list for PBXProject "UTSChatAdapter" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 847DD5E02CBC34E5000F89AE /* Debug */,
+ 847DD5E12CBC34E5000F89AE /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 847DD5E22CBC34E5000F89AE /* Build configuration list for PBXNativeTarget "UTSChatAdapter" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 847DD5E32CBC34E5000F89AE /* Debug */,
+ 847DD5E42CBC34E5000F89AE /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+
+/* Begin XCLocalSwiftPackageReference section */
+ 84E1054C2CBEEC9800244C8F /* XCLocalSwiftPackageReference "../../ably-chat-swift" */ = {
+ isa = XCLocalSwiftPackageReference;
+ relativePath = "../../ably-chat-swift";
+ };
+/* End XCLocalSwiftPackageReference section */
+
+/* Begin XCSwiftPackageProductDependency section */
+ 84E1054D2CBEEC9800244C8F /* AblyChat */ = {
+ isa = XCSwiftPackageProductDependency;
+ productName = AblyChat;
+ };
+/* End XCSwiftPackageProductDependency section */
+ };
+ rootObject = 847DD5D32CBC34E5000F89AE /* Project object */;
+}
diff --git a/UTSChatAdapter/UTSChatAdapter.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/UTSChatAdapter/UTSChatAdapter.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 00000000..919434a6
--- /dev/null
+++ b/UTSChatAdapter/UTSChatAdapter.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/UTSChatAdapter/UTSChatAdapter.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/UTSChatAdapter/UTSChatAdapter.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
new file mode 100644
index 00000000..605b7911
--- /dev/null
+++ b/UTSChatAdapter/UTSChatAdapter.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -0,0 +1,51 @@
+{
+ "originHash" : "901dc16707a50f161b9ca480d3ab40ad69e36f382e77f0862a0e8f857b7206be",
+ "pins" : [
+ {
+ "identity" : "ably-cocoa",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/ably/ably-cocoa",
+ "state" : {
+ "branch" : "main",
+ "revision" : "4856ba6a423788902a6ef680793e7f404ceb4a51"
+ }
+ },
+ {
+ "identity" : "delta-codec-cocoa",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/ably/delta-codec-cocoa",
+ "state" : {
+ "revision" : "3ee62ea40a63996b55818d44b3f0e56d8753be88",
+ "version" : "1.3.3"
+ }
+ },
+ {
+ "identity" : "msgpack-objective-c",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/rvi/msgpack-objective-C",
+ "state" : {
+ "revision" : "3e36b48e04ecd756cb927bd5f5b9bf6d45e475f9",
+ "version" : "0.4.0"
+ }
+ },
+ {
+ "identity" : "swift-async-algorithms",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-async-algorithms",
+ "state" : {
+ "revision" : "5c8bd186f48c16af0775972700626f0b74588278",
+ "version" : "1.0.2"
+ }
+ },
+ {
+ "identity" : "swift-collections",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-collections.git",
+ "state" : {
+ "revision" : "671108c96644956dddcd89dd59c203dcdb36cec7",
+ "version" : "1.1.4"
+ }
+ }
+ ],
+ "version" : 3
+}
diff --git a/UTSChatAdapter/UTSChatAdapter/ChatAdapter.swift b/UTSChatAdapter/UTSChatAdapter/ChatAdapter.swift
new file mode 100644
index 00000000..8c9677de
--- /dev/null
+++ b/UTSChatAdapter/UTSChatAdapter/ChatAdapter.swift
@@ -0,0 +1,58 @@
+import Ably
+import AblyChat
+
+/**
+ * Unified Test Suite adapter for swift Chat SDK
+ */
+class ChatAdapter {
+ // Runtime SDK objects storage
+ private var idToChannel = [String: ARTRealtimeChannel]()
+ private var idToChannels = [String: ARTRealtimeChannels]()
+ private var idToChatClient = [String: ChatClient]()
+ private var idToConnection = [String: Connection]()
+ private var idToConnectionStatus = [String: ConnectionStatus]()
+ private var idToMessage = [String: Message]()
+ private var idToMessages = [String: Messages]()
+ private var idToOccupancy = [String: Occupancy]()
+ private var idToPaginatedResult = [String: any PaginatedResult]()
+ private var idToPresence = [String: Presence]()
+ private var idToRealtime = [String: RealtimeClient]()
+ private var idToRealtimeChannel = [String: RealtimeChannelProtocol]()
+ private var idToRoom = [String: Room]()
+ private var idToRoomReactions = [String: RoomReactions]()
+ private var idToRooms = [String: Rooms]()
+ private var idToRoomStatus = [String: RoomStatus]()
+ private var idToTyping = [String: Typing]()
+ private var idToPaginatedResultMessage = [String: any PaginatedResultMessage]()
+ private var idToMessageSubscription = [String: MessageSubscription]()
+ private var idToOnConnectionStatusChange = [String: OnConnectionStatusChange]()
+ private var idToOnDiscontinuitySubscription = [String: OnDiscontinuitySubscription]()
+ private var idToOccupancySubscription = [String: OccupancySubscription]()
+ private var idToRoomReactionsSubscription = [String: RoomReactionsSubscription]()
+ private var idToOnRoomStatusChange = [String: OnRoomStatusChange]()
+ private var idToTypingSubscription = [String: TypingSubscription]()
+ private var idToPresenceSubscription = [String: PresenceSubscription]()
+
+ private var webSocket: WebSocketWrapper
+
+ init(webSocket: WebSocketWrapper) {
+ self.webSocket = webSocket
+ }
+
+ func handleRpcCall(rpcParams: JSON) async throws -> String? {
+ guard let method = rpcParams["method"] as? String else {
+ print("Method not found.")
+ return nil
+ }
+
+ switch method {
+
+ // GENERATED CONTENT BEGIN
+ // ...
+ // GENERATED CONTENT END
+
+ default:
+ return jsonRpcResult(rpcParams.requestId, "{\"refId\":\"Unknown method provided.\"}")
+ }
+ }
+}
diff --git a/UTSChatAdapter/UTSChatAdapter/ChatAdapterGenerator.swift b/UTSChatAdapter/UTSChatAdapter/ChatAdapterGenerator.swift
new file mode 100644
index 00000000..b390169d
--- /dev/null
+++ b/UTSChatAdapter/UTSChatAdapter/ChatAdapterGenerator.swift
@@ -0,0 +1,250 @@
+import Foundation
+
+/**
+ * Unified Test Suite adapter generator for swift Chat SDK
+ */
+class ChatAdapterGenerator {
+
+ var generatedFileContent = "// GENERATED CONTENT BEGIN\n\n"
+
+ func generate() {
+ Schema.json.forEach { generateSchema($0) }
+ generatedFileContent += "// GENERATED CONTENT END"
+ print(generatedFileContent)
+ }
+
+ func generateSchema(_ schema: JSON) {
+ guard let objectType = schema.name else {
+ return print("Schema should have a name.")
+ }
+ if let constructor = schema.constructor {
+ generateConstructorForType(objectType, schema: constructor, isAsync: false, throwing: false)
+ }
+ for method in schema.syncMethods?.sortedByKey() ?? [] {
+ generateMethodForType(objectType, methodName: method.key, methodSchema: method.value as! JSON, isAsync: false, throwing: true)
+ }
+ for method in schema.asyncMethods?.sortedByKey() ?? [] {
+ generateMethodForType(objectType, methodName: method.key, methodSchema: method.value as! JSON, isAsync: true, throwing: true)
+ }
+ for field in schema.fields?.sortedByKey() ?? [] {
+ generateFieldForType(objectType, fieldName: field.key, fieldSchema: field.value as! JSON)
+ }
+ for method in schema.listeners?.sortedByKey() ?? [] {
+ generateMethodWithCallbackForType(objectType, methodName: method.key, methodSchema: method.value as! JSON, isAsync: true, throwing: true)
+ }
+ }
+
+ func generateConstructorForType(_ objectType: String, schema: JSON, isAsync: Bool, throwing: Bool) {
+ let implPath = "\(objectType)"
+ if Schema.skipPaths.contains([implPath]) {
+ return print("\(implPath) was not yet implemented or requires custom implementation.")
+ }
+ let methodArgs = schema.args ?? [:]
+ let paramsDeclarations = methodArgs.map {
+ let argSchema = $0.value as! JSON
+ return " let \($0.key.bigD()) = \(altTypeName(argSchema.type!)).from(rpcParams.methodArgs[\"\($0.key)\"])"
+ }
+ let callParams = methodArgs.map { "\($0.key.bigD()): \($0.key.bigD())" }.joined(separator: ", ")
+ generatedFileContent +=
+ """
+ case "\(objectType)":
+ """
+ if !paramsDeclarations.isEmpty {
+ generatedFileContent += paramsDeclarations.joined(separator: "\n") + "\n"
+ }
+ generatedFileContent +=
+ """
+ let \(altTypeName(objectType).firstLowercased()) = \(altTypeName(objectType))(\(callParams))
+ let instanceId = generateId()
+ idTo\(altTypeName(objectType))[instanceId] = \(altTypeName(objectType).firstLowercased())
+ return jsonRpcResult(rpcParams.requestId, "{\\"instanceId\\":\\"\\(instanceId)\\"}")
+ \n
+ """
+ }
+
+ func generateMethodForType(_ objectType: String, methodName: String, methodSchema: JSON, isAsync: Bool, throwing: Bool) {
+ let implPath = "\(objectType).\(methodName)"
+ if Schema.skipPaths.contains([implPath]) {
+ return print("\(implPath) was not yet implemented or requires custom implementation.")
+ }
+ let methodArgs = methodSchema.args ?? [:]
+ let paramsDeclarations = methodArgs.map {
+ let argSchema = $0.value as! JSON
+ return " let \($0.key.bigD()) = \(altTypeName(argSchema.type!)).from(rpcParams.methodArgs[\"\($0.key)\"])"
+ }
+ let callParams = methodArgs.map { "\($0.key.bigD()): \($0.key.bigD())" }.joined(separator: ", ")
+ let hasResult = methodSchema.result.type != nil && methodSchema.result.type != "void"
+ let resultType = altTypeName(methodSchema.result.type ?? "void")
+ generatedFileContent +=
+ """
+ case "\(objectType).\(methodName)":\n
+ """
+ if !paramsDeclarations.isEmpty {
+ generatedFileContent += paramsDeclarations.joined(separator: "\n") + "\n"
+ }
+ generatedFileContent +=
+ """
+ guard let \(altTypeName(objectType).firstLowercased())Ref = idTo\(altTypeName(objectType))[rpcParams.refId] else {
+ print("\(altTypeName(objectType)) with `refId == \\(rpcParams.refId)` doesn't exist.")
+ return nil
+ }
+ \(hasResult ? "let \(resultType.firstLowercased()) = " : "")\(throwing ? "try " : "")\(isAsync ? "await " : "")\(altTypeName(objectType).firstLowercased())Ref.\(methodName)(\(callParams)) // \(resultType)\n
+ """
+ if hasResult {
+ if isJsonPrimitiveType(methodSchema.result.type!) {
+ generatedFileContent +=
+ """
+ return jsonRpcResult(rpcParams.requestId, "{\\"response\\": \\"\\(\(resultType.firstLowercased()))\\"}")
+ \n
+ """
+ } else if methodSchema.result.isSerializable {
+ generatedFileContent +=
+ """
+ return jsonRpcResult(rpcParams.requestId, "{\\"response\\": \\(jsonString(\(resultType.firstLowercased())))}")
+ \n
+ """
+ } else {
+ generatedFileContent +=
+ """
+ let resultRefId = generateId()
+ idTo\(altTypeName(methodSchema.result.type!))[resultRefId] = \(resultType.firstLowercased())
+ return jsonRpcResult(rpcParams.requestId, "{\\"refId\\":\\"\\(resultRefId)\\"}")
+ \n
+ """
+ }
+ }
+ else {
+ generatedFileContent +=
+ """
+ return jsonRpcResult(rpcParams.requestId, "{}")
+ \n
+ """
+ }
+ }
+
+ func generateFieldForType(_ objectType: String, fieldName: String, fieldSchema: JSON) {
+ guard let fieldType = fieldSchema.type else {
+ return print("Type information for '\(fieldName)' field is incorrect.")
+ }
+ let implPath = "\(objectType)#\(fieldName)"
+ if Schema.skipPaths.contains([implPath]) {
+ return print("\(implPath) was not yet implemented or requires custom implementation.")
+ }
+ generatedFileContent +=
+ """
+ case "\(implPath)":
+ guard let \(altTypeName(objectType).firstLowercased())Ref = idTo\(altTypeName(objectType))[rpcParams.refId] else {
+ print("\(altTypeName(objectType)) with `refId == \\(rpcParams.refId)` doesn't exist.")
+ return nil
+ }
+ let \(fieldName.bigD()) = \(altTypeName(objectType).firstLowercased())Ref.\(fieldName.bigD()) // \(fieldType)\n
+ """
+
+ if fieldSchema.isSerializable {
+ if isJsonPrimitiveType(fieldType) {
+ generatedFileContent +=
+ """
+ return jsonRpcResult(rpcParams.requestId, "{\\"response\\": \\"\\(\(fieldName.bigD()))\\"}")
+ \n
+ """
+ } else {
+ generatedFileContent +=
+ """
+ return jsonRpcResult(rpcParams.requestId, "{\\"response\\": \\(jsonString(\(fieldName.bigD())))}")
+ \n
+ """
+ }
+ } else {
+ generatedFileContent +=
+ """
+ let fieldRefId = generateId()
+ idTo\(fieldType)[fieldRefId] = \(fieldName.bigD())
+ return jsonRpcResult(rpcParams.requestId, "{\\"refId\\":\\"\\(fieldRefId)\\"}")
+ \n
+ """
+ }
+ }
+
+ func generateMethodWithCallbackForType(_ objectType: String, methodName: String, methodSchema: JSON, isAsync: Bool, throwing: Bool) {
+ let implPath = "\(objectType).\(methodName)"
+ if Schema.skipPaths.contains([implPath]) {
+ return print("\(implPath) was not yet implemented or requires custom implementation.")
+ }
+ let methodArgs = methodSchema.args ?? [:]
+ let paramsSignatures = methodArgs.compactMap {
+ let argName = $0.key
+ let argType = ($0.value as! JSON).type!
+ if argType != "callback" {
+ return (declaration: " let \(argName.bigD()) = \(altTypeName(argType)).from(rpcParams.methodArgs[\"\(argName)\"])",
+ usage: "\(argName.bigD()): \(argName.bigD())")
+ } else {
+ return nil
+ }
+ }
+ let callParams = (paramsSignatures.map { $0.usage } + ["bufferingPolicy: .unbounded"]).joined(separator: ", ")
+ generatedFileContent +=
+ """
+ case "\(objectType).\(methodName)":\n
+ """
+ if !paramsSignatures.isEmpty {
+ generatedFileContent += paramsSignatures.map { $0.declaration }.joined(separator: "\n") + "\n"
+ }
+ generatedFileContent +=
+ """
+ guard let \(altTypeName(objectType).firstLowercased())Ref = idTo\(altTypeName(objectType))[rpcParams.refId] else {
+ print("\(altTypeName(objectType)) with `refId == \\(rpcParams.refId)` doesn't exist.")
+ return nil
+ }
+ let subscription = \(throwing ? "try " : "")\(isAsync ? "await " : "")\(altTypeName(objectType).firstLowercased())Ref.\(altMethodName(methodName))(\(callParams))\n
+ """
+ generatedFileContent += generateCallback(methodSchema.callback!, isAsync: false, throwing: false)
+ generatedFileContent +=
+ """
+ let resultRefId = generateId()
+ idTo\(altTypeName(methodSchema.result.type!))[resultRefId] = subscription
+ return jsonRpcResult(rpcParams.requestId, "{\\"refId\\":\\"\\(resultRefId)\\"}")
+ \n
+ """
+ }
+
+ func generateCallback(_ callbackSchema: JSON, isAsync: Bool, throwing: Bool) -> String {
+ let callbackArgs = callbackSchema.args ?? [:]
+ let paramsSignatures = callbackArgs.prefix(1).compactMap { // code below simplifies it to just one callback parameter
+ let argName = $0.key
+ let argType = ($0.value as! JSON).type!
+ let isOptional = ($0.value as! JSON).isOptional
+ return (declaration: "\(altTypeName(argType))" + (isOptional ? "?" : ""), usage: "\(argName.bigD())")
+ }
+ let paramsDeclaration = paramsSignatures.map { $0.declaration }.joined(separator: ", ")
+ let paramsUsage = paramsSignatures.map { $0.usage }.joined(separator: ", ")
+ var result =
+ """
+ let callback: (\(paramsDeclaration)) -> \(altTypeName(callbackSchema.result.type!)) = {\n
+ """
+ if (callbackArgs.first?.value as? JSON)?.isOptional ?? false {
+ result +=
+ """
+ if let param = $0 {
+ self.webSocket.send(text: jsonRpcCallback(rpcParams.callbackId, "\\(jsonString(param))"))
+ } else {
+ self.webSocket.send(text: jsonRpcCallback(rpcParams.callbackId, "{}"))
+ }\n
+ """
+ } else {
+ result +=
+ """
+ self.webSocket.send(text: jsonRpcCallback(rpcParams.callbackId, "\\(jsonString($0))"))\n
+ """
+ }
+ result +=
+ """
+ }
+ Task {
+ for await \(paramsUsage) in subscription {
+ callback(\(paramsUsage))
+ }
+ }\n
+ """
+ return result
+ }
+}
diff --git a/UTSChatAdapter/UTSChatAdapter/NanoID.swift b/UTSChatAdapter/UTSChatAdapter/NanoID.swift
new file mode 100644
index 00000000..55a184f6
--- /dev/null
+++ b/UTSChatAdapter/UTSChatAdapter/NanoID.swift
@@ -0,0 +1,125 @@
+//
+// NanoID.swift
+//
+// Created by Anton Lovchikov on 05/07/2018.
+// Copyright © 2018 Anton Lovchikov. All rights reserved.
+//
+
+import Foundation
+
+/// USAGE
+///
+/// Nano ID with default alphabet (0-9a-zA-Z_~) and length (21 chars)
+/// let id = NanoID.new()
+///
+/// Nano ID with default alphabet and given length
+/// let id = NanoID.new(12)
+///
+/// Nano ID with given alphabet and length
+/// let id = NanoID.new(alphabet: .uppercasedLatinLetters, size: 15)
+///
+/// Nano ID with preset custom parameters
+/// let nanoID = NanoID(alphabet: .lowercasedLatinLetters,.numbers, size:10)
+/// let idFirst = nanoID.new()
+/// let idSecond = nanoID.new()
+
+class NanoID {
+
+ // Shared Parameters
+ private var size: Int
+ private var alphabet: String
+
+ /// Inits an instance with Shared Parameters
+ init(alphabet: NanoIDAlphabet..., size: Int) {
+ self.size = size
+ self.alphabet = NanoIDHelper.parse(alphabet)
+ }
+
+ /// Generates a Nano ID using Shared Parameters
+ func new() -> String {
+ return NanoIDHelper.generate(from: alphabet, of: size)
+ }
+
+ // Default Parameters
+ private static let defaultSize = 21
+ private static let defaultAphabet = NanoIDAlphabet.urlSafe.toString()
+
+ /// Generates a Nano ID using Default Parameters
+ static func new() -> String {
+ return NanoIDHelper.generate(from: defaultAphabet, of: defaultSize)
+ }
+
+ /// Generates a Nano ID using given occasional parameters
+ static func new(alphabet: NanoIDAlphabet..., size: Int) -> String {
+ let charactersString = NanoIDHelper.parse(alphabet)
+ return NanoIDHelper.generate(from: charactersString, of: size)
+ }
+
+ /// Generates a Nano ID using Default Alphabet and given size
+ static func new(_ size: Int) -> String {
+ return NanoIDHelper.generate(from: NanoID.defaultAphabet, of: size)
+ }
+}
+
+fileprivate class NanoIDHelper {
+
+ /// Parses input alphabets into a string
+ static func parse(_ alphabets: [NanoIDAlphabet]) -> String {
+
+ var stringCharacters = ""
+
+ for alphabet in alphabets {
+ stringCharacters.append(alphabet.toString())
+ }
+
+ return stringCharacters
+ }
+
+ /// Generates a Nano ID using given parameters
+ static func generate(from alphabet: String, of length: Int) -> String {
+ var nanoID = ""
+
+ for _ in 0.. Character {
+ let randomNum = Int(arc4random_uniform(UInt32(string.count)))
+ let randomIndex = string.index(string.startIndex, offsetBy: randomNum)
+ return string[randomIndex]
+ }
+}
+
+enum NanoIDAlphabet {
+ case urlSafe
+ case uppercasedLatinLetters
+ case lowercasedLatinLetters
+ case numbers
+
+ func toString() -> String {
+ switch self {
+ case .uppercasedLatinLetters, .lowercasedLatinLetters, .numbers:
+ return self.chars()
+ case .urlSafe:
+ return ("\(NanoIDAlphabet.uppercasedLatinLetters.chars())\(NanoIDAlphabet.lowercasedLatinLetters.chars())\(NanoIDAlphabet.numbers.chars())~_")
+ }
+ }
+
+ private func chars() -> String {
+ switch self {
+ case .uppercasedLatinLetters:
+ return "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ case .lowercasedLatinLetters:
+ return "abcdefghijklmnopqrstuvwxyz"
+ case .numbers:
+ return "1234567890"
+ default:
+ return ""
+ }
+ }
+}
diff --git a/UTSChatAdapter/UTSChatAdapter/Schema+Adjustments.swift b/UTSChatAdapter/UTSChatAdapter/Schema+Adjustments.swift
new file mode 100644
index 00000000..ce066b1d
--- /dev/null
+++ b/UTSChatAdapter/UTSChatAdapter/Schema+Adjustments.swift
@@ -0,0 +1,138 @@
+import Ably
+import AblyChat
+
+typealias ErrorInfo = ARTErrorInfo
+typealias AblyErrorInfo = ARTErrorInfo
+typealias RealtimePresenceParams = PresenceQuery
+typealias PaginatedResultMessage = PaginatedResult
+typealias OnConnectionStatusChange = Subscription
+typealias OnDiscontinuitySubscription = Subscription
+typealias OccupancySubscription = Subscription
+typealias RoomReactionsSubscription = Subscription
+typealias OnRoomStatusChange = Subscription
+typealias TypingSubscription = Subscription
+typealias PresenceSubscription = Subscription
+
+struct PresenceDataWrapper { }
+
+fileprivate let altTypesMap = [
+ "void": "Void",
+ "PresenceData": "\(PresenceDataWrapper.self)",
+ "MessageSubscriptionResponse": "\(MessageSubscription.self)",
+ "OnConnectionStatusChangeResponse": "OnConnectionStatusChange",
+ "OccupancySubscriptionResponse": "OccupancySubscription",
+ "RoomReactionsSubscriptionResponse": "RoomReactionsSubscription",
+ "OnDiscontinuitySubscriptionResponse": "OnDiscontinuitySubscription",
+ "OnRoomStatusChangeResponse": "OnRoomStatusChange",
+ "TypingSubscriptionResponse": "TypingSubscription",
+ "PresenceSubscriptionResponse": "PresenceSubscription",
+ "MessageEventPayload": "\(Message.self)"
+]
+
+fileprivate let jsonPrimitiveTypesMap = [
+ "string": "\(String.self)",
+ "boolean": "\(Bool.self)",
+ "number": "\(Int.self)"
+]
+
+fileprivate let altMethodsMap = [
+ "onDiscontinuity": "subscribeToDiscontinuities",
+ "subscribe_listener": "subscribeAll",
+]
+
+func isJsonPrimitiveType(_ typeName: String) -> Bool {
+ jsonPrimitiveTypesMap.keys.contains([typeName])
+}
+
+func altTypeName(_ typeName: String) -> String {
+ (altTypesMap[typeName] ?? jsonPrimitiveTypesMap[typeName]) ?? typeName
+}
+
+func altMethodName(_ methodName: String) -> String {
+ altMethodsMap[methodName] ?? methodName
+}
+
+extension Message {
+ public func before(message: Message) throws -> Bool {
+ try isBefore(message)
+ }
+
+ public func after(message: Message) throws -> Bool {
+ try isAfter(message)
+ }
+
+ public func equal(message: Message) throws -> Bool {
+ try isEqual(message)
+ }
+}
+
+extension Messages {
+ func send(options: SendMessageParams) async throws -> Message {
+ try await send(params: options)
+ }
+}
+
+extension String {
+ func bigD() -> String {
+ replacingOccurrences(of: "Id", with: "ID")
+ }
+}
+
+extension Room {
+ func options() -> RoomOptions { options }
+}
+
+extension PaginatedResult {
+ func hasNext() -> Bool { hasNext }
+ func isLast() -> Bool { isLast }
+ func next() async throws -> (any PaginatedResult)? { try await next }
+ func first() async throws -> (any PaginatedResult)? { try await first }
+ func current() async throws -> (any PaginatedResult)? { try await current }
+}
+
+extension Presence {
+ func subscribeAll() async -> Subscription {
+ await subscribe(events: [.enter, .leave, .present, .update])
+ }
+}
+
+extension Schema {
+ // These paths were not yet implemented in SDK or require custom implementation:
+ static let skipPaths = [
+ "ChatClient", // custom constructor with realtime instance
+ "ChatClient#logger", // not exposed
+ "ConnectionStatus#error", // optional
+ "Presence#channel", // not implemented
+ "RoomStatus#error", // not available directly (via lifecycle object)
+ "Message#createdAt", // optional
+ "Presence.subscribe_eventsAndListener", // impossible to infer param type from `string`
+
+ "ChatClient.addReactAgent",
+
+ "Messages.unsubscribeAll",
+ "Presence.unsubscribeAll",
+ "Occupancy.unsubscribeAll",
+ "RoomReactions.unsubscribeAll",
+ "Typing.unsubscribeAll",
+
+ "TypingSubscriptionResponse.unsubscribe",
+ "MessageSubscriptionResponse.unsubscribe",
+ "OccupancySubscriptionResponse.unsubscribe",
+ "PresenceSubscriptionResponse.unsubscribe",
+ "PresenceSubscriptionResponse.unsubscribe",
+ "RoomReactionsSubscriptionResponse.unsubscribe",
+
+ "OnConnectionStatusChangeResponse.off",
+ "OnDiscontinuitySubscriptionResponse.off",
+ "OnRoomStatusChangeResponse.off",
+
+ "ConnectionStatus.offAll",
+ "RoomStatus.offAll",
+
+ "Logger.error",
+ "Logger.trace",
+ "Logger.info",
+ "Logger.debug",
+ "Logger.warn",
+ ]
+}
diff --git a/UTSChatAdapter/UTSChatAdapter/Schema.swift b/UTSChatAdapter/UTSChatAdapter/Schema.swift
new file mode 100644
index 00000000..ccbd5fda
--- /dev/null
+++ b/UTSChatAdapter/UTSChatAdapter/Schema.swift
@@ -0,0 +1,994 @@
+import Foundation
+
+struct Schema {
+ static var json: [JSON] = {
+ do {
+ return try JSONSerialization.jsonObject(with: Self.content.data(using: .utf8)!) as! [JSON]
+ } catch {
+ print("Couldn't parse schema JSON.")
+ return []
+ }
+ }()
+}
+
+extension Schema {
+ static let content =
+"""
+[
+ {
+ "name": "ChatClient",
+ "konstructor": {
+ "args": {
+ "realtimeClientOptions": {
+ "type": "RealtimeClientOptions",
+ "serializable": true
+ },
+ "clientOptions": {
+ "type": "ClientOptions",
+ "serializable": true,
+ "optional": true
+ }
+ }
+ },
+ "fields": {
+ "rooms": {
+ "type": "Rooms",
+ "serializable": false
+ },
+ "connection": {
+ "type": "Connection",
+ "serializable": false
+ },
+ "clientId": {
+ "type": "string",
+ "serializable": true
+ },
+ "realtime": {
+ "type": "Realtime",
+ "serializable": false
+ },
+ "clientOptions": {
+ "type": "ClientOptions",
+ "serializable": true
+ },
+ "logger": {
+ "type": "Logger",
+ "serializable": false
+ }
+ },
+ "syncMethods": {
+ "addReactAgent": {
+ "result": {
+ "type": "void"
+ }
+ }
+ }
+ },
+ {
+ "name": "ConnectionStatus",
+ "fields": {
+ "current": {
+ "type": "string",
+ "serializable": true
+ },
+ "error": {
+ "type": "ErrorInfo",
+ "serializable": true
+ }
+ },
+ "syncMethods": {
+ "offAll": {
+ "result": {
+ "type": "void"
+ }
+ }
+ },
+ "listeners": {
+ "onChange": {
+ "args": {
+ "listener": {
+ "type": "callback",
+ "args": {
+ "change": {
+ "type": "ConnectionStatusChange",
+ "serializable": true
+ }
+ },
+ "result": {
+ "type": "void"
+ }
+ }
+ },
+ "result": {
+ "type": "OnConnectionStatusChangeResponse",
+ "serializable": false
+ }
+ }
+ }
+ },
+ {
+ "name": "OnConnectionStatusChangeResponse",
+ "syncMethods": {
+ "off": {
+ "result": {
+ "type": "void"
+ }
+ }
+ }
+ },
+ {
+ "name": "Connection",
+ "fields": {
+ "status": {
+ "type": "ConnectionStatus",
+ "serializable": false
+ }
+ }
+ },
+ {
+ "name": "Logger",
+ "syncMethods": {
+ "trace": {
+ "args": {
+ "message": {
+ "type": "string",
+ "serializable": true
+ },
+ "context": {
+ "type": "object",
+ "serializable": true,
+ "optional": true
+ }
+ },
+ "result": {
+ "type": "void"
+ }
+ },
+ "debug": {
+ "args": {
+ "message": {
+ "type": "string",
+ "serializable": true
+ },
+ "context": {
+ "type": "object",
+ "serializable": true,
+ "optional": true
+ }
+ },
+ "result": {
+ "type": "void"
+ }
+ },
+ "info": {
+ "args": {
+ "message": {
+ "type": "string",
+ "serializable": true
+ },
+ "context": {
+ "type": "object",
+ "serializable": true,
+ "optional": true
+ }
+ },
+ "result": {
+ "type": "void"
+ }
+ },
+ "warn": {
+ "args": {
+ "message": {
+ "type": "string",
+ "serializable": true
+ },
+ "context": {
+ "type": "object",
+ "serializable": true,
+ "optional": true
+ }
+ },
+ "result": {
+ "type": "void"
+ }
+ },
+ "error": {
+ "args": {
+ "message": {
+ "type": "string",
+ "serializable": true
+ },
+ "context": {
+ "type": "object",
+ "serializable": true,
+ "optional": true
+ }
+ },
+ "result": {
+ "type": "void"
+ }
+ }
+ }
+ },
+ {
+ "name": "Message",
+ "fields": {
+ "timeserial": {
+ "type": "string",
+ "serializable": true
+ },
+ "clientId": {
+ "type": "string",
+ "serializable": true
+ },
+ "roomId": {
+ "type": "string",
+ "serializable": true
+ },
+ "text": {
+ "type": "string",
+ "serializable": true
+ },
+ "createdAt": {
+ "type": "number",
+ "serializable": true
+ },
+ "metadata": {
+ "type": "object",
+ "serializable": true
+ },
+ "headers": {
+ "type": "object",
+ "serializable": true
+ }
+ },
+ "syncMethods": {
+ "before": {
+ "args": {
+ "message": {
+ "type": "Message",
+ "serializable": false
+ }
+ },
+ "result": {
+ "type": "boolean"
+ }
+ },
+ "after": {
+ "args": {
+ "message": {
+ "type": "Message",
+ "serializable": false
+ }
+ },
+ "result": {
+ "type": "boolean"
+ }
+ },
+ "equal": {
+ "args": {
+ "message": {
+ "type": "Message",
+ "serializable": false
+ }
+ },
+ "result": {
+ "type": "boolean"
+ }
+ }
+ }
+ },
+ {
+ "name": "Messages",
+ "fields": {
+ "channel": {
+ "type": "RealtimeChannel",
+ "serializable": false
+ }
+ },
+ "syncMethods": {
+ "unsubscribeAll": {
+ "result": {
+ "type": "void"
+ }
+ }
+ },
+ "asyncMethods": {
+ "get": {
+ "args": {
+ "options": {
+ "type": "QueryOptions",
+ "serializable": true
+ }
+ },
+ "result": {
+ "type": "PaginatedResultMessage",
+ "serializable": false
+ }
+ },
+ "send": {
+ "args": {
+ "options": {
+ "type": "SendMessageParams",
+ "serializable": true
+ }
+ },
+ "result": {
+ "type": "Message",
+ "serializable": false
+ }
+ }
+ },
+ "listeners": {
+ "subscribe": {
+ "args": {
+ "listener": {
+ "type": "callback",
+ "args": {
+ "event": {
+ "type": "MessageEventPayload",
+ "serializable": false
+ }
+ },
+ "result": {
+ "type": "void"
+ }
+ }
+ },
+ "result": {
+ "type": "MessageSubscriptionResponse",
+ "serializable": false
+ }
+ },
+ "onDiscontinuity": {
+ "args": {
+ "listener": {
+ "type": "callback",
+ "args": {
+ "reason": {
+ "type": "AblyErrorInfo",
+ "serializable": true,
+ "optional": true
+ }
+ },
+ "result": {
+ "type": "void"
+ }
+ }
+ },
+ "result": {
+ "type": "OnDiscontinuitySubscriptionResponse",
+ "serializable": false
+ }
+ }
+ }
+ },
+ {
+ "name": "MessageSubscriptionResponse",
+ "syncMethods": {
+ "unsubscribe": {
+ "result": {
+ "type": "void"
+ }
+ }
+ },
+ "asyncMethods": {
+ "getPreviousMessages": {
+ "args": {
+ "params": {
+ "type": "QueryOptions",
+ "serializable": true
+ }
+ },
+ "result": {
+ "type": "PaginatedResultMessage",
+ "serializable": false
+ }
+ }
+ }
+ },
+ {
+ "name": "Occupancy",
+ "fields": {
+ "channel": {
+ "type": "RealtimeChannel",
+ "serializable": false
+ }
+ },
+ "syncMethods": {
+ "unsubscribeAll": {
+ "result": {
+ "type": "void"
+ }
+ }
+ },
+ "asyncMethods": {
+ "get": {
+ "result": {
+ "type": "OccupancyEvent",
+ "serializable": true
+ }
+ }
+ },
+ "listeners": {
+ "subscribe": {
+ "args": {
+ "listener": {
+ "type": "callback",
+ "args": {
+ "event": {
+ "type": "OccupancyEvent",
+ "serializable": true
+ }
+ },
+ "result": {
+ "type": "void"
+ }
+ }
+ },
+ "result": {
+ "type": "OccupancySubscriptionResponse",
+ "serializable": false
+ }
+ },
+ "onDiscontinuity": {
+ "args": {
+ "listener": {
+ "type": "callback",
+ "args": {
+ "reason": {
+ "type": "AblyErrorInfo",
+ "serializable": true,
+ "optional": true
+ }
+ },
+ "result": {
+ "type": "void"
+ }
+ }
+ },
+ "result": {
+ "type": "OnDiscontinuitySubscriptionResponse",
+ "serializable": false
+ }
+ }
+ }
+ },
+ {
+ "name": "OccupancySubscriptionResponse",
+ "syncMethods": {
+ "unsubscribe": {
+ "result": {
+ "type": "void"
+ }
+ }
+ }
+ },
+ {
+ "name": "OnDiscontinuitySubscriptionResponse",
+ "syncMethods": {
+ "off": {
+ "result": {
+ "type": "void"
+ }
+ }
+ }
+ },
+ {
+ "name": "PaginatedResult",
+ "fields": {
+ "items": {
+ "type": "object",
+ "serializable": true,
+ "array": true
+ }
+ },
+ "syncMethods": {
+ "hasNext": {
+ "result": {
+ "type": "boolean"
+ }
+ },
+ "isLast": {
+ "result": {
+ "type": "boolean"
+ }
+ }
+ },
+ "asyncMethods": {
+ "next": {
+ "result": {
+ "type": "PaginatedResult",
+ "serializable": false
+ }
+ },
+ "first": {
+ "result": {
+ "type": "PaginatedResult",
+ "serializable": false
+ }
+ },
+ "current": {
+ "result": {
+ "type": "PaginatedResult",
+ "serializable": false
+ }
+ }
+ }
+ },
+ {
+ "name": "Presence",
+ "fields": {
+ "channel": {
+ "type": "RealtimeChannel",
+ "serializable": false
+ }
+ },
+ "syncMethods": {
+ "unsubscribeAll": {
+ "result": {
+ "type": "void"
+ }
+ }
+ },
+ "asyncMethods": {
+ "get": {
+ "args": {
+ "params": {
+ "type": "RealtimePresenceParams",
+ "serializable": true,
+ "optional": true
+ }
+ },
+ "result": {
+ "type": "PresenceMember",
+ "serializable": true,
+ "array": true
+ }
+ },
+ "isUserPresent": {
+ "args": {
+ "clientId": {
+ "type": "string",
+ "serializable": true
+ }
+ },
+ "result": {
+ "type": "boolean",
+ "serializable": true
+ }
+ },
+ "enter": {
+ "args": {
+ "data": {
+ "type": "PresenceData",
+ "serializable": true,
+ "optional": true
+ }
+ },
+ "result": {
+ "type": "void"
+ }
+ },
+ "update": {
+ "args": {
+ "data": {
+ "type": "PresenceData",
+ "serializable": true,
+ "optional": true
+ }
+ },
+ "result": {
+ "type": "void"
+ }
+ },
+ "leave": {
+ "args": {
+ "data": {
+ "type": "PresenceData",
+ "serializable": true,
+ "optional": true
+ }
+ },
+ "result": {
+ "type": "void"
+ }
+ }
+ },
+ "listeners": {
+ "subscribe_listener": {
+ "args": {
+ "listener": {
+ "type": "callback",
+ "args": {
+ "event": {
+ "type": "PresenceEvent",
+ "serializable": true
+ }
+ },
+ "result": {
+ "type": "void"
+ }
+ }
+ },
+ "result": {
+ "type": "PresenceSubscriptionResponse",
+ "serializable": false
+ }
+ },
+ "subscribe_eventsAndListener": {
+ "args": {
+ "events": {
+ "type": "string",
+ "array": true,
+ "serializable": true
+ },
+ "listener": {
+ "type": "callback",
+ "args": {
+ "event": {
+ "type": "PresenceEvent",
+ "serializable": true
+ }
+ },
+ "result": {
+ "type": "void"
+ }
+ }
+ },
+ "result": {
+ "type": "PresenceSubscriptionResponse",
+ "serializable": false
+ }
+ },
+ "onDiscontinuity": {
+ "args": {
+ "listener": {
+ "type": "callback",
+ "args": {
+ "reason": {
+ "type": "AblyErrorInfo",
+ "serializable": true,
+ "optional": true
+ }
+ },
+ "result": {
+ "type": "void"
+ }
+ }
+ },
+ "result": {
+ "type": "OnDiscontinuitySubscriptionResponse",
+ "serializable": false
+ }
+ }
+ }
+ },
+ {
+ "name": "PresenceSubscriptionResponse",
+ "syncMethods": {
+ "unsubscribe": {
+ "result": {
+ "type": "void"
+ }
+ }
+ }
+ },
+ {
+ "name": "RoomReactions",
+ "fields": {
+ "channel": {
+ "type": "RealtimeChannel",
+ "serializable": false
+ }
+ },
+ "syncMethods": {
+ "unsubscribeAll": {
+ "result": {
+ "type": "void"
+ }
+ }
+ },
+ "asyncMethods": {
+ "send": {
+ "args": {
+ "params": {
+ "type": "SendReactionParams",
+ "serializable": true
+ }
+ },
+ "result": {
+ "type": "void"
+ }
+ }
+ },
+ "listeners": {
+ "subscribe": {
+ "args": {
+ "listener": {
+ "type": "callback",
+ "args": {
+ "reaction": {
+ "type": "Reaction",
+ "serializable": true
+ }
+ },
+ "result": {
+ "type": "void"
+ }
+ }
+ },
+ "result": {
+ "type": "RoomReactionsSubscriptionResponse",
+ "serializable": false
+ }
+ },
+ "onDiscontinuity": {
+ "args": {
+ "listener": {
+ "type": "callback",
+ "args": {
+ "reason": {
+ "type": "AblyErrorInfo",
+ "serializable": true,
+ "optional": true
+ }
+ },
+ "result": {
+ "type": "void"
+ }
+ }
+ },
+ "result": {
+ "type": "OnDiscontinuitySubscriptionResponse",
+ "serializable": false
+ }
+ }
+ }
+ },
+ {
+ "name": "RoomReactionsSubscriptionResponse",
+ "syncMethods": {
+ "unsubscribe": {
+ "result": {
+ "type": "void"
+ }
+ }
+ }
+ },
+ {
+ "name": "RoomStatus",
+ "fields": {
+ "current": {
+ "type": "string",
+ "serializable": true
+ },
+ "error": {
+ "type": "ErrorInfo",
+ "serializable": true
+ }
+ },
+ "syncMethods": {
+ "offAll": {
+ "result": {
+ "type": "void"
+ }
+ }
+ },
+ "listeners": {
+ "onChange": {
+ "args": {
+ "listener": {
+ "type": "callback",
+ "args": {
+ "change": {
+ "type": "RoomStatusChange",
+ "serializable": true
+ }
+ },
+ "result": {
+ "type": "void"
+ }
+ }
+ },
+ "result": {
+ "type": "OnRoomStatusChangeResponse",
+ "serializable": false
+ }
+ }
+ }
+ },
+ {
+ "name": "OnRoomStatusChangeResponse",
+ "syncMethods": {
+ "off": {
+ "result": {
+ "type": "void"
+ }
+ }
+ }
+ },
+ {
+ "name": "Room",
+ "fields": {
+ "roomId": {
+ "type": "string",
+ "serializable": true
+ },
+ "messages": {
+ "type": "Messages",
+ "serializable": false
+ },
+ "presence": {
+ "type": "Presence",
+ "serializable": false
+ },
+ "reactions": {
+ "type": "RoomReactions",
+ "serializable": false
+ },
+ "typing": {
+ "type": "Typing",
+ "serializable": false
+ },
+ "occupancy": {
+ "type": "Occupancy",
+ "serializable": false
+ },
+ "status": {
+ "type": "RoomStatus",
+ "serializable": false
+ }
+ },
+ "syncMethods": {
+ "options": {
+ "result": {
+ "type": "RoomOptions",
+ "serializable": true
+ }
+ }
+ },
+ "asyncMethods": {
+ "attach": {
+ "result": {
+ "type": "void"
+ }
+ },
+ "detach": {
+ "result": {
+ "type": "void"
+ }
+ }
+ }
+ },
+ {
+ "name": "Rooms",
+ "fields": {
+ "clientOptions": {
+ "type": "ClientOptions",
+ "serializable": true
+ }
+ },
+ "syncMethods": {
+ "get": {
+ "args": {
+ "roomId": {
+ "type": "string",
+ "serializable": true
+ },
+ "options": {
+ "type": "RoomOptions",
+ "serializable": true
+ }
+ },
+ "result": {
+ "type": "Room",
+ "serializable": false
+ }
+ }
+ },
+ "asyncMethods": {
+ "release": {
+ "args": {
+ "roomId": {
+ "type": "string",
+ "serializable": true
+ }
+ },
+ "result": {
+ "type": "void"
+ }
+ }
+ }
+ },
+ {
+ "name": "Typing",
+ "fields": {
+ "channel": {
+ "type": "RealtimeChannel",
+ "serializable": false
+ }
+ },
+ "syncMethods": {
+ "unsubscribeAll": {
+ "result": {
+ "type": "void"
+ }
+ }
+ },
+ "asyncMethods": {
+ "get": {
+ "result": {
+ "type": "string",
+ "serializable": true,
+ "array": true
+ }
+ },
+ "start": {
+ "result": {
+ "type": "void"
+ }
+ },
+ "stop": {
+ "result": {
+ "type": "void"
+ }
+ }
+ },
+ "listeners": {
+ "subscribe": {
+ "args": {
+ "listener": {
+ "type": "callback",
+ "args": {
+ "event": {
+ "type": "TypingEvent",
+ "serializable": false
+ }
+ },
+ "result": {
+ "type": "void"
+ }
+ }
+ },
+ "result": {
+ "type": "TypingSubscriptionResponse",
+ "serializable": false
+ }
+ },
+ "onDiscontinuity": {
+ "args": {
+ "listener": {
+ "type": "callback",
+ "args": {
+ "reason": {
+ "type": "AblyErrorInfo",
+ "serializable": true,
+ "optional": true
+ }
+ },
+ "result": {
+ "type": "void"
+ }
+ }
+ },
+ "result": {
+ "type": "OnDiscontinuitySubscriptionResponse",
+ "serializable": false
+ }
+ }
+ }
+ },
+ {
+ "name": "TypingSubscriptionResponse",
+ "syncMethods": {
+ "unsubscribe": {
+ "result": {
+ "type": "void"
+ }
+ }
+ }
+ }
+]
+"""
+}
diff --git a/UTSChatAdapter/UTSChatAdapter/Utils.swift b/UTSChatAdapter/UTSChatAdapter/Utils.swift
new file mode 100644
index 00000000..da5f7c97
--- /dev/null
+++ b/UTSChatAdapter/UTSChatAdapter/Utils.swift
@@ -0,0 +1,276 @@
+import Ably
+import AblyChat
+
+typealias JSON = [String: Any]
+
+extension JSON {
+ var name: String? { self["name"] as? String }
+ var type: String? { self["type"] as? String }
+ var args: JSON? { self["args"] as? JSON }
+ var params: JSON? { self["params"] as? JSON }
+ var result: JSON { self["result"] as! JSON }
+ var isSerializable: Bool { self["serializable"] as? Bool ?? false }
+ var isOptional: Bool { self["optional"] as? Bool ?? false }
+ var constructor: JSON? { self["konstructor"] as? JSON }
+ var fields: JSON? { self["fields"] as? JSON }
+ var syncMethods: JSON? { self["syncMethods"] as? JSON }
+ var asyncMethods: JSON? { self["asyncMethods"] as? JSON }
+ var listeners: JSON? { self["listeners"] as? JSON }
+ var listener: JSON? { self["listener"] as? JSON }
+ var callback: JSON? { args?.listener }
+
+ var methodArgs: JSON { params!.args! }
+ var refId: String { params!["refId"] as! String }
+ var callbackId: String { self["callbackId"] as! String }
+ var requestId: String { self["id"] as! String }
+}
+
+func jsonRpcResult(_ id: String, _ result: String) -> String {
+ "{\"jsonrpc\":\"2.0\",\"id\":\"\(id)\",\"result\":\(result)}"
+}
+
+func jsonRpcCallback(_ callbackId: String, _ message: String) -> String {
+ "{\"jsonrpc\":\"2.0\",\"id\":\"\(UUID().uuidString)\",\"method\":\"callback\",\"params\":{\"callbackId\":\"\(callbackId)\",\"args\":[\(message)]}}"
+}
+
+func jsonFromWebSocketMessage(_ message: URLSessionWebSocketTask.Message) -> JSON? {
+ var json: [String: Any]?
+
+ do {
+ switch message {
+ case .data(let data):
+ json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
+ case .string(let string):
+ json = try JSONSerialization.jsonObject(with: string.data(using: .utf8)!) as? JSON
+ @unknown default:
+ print("Unknown Websocket data.")
+ return nil
+ }
+ } catch {
+ print("Error parsing JSON: \(error)")
+ return nil
+ }
+
+ guard let json else {
+ print("Data provided is not a valid JSON dictionary.")
+ return nil
+ }
+
+ print("Received: \(json)")
+
+ if json["method"] == nil || json["jsonrpc"] == nil {
+ print("No valid fields in the provided JSON were found.")
+ return nil
+ }
+ return json
+}
+
+func generateId() -> String { NanoID.new() }
+
+protocol JsonSerialisable {
+ func jsonString() -> String
+}
+
+extension CommandLine {
+ static func hasParam(_ name: String) -> Bool {
+ arguments.contains(where: { $0 == name })
+ }
+}
+
+extension StringProtocol {
+ func firstLowercased() -> String { prefix(1).lowercased() + dropFirst() }
+ func firstUppercased() -> String { prefix(1).uppercased() + dropFirst() }
+}
+
+extension JSON {
+ func sortedByKey() -> Array {
+ sorted {
+ $0.key > $1.key
+ }
+ }
+}
+
+extension ClientOptions: JsonSerialisable {
+ func jsonString() -> String {
+ "{\"logLevel\": \"\(logLevel ?? .info)\"}"
+ }
+}
+
+extension RoomOptions: JsonSerialisable {
+ func jsonString() -> String {
+ fatalError("Not implemented")
+ }
+}
+
+extension ErrorInfo: JsonSerialisable {
+ func jsonString() -> String {
+ "{\"error\": \"\(description())\"}"
+ }
+}
+
+extension OccupancyEvent: JsonSerialisable {
+ func jsonString() -> String {
+ fatalError("Not implemented")
+ }
+}
+
+extension Message: JsonSerialisable {
+ func jsonString() -> String {
+ fatalError("Not implemented")
+ }
+}
+
+extension ConnectionStatusChange: JsonSerialisable {
+ func jsonString() -> String {
+ fatalError("Not implemented")
+ }
+}
+
+extension RoomStatusChange: JsonSerialisable {
+ func jsonString() -> String {
+ fatalError("Not implemented")
+ }
+}
+
+extension TypingEvent: JsonSerialisable {
+ func jsonString() -> String {
+ fatalError("Not implemented")
+ }
+}
+
+extension Reaction: JsonSerialisable {
+ func jsonString() -> String {
+ fatalError("Not implemented")
+ }
+}
+
+extension PresenceEvent: JsonSerialisable {
+ func jsonString() -> String {
+ fatalError("Not implemented")
+ }
+}
+
+extension String: JsonSerialisable {
+ func jsonString() -> String {
+ self
+ }
+}
+
+func jsonString(_ value: Any) -> String {
+ if value is JsonSerialisable {
+ return (value as! JsonSerialisable).jsonString()
+ }
+ else if value is any Sequence {
+ return "[" + (value as! any Sequence).map { $0.jsonString() }.joined(separator: ",\n") + "]"
+ }
+ fatalError("Not implemented")
+}
+
+extension Message {
+ static func from(_ value: Any?) -> Self {
+ fatalError("Not implemented")
+ }
+}
+
+extension QueryOptions {
+ static func from(_ value: Any?) -> Self {
+ fatalError("Not implemented")
+ }
+}
+
+extension SendMessageParams {
+ static func from(_ value: Any?) -> Self {
+ fatalError("Not implemented")
+ }
+}
+
+extension String {
+ static func from(_ value: Any?) -> Self {
+ value as! String
+ }
+}
+
+extension RealtimePresenceParams {
+ static func from(_ value: Any?) -> Self {
+ fatalError("Not implemented")
+ }
+}
+
+extension SendReactionParams {
+ static func from(_ value: Any?) -> Self {
+ fatalError("Not implemented")
+ }
+}
+
+extension RoomOptions {
+ static func from(_ value: Any?) -> Self {
+ guard let json = value as? JSON else {
+ fatalError("Not compatible data for creating RoomOptions. Expected JSON.")
+ }
+ var presence = PresenceOptions()
+ presence.enter = (json["presence"] as! JSON)["enter"] as? Bool ?? false
+ presence.enter = (json["presence"] as! JSON)["subscribe"] as? Bool ?? false
+ var typing = TypingOptions()
+ if let timeoutMs = (json["typing"] as! JSON)["timeoutMs"] as? Double {
+ typing.timeout = timeoutMs / 1000
+ }
+ let reactions = RoomReactionsOptions()
+ let occupancy = OccupancyOptions()
+ return RoomOptions(presence: presence, typing: typing, reactions: reactions, occupancy: occupancy)
+ }
+}
+
+// This should be replaced with `LogLevel` conforming to `String`.
+extension LogLevel {
+ static func from(string: String) -> Self {
+ switch string {
+ case "trace": return .trace
+ case "debug": return .debug
+ case "info": return .info
+ case "warn": return .warn
+ case "error": return .error
+ case "silent": return .silent
+ default: return .debug
+ }
+ }
+}
+
+extension ClientOptions {
+ static func from(_ value: Any?) -> Self {
+ guard let json = value as? JSON else {
+ fatalError("Not compatible data for creating ClientOptions. Expected JSON.")
+ }
+ var options = ClientOptions()
+ options.logLevel = .from(string: json["logLevel"] as! String)
+ return options
+ }
+}
+
+extension ARTClientOptions {
+ static func from(_ value: Any?) -> ARTClientOptions {
+ guard let json = value as? JSON else {
+ fatalError("Not compatible data for creating ClientOptions. Expected JSON.")
+ }
+ let options = ARTClientOptions()
+ options.clientId = json["clientId"] as? String
+ options.environment = json["environment"] as! String
+ options.key = json["key"] as? String
+ options.logLevel = .init(rawValue: json["logLevel"] as! UInt) ?? .debug
+ options.token = json["token"] as? String
+ options.useBinaryProtocol = json["useBinaryProtocol"] as? Bool ?? false
+ options.useTokenAuth = json["useTokenAuth"] as? Bool ?? false
+ return options
+ }
+}
+
+extension PresenceDataWrapper {
+ static func from(_ value: Any?) -> PresenceData {
+ fatalError("Not implemented")
+ }
+}
+
+extension PresenceEventType {
+ static func from(_ value: Any?) -> Self {
+ fatalError("Not implemented")
+ }
+}
diff --git a/UTSChatAdapter/UTSChatAdapter/WebSocketWrapper.swift b/UTSChatAdapter/UTSChatAdapter/WebSocketWrapper.swift
new file mode 100644
index 00000000..9d2495b8
--- /dev/null
+++ b/UTSChatAdapter/UTSChatAdapter/WebSocketWrapper.swift
@@ -0,0 +1,41 @@
+import Foundation
+
+final class WebSocketWrapper: NSObject, URLSessionWebSocketDelegate {
+
+ private var webSocket: URLSessionWebSocketTask!
+
+ func start(onMessage: @escaping (URLSessionWebSocketTask.Message) async throws -> Void) async throws {
+ let session = URLSession(configuration: .default, delegate: self, delegateQueue: .current)
+ let url = URL(string: "ws://localhost:3000")!
+
+ self.webSocket = session.webSocketTask(with: url)
+ self.webSocket.resume()
+
+ while !Task.isCancelled {
+ do {
+ try await onMessage(webSocket.receive())
+ } catch {
+ print("Can't connect to \(url): \(error.localizedDescription)")
+ sleep(5) // try again in 5 seconds
+ }
+ }
+ }
+
+ func send(text: String) {
+ print("Send: \(text)")
+ webSocket.send(URLSessionWebSocketTask.Message.string(text)) { error in
+ print(error == nil ? "Message sent" : "Error sending message: \(error!)")
+ }
+ }
+
+ // MARK: URLSessionWebSocketDelegate
+
+ func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?) {
+ print("Connected to server")
+ send(text: "{\"role\":\"ADAPTER\"}")
+ }
+
+ func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
+ print("Disconnected from server")
+ }
+}
diff --git a/UTSChatAdapter/UTSChatAdapter/main.swift b/UTSChatAdapter/UTSChatAdapter/main.swift
new file mode 100644
index 00000000..a9c71d3e
--- /dev/null
+++ b/UTSChatAdapter/UTSChatAdapter/main.swift
@@ -0,0 +1,39 @@
+import Foundation
+
+func serve() async throws {
+ let webSocket = WebSocketWrapper()
+ let adapter = ChatAdapter(webSocket: webSocket)
+
+ try await webSocket.start { message in
+ var result: String? = nil
+
+ guard let params = jsonFromWebSocketMessage(message) else {
+ print("Websocket message can't be processed.")
+ return
+ }
+ result = try await adapter.handleRpcCall(rpcParams: params)
+
+ if result != nil {
+ webSocket.send(text: result!)
+ }
+ }
+}
+
+if CommandLine.hasParam("generate") {
+ print("Generating swift code...")
+ ChatAdapterGenerator().generate()
+}
+else {
+ let task = Task {
+ do {
+ try await serve()
+ } catch {
+ print("Exiting due to fatal error: \(error)") // TODO: replace with logger
+ }
+ }
+ print("Waiting adapter to connect...")
+ print("Type 0 to exit:")
+ if readLine() == "0" {
+ task.cancel()
+ }
+}