From 5557b0cb759e526f0101167e20596adc3e43cc40 Mon Sep 17 00:00:00 2001 From: Marat Al Date: Sun, 13 Oct 2024 23:51:41 +0200 Subject: [PATCH 1/3] WIP: Initial implementation of generator (except callback generation). --- AblyChat.xcworkspace/contents.xcworkspacedata | 3 + .../UTSChatAdapter.xcodeproj/project.pbxproj | 323 ++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/swiftpm/Package.resolved | 51 + .../UTSChatAdapter/ChatAdapter.swift | 59 ++ .../UTSChatAdapter/ChatAdapterGenerator.swift | 250 +++++ .../UTSChatAdapter/Schema+Adjustments.swift | 138 +++ UTSChatAdapter/UTSChatAdapter/Schema.swift | 994 ++++++++++++++++++ UTSChatAdapter/UTSChatAdapter/Utils.swift | 230 ++++ .../UTSChatAdapter/WebSocketWrapper.swift | 41 + UTSChatAdapter/UTSChatAdapter/main.swift | 39 + 11 files changed, 2135 insertions(+) create mode 100644 UTSChatAdapter/UTSChatAdapter.xcodeproj/project.pbxproj create mode 100644 UTSChatAdapter/UTSChatAdapter.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 UTSChatAdapter/UTSChatAdapter.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 UTSChatAdapter/UTSChatAdapter/ChatAdapter.swift create mode 100644 UTSChatAdapter/UTSChatAdapter/ChatAdapterGenerator.swift create mode 100644 UTSChatAdapter/UTSChatAdapter/Schema+Adjustments.swift create mode 100644 UTSChatAdapter/UTSChatAdapter/Schema.swift create mode 100644 UTSChatAdapter/UTSChatAdapter/Utils.swift create mode 100644 UTSChatAdapter/UTSChatAdapter/WebSocketWrapper.swift create mode 100644 UTSChatAdapter/UTSChatAdapter/main.swift diff --git a/AblyChat.xcworkspace/contents.xcworkspacedata b/AblyChat.xcworkspace/contents.xcworkspacedata index 56985fe..806958d 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 0000000..6f22e97 --- /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 0000000..919434a --- /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 0000000..605b791 --- /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 0000000..0e3a28a --- /dev/null +++ b/UTSChatAdapter/UTSChatAdapter/ChatAdapter.swift @@ -0,0 +1,59 @@ +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: + print("Unknown method provided.") + return nil + } + } +} diff --git a/UTSChatAdapter/UTSChatAdapter/ChatAdapterGenerator.swift b/UTSChatAdapter/UTSChatAdapter/ChatAdapterGenerator.swift new file mode 100644 index 0000000..64f5a6f --- /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[\"\($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[\"\($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\\": \\"\\(JSON.from(\(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\\": \\"\\(JSON.from(\(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[\"\(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, "\\(param.json())")) + } else { + self.webSocket.send(text: jsonRpcCallback(rpcParams.callbackId, "{}")) + }\n + """ + } else { + result += + """ + self.webSocket.send(text: jsonRpcCallback(rpcParams.callbackId, "\\($0.json())"))\n + """ + } + result += + """ + } + Task { + for await \(paramsUsage) in subscription { + callback(\(paramsUsage)) + } + }\n + """ + return result + } +} diff --git a/UTSChatAdapter/UTSChatAdapter/Schema+Adjustments.swift b/UTSChatAdapter/UTSChatAdapter/Schema+Adjustments.swift new file mode 100644 index 0000000..ce066b1 --- /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 0000000..ccbd5fd --- /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 0000000..8bc761b --- /dev/null +++ b/UTSChatAdapter/UTSChatAdapter/Utils.swift @@ -0,0 +1,230 @@ +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 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 refId: String { self["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 { UUID().uuidString.replacingOccurrences(of: "-", with: "") } + +protocol JsonSerialisable { + func json() -> Any +} + +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 json() -> Any { + ["logLevel": logLevel ?? .info] + } +} + +extension RoomOptions: JsonSerialisable { + func json() -> Any { + fatalError("Not implemented") + } +} + +extension ErrorInfo: JsonSerialisable { + func json() -> Any { + ["error": description()] + } +} + +extension Set: JsonSerialisable { + func json() -> Any { + Array(self) + } +} + +extension OccupancyEvent: JsonSerialisable { + func json() -> Any { + fatalError("Not implemented") + } +} + +extension Message: JsonSerialisable { + func json() -> Any { + fatalError("Not implemented") + } +} + +extension ConnectionStatusChange: JsonSerialisable { + func json() -> Any { + fatalError("Not implemented") + } +} + +extension RoomStatusChange: JsonSerialisable { + func json() -> Any { + fatalError("Not implemented") + } +} + +extension TypingEvent: JsonSerialisable { + func json() -> Any { + fatalError("Not implemented") + } +} + +extension Reaction: JsonSerialisable { + func json() -> Any { + fatalError("Not implemented") + } +} + +extension PresenceEvent: JsonSerialisable { + func json() -> Any { + fatalError("Not implemented") + } +} + +extension JSON { + static func from(_ value: Any) -> Self { + if value is JsonSerialisable { + return (value as! JsonSerialisable).json() as! JSON + } + 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 { + fatalError("Not implemented") + } +} + +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 { + fatalError("Not implemented") + } +} + +extension ClientOptions { + static func from(_ value: Any?) -> Self { + fatalError("Not implemented") + } +} + +extension ARTClientOptions { + static func from(_ value: Any?) -> Self { + fatalError("Not implemented") + } +} + +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 0000000..399d267 --- /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\":\"IMPLEMENTATION\"}") + } + + 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 0000000..a9c71d3 --- /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() + } +} From 26c9c60ecb8bfc6b815338fc3cd98e5f12d92538 Mon Sep 17 00:00:00 2001 From: Marat Al Date: Sun, 27 Oct 2024 15:41:30 +0100 Subject: [PATCH 2/3] Added some custom implementations. --- .../UTSChatAdapter/ChatAdapter.swift | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/UTSChatAdapter/UTSChatAdapter/ChatAdapter.swift b/UTSChatAdapter/UTSChatAdapter/ChatAdapter.swift index 0e3a28a..987bef1 100644 --- a/UTSChatAdapter/UTSChatAdapter/ChatAdapter.swift +++ b/UTSChatAdapter/UTSChatAdapter/ChatAdapter.swift @@ -50,6 +50,65 @@ class ChatAdapter { // GENERATED CONTENT BEGIN // ... // GENERATED CONTENT END + + // Custom fields implementation (see `Schema.skipPaths` for reasons): + + case "ChatClient": + let requestId = rpcParams["id"] as! String + let chatOptions = ClientOptions.from(rpcParams["clientOptions"]) + let realtimeOptions = ARTClientOptions.from(rpcParams["realtimeClientOptions"]) + let realtime = ARTRealtime(options: realtimeOptions) + let chatClient = DefaultChatClient(realtime: realtime, clientOptions: chatOptions) + let instanceId = generateId() + idToChatClient[instanceId] = chatClient + return jsonRpcResult(requestId, "{\"instanceId\":\"\(instanceId)\"}") + + // This field is optional and should be included in a corresponding json schema for automatic generation + case "ConnectionStatus#error": + let refId = rpcParams["refId"] as! String + guard let instance = idToConnectionStatus[refId] else { + print("Instance with this `refId` doesn't exist."); return nil; + } + if let error = instance.error { // ErrorInfo + return jsonRpcResult(rpcParams["id"] as! String, "{\"response\": \"\(error.json())\"}") + } else { + return jsonRpcResult(rpcParams["id"] as! String, "{\"response\": \(NSNull()) }") + } + + // This field is optional and should be included in a corresponding json schema for automatic generation + case "Message#createdAt": + let refId = rpcParams["refId"] as! String + guard let message = idToMessage[refId] else { + print("Message with `refId == \(refId)` doesn't exist.") + return nil + } + if let createdAt = message.createdAt { // number + return jsonRpcResult(rpcParams["id"] as! String, "{\"response\": \"\(createdAt)\"}") + } else { + return jsonRpcResult(rpcParams["id"] as! String, "{\"response\": \(NSNull()) }") + } + + // `events` is an array of strings in schema file which is not enougth for param auto-generation (should be `PresenceEventType`) + case "Presence.subscribe_eventsAndListener": + guard let events = rpcParams["events"] as? [String] else { + return nil + } + guard let presenceRef = idToPresence[rpcParams.refId] else { + print("Presence with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let subscription = await presenceRef.subscribe(events: events.map { PresenceEventType.from($0) }) + let callback: (PresenceEvent) -> Void = { + self.webSocket.send(text: jsonRpcCallback(rpcParams.callbackId, "\($0.json())")) + } + Task { + for await event in subscription { + callback(event) + } + } + let resultRefId = generateId() + idToPresenceSubscription[resultRefId] = subscription + return jsonRpcResult(rpcParams.requestId, "{\"refId\":\"\(resultRefId)\"}") default: print("Unknown method provided.") From 366ecb12f14dca874c990e5411dc5bcf535203ad Mon Sep 17 00:00:00 2001 From: Marat Al Date: Sun, 27 Oct 2024 15:41:45 +0100 Subject: [PATCH 3/3] Added generated implementations. --- .../UTSChatAdapter/ChatAdapter.swift | 719 +++++++++++++++++- 1 file changed, 718 insertions(+), 1 deletion(-) diff --git a/UTSChatAdapter/UTSChatAdapter/ChatAdapter.swift b/UTSChatAdapter/UTSChatAdapter/ChatAdapter.swift index 987bef1..63c8f58 100644 --- a/UTSChatAdapter/UTSChatAdapter/ChatAdapter.swift +++ b/UTSChatAdapter/UTSChatAdapter/ChatAdapter.swift @@ -48,7 +48,724 @@ class ChatAdapter { switch method { // GENERATED CONTENT BEGIN - // ... + + case "ChatClient#rooms": + guard let chatClientRef = idToChatClient[rpcParams.refId] else { + print("ChatClient with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let rooms = chatClientRef.rooms // Rooms + let fieldRefId = generateId() + idToRooms[fieldRefId] = rooms + return jsonRpcResult(rpcParams.requestId, "{\"refId\":\"\(fieldRefId)\"}") + + case "ChatClient#realtime": + guard let chatClientRef = idToChatClient[rpcParams.refId] else { + print("ChatClient with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let realtime = chatClientRef.realtime // Realtime + let fieldRefId = generateId() + idToRealtime[fieldRefId] = realtime + return jsonRpcResult(rpcParams.requestId, "{\"refId\":\"\(fieldRefId)\"}") + + case "ChatClient#connection": + guard let chatClientRef = idToChatClient[rpcParams.refId] else { + print("ChatClient with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let connection = chatClientRef.connection // Connection + let fieldRefId = generateId() + idToConnection[fieldRefId] = connection + return jsonRpcResult(rpcParams.requestId, "{\"refId\":\"\(fieldRefId)\"}") + + case "ChatClient#clientOptions": + guard let chatClientRef = idToChatClient[rpcParams.refId] else { + print("ChatClient with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let clientOptions = chatClientRef.clientOptions // ClientOptions + return jsonRpcResult(rpcParams.requestId, "{\"response\": \"\(JSON.from(clientOptions))\"}") + + case "ChatClient#clientId": + guard let chatClientRef = idToChatClient[rpcParams.refId] else { + print("ChatClient with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let clientID = chatClientRef.clientID // string + return jsonRpcResult(rpcParams.requestId, "{\"response\": \"\(clientID)\"}") + + case "ConnectionStatus#current": + guard let connectionStatusRef = idToConnectionStatus[rpcParams.refId] else { + print("ConnectionStatus with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let current = connectionStatusRef.current // string + return jsonRpcResult(rpcParams.requestId, "{\"response\": \"\(current)\"}") + + case "ConnectionStatus.onChange": + guard let connectionStatusRef = idToConnectionStatus[rpcParams.refId] else { + print("ConnectionStatus with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let subscription = connectionStatusRef.onChange(bufferingPolicy: .unbounded) + let callback: (ConnectionStatusChange) -> Void = { + self.webSocket.send(text: jsonRpcCallback(rpcParams.callbackId, "\($0.json())")) + } + Task { + for await change in subscription { + callback(change) + } + } + let resultRefId = generateId() + idToOnConnectionStatusChange[resultRefId] = subscription + return jsonRpcResult(rpcParams.requestId, "{\"refId\":\"\(resultRefId)\"}") + + case "Connection#status": + guard let connectionRef = idToConnection[rpcParams.refId] else { + print("Connection with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let status = connectionRef.status // ConnectionStatus + let fieldRefId = generateId() + idToConnectionStatus[fieldRefId] = status + return jsonRpcResult(rpcParams.requestId, "{\"refId\":\"\(fieldRefId)\"}") + + case "Message.equal": + let message = Message.from(rpcParams["message"]) + guard let messageRef = idToMessage[rpcParams.refId] else { + print("Message with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let bool = try messageRef.equal(message: message) // Bool + return jsonRpcResult(rpcParams.requestId, "{\"response\": \"\(bool)\"}") + + case "Message.before": + let message = Message.from(rpcParams["message"]) + guard let messageRef = idToMessage[rpcParams.refId] else { + print("Message with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let bool = try messageRef.before(message: message) // Bool + return jsonRpcResult(rpcParams.requestId, "{\"response\": \"\(bool)\"}") + + case "Message.after": + let message = Message.from(rpcParams["message"]) + guard let messageRef = idToMessage[rpcParams.refId] else { + print("Message with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let bool = try messageRef.after(message: message) // Bool + return jsonRpcResult(rpcParams.requestId, "{\"response\": \"\(bool)\"}") + + case "Message#timeserial": + guard let messageRef = idToMessage[rpcParams.refId] else { + print("Message with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let timeserial = messageRef.timeserial // string + return jsonRpcResult(rpcParams.requestId, "{\"response\": \"\(timeserial)\"}") + + case "Message#text": + guard let messageRef = idToMessage[rpcParams.refId] else { + print("Message with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let text = messageRef.text // string + return jsonRpcResult(rpcParams.requestId, "{\"response\": \"\(text)\"}") + + case "Message#roomId": + guard let messageRef = idToMessage[rpcParams.refId] else { + print("Message with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let roomID = messageRef.roomID // string + return jsonRpcResult(rpcParams.requestId, "{\"response\": \"\(roomID)\"}") + + case "Message#metadata": + guard let messageRef = idToMessage[rpcParams.refId] else { + print("Message with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let metadata = messageRef.metadata // object + return jsonRpcResult(rpcParams.requestId, "{\"response\": \"\(JSON.from(metadata))\"}") + + case "Message#headers": + guard let messageRef = idToMessage[rpcParams.refId] else { + print("Message with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let headers = messageRef.headers // object + return jsonRpcResult(rpcParams.requestId, "{\"response\": \"\(JSON.from(headers))\"}") + + case "Message#clientId": + guard let messageRef = idToMessage[rpcParams.refId] else { + print("Message with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let clientID = messageRef.clientID // string + return jsonRpcResult(rpcParams.requestId, "{\"response\": \"\(clientID)\"}") + + case "Messages.send": + let options = SendMessageParams.from(rpcParams["options"]) + guard let messagesRef = idToMessages[rpcParams.refId] else { + print("Messages with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let message = try await messagesRef.send(options: options) // Message + let resultRefId = generateId() + idToMessage[resultRefId] = message + return jsonRpcResult(rpcParams.requestId, "{\"refId\":\"\(resultRefId)\"}") + + case "Messages.get": + let options = QueryOptions.from(rpcParams["options"]) + guard let messagesRef = idToMessages[rpcParams.refId] else { + print("Messages with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let paginatedResultMessage = try await messagesRef.get(options: options) // PaginatedResultMessage + let resultRefId = generateId() + idToPaginatedResultMessage[resultRefId] = paginatedResultMessage + return jsonRpcResult(rpcParams.requestId, "{\"refId\":\"\(resultRefId)\"}") + + case "Messages#channel": + guard let messagesRef = idToMessages[rpcParams.refId] else { + print("Messages with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let channel = messagesRef.channel // RealtimeChannel + let fieldRefId = generateId() + idToRealtimeChannel[fieldRefId] = channel + return jsonRpcResult(rpcParams.requestId, "{\"refId\":\"\(fieldRefId)\"}") + + case "Messages.subscribe": + guard let messagesRef = idToMessages[rpcParams.refId] else { + print("Messages with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let subscription = try await messagesRef.subscribe(bufferingPolicy: .unbounded) + let callback: (Message) -> Void = { + self.webSocket.send(text: jsonRpcCallback(rpcParams.callbackId, "\($0.json())")) + } + Task { + for await event in subscription { + callback(event) + } + } + let resultRefId = generateId() + idToMessageSubscription[resultRefId] = subscription + return jsonRpcResult(rpcParams.requestId, "{\"refId\":\"\(resultRefId)\"}") + + case "Messages.onDiscontinuity": + guard let messagesRef = idToMessages[rpcParams.refId] else { + print("Messages with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let subscription = await messagesRef.subscribeToDiscontinuities() + let callback: (AblyErrorInfo?) -> Void = { + if let param = $0 { + self.webSocket.send(text: jsonRpcCallback(rpcParams.callbackId, "\(param.json())")) + } else { + self.webSocket.send(text: jsonRpcCallback(rpcParams.callbackId, "{}")) + } + } + Task { + for await reason in subscription { + callback(reason) + } + } + let resultRefId = generateId() + idToOnDiscontinuitySubscription[resultRefId] = subscription + return jsonRpcResult(rpcParams.requestId, "{\"refId\":\"\(resultRefId)\"}") + + case "MessageSubscriptionResponse.getPreviousMessages": + let params = QueryOptions.from(rpcParams["params"]) + guard let messageSubscriptionRef = idToMessageSubscription[rpcParams.refId] else { + print("MessageSubscription with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let paginatedResultMessage = try await messageSubscriptionRef.getPreviousMessages(params: params) // PaginatedResultMessage + let resultRefId = generateId() + idToPaginatedResultMessage[resultRefId] = paginatedResultMessage + return jsonRpcResult(rpcParams.requestId, "{\"refId\":\"\(resultRefId)\"}") + + case "Occupancy.get": + guard let occupancyRef = idToOccupancy[rpcParams.refId] else { + print("Occupancy with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let occupancyEvent = try await occupancyRef.get() // OccupancyEvent + return jsonRpcResult(rpcParams.requestId, "{\"response\": \"\(JSON.from(occupancyEvent))\"}") + + case "Occupancy#channel": + guard let occupancyRef = idToOccupancy[rpcParams.refId] else { + print("Occupancy with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let channel = occupancyRef.channel // RealtimeChannel + let fieldRefId = generateId() + idToRealtimeChannel[fieldRefId] = channel + return jsonRpcResult(rpcParams.requestId, "{\"refId\":\"\(fieldRefId)\"}") + + case "Occupancy.subscribe": + guard let occupancyRef = idToOccupancy[rpcParams.refId] else { + print("Occupancy with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let subscription = await occupancyRef.subscribe(bufferingPolicy: .unbounded) + let callback: (OccupancyEvent) -> Void = { + self.webSocket.send(text: jsonRpcCallback(rpcParams.callbackId, "\($0.json())")) + } + Task { + for await event in subscription { + callback(event) + } + } + let resultRefId = generateId() + idToOccupancySubscription[resultRefId] = subscription + return jsonRpcResult(rpcParams.requestId, "{\"refId\":\"\(resultRefId)\"}") + + case "Occupancy.onDiscontinuity": + guard let occupancyRef = idToOccupancy[rpcParams.refId] else { + print("Occupancy with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let subscription = await occupancyRef.subscribeToDiscontinuities() + let callback: (AblyErrorInfo?) -> Void = { + if let param = $0 { + self.webSocket.send(text: jsonRpcCallback(rpcParams.callbackId, "\(param.json())")) + } else { + self.webSocket.send(text: jsonRpcCallback(rpcParams.callbackId, "{}")) + } + } + Task { + for await reason in subscription { + callback(reason) + } + } + let resultRefId = generateId() + idToOnDiscontinuitySubscription[resultRefId] = subscription + return jsonRpcResult(rpcParams.requestId, "{\"refId\":\"\(resultRefId)\"}") + + case "PaginatedResult.isLast": + guard let paginatedResultRef = idToPaginatedResult[rpcParams.refId] else { + print("PaginatedResult with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let bool = paginatedResultRef.isLast() // Bool + return jsonRpcResult(rpcParams.requestId, "{\"response\": \"\(bool)\"}") + + case "PaginatedResult.hasNext": + guard let paginatedResultRef = idToPaginatedResult[rpcParams.refId] else { + print("PaginatedResult with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let bool = paginatedResultRef.hasNext() // Bool + return jsonRpcResult(rpcParams.requestId, "{\"response\": \"\(bool)\"}") + + case "PaginatedResult.next": + guard let paginatedResultRef = idToPaginatedResult[rpcParams.refId] else { + print("PaginatedResult with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let paginatedResult = try await paginatedResultRef.next() // PaginatedResult + let resultRefId = generateId() + idToPaginatedResult[resultRefId] = paginatedResult + return jsonRpcResult(rpcParams.requestId, "{\"refId\":\"\(resultRefId)\"}") + + case "PaginatedResult.first": + guard let paginatedResultRef = idToPaginatedResult[rpcParams.refId] else { + print("PaginatedResult with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let paginatedResult = try await paginatedResultRef.first() // PaginatedResult + let resultRefId = generateId() + idToPaginatedResult[resultRefId] = paginatedResult + return jsonRpcResult(rpcParams.requestId, "{\"refId\":\"\(resultRefId)\"}") + + case "PaginatedResult.current": + guard let paginatedResultRef = idToPaginatedResult[rpcParams.refId] else { + print("PaginatedResult with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let paginatedResult = try await paginatedResultRef.current() // PaginatedResult + let resultRefId = generateId() + idToPaginatedResult[resultRefId] = paginatedResult + return jsonRpcResult(rpcParams.requestId, "{\"refId\":\"\(resultRefId)\"}") + + case "PaginatedResult#items": + guard let paginatedResultRef = idToPaginatedResult[rpcParams.refId] else { + print("PaginatedResult with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let items = paginatedResultRef.items // object + return jsonRpcResult(rpcParams.requestId, "{\"response\": \"\(JSON.from(items))\"}") + + case "Presence.update": + let data = PresenceDataWrapper.from(rpcParams["data"]) + guard let presenceRef = idToPresence[rpcParams.refId] else { + print("Presence with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + try await presenceRef.update(data: data) // Void + return jsonRpcResult(rpcParams.requestId, "{}") + + case "Presence.leave": + let data = PresenceDataWrapper.from(rpcParams["data"]) + guard let presenceRef = idToPresence[rpcParams.refId] else { + print("Presence with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + try await presenceRef.leave(data: data) // Void + return jsonRpcResult(rpcParams.requestId, "{}") + + case "Presence.isUserPresent": + let clientID = String.from(rpcParams["clientId"]) + guard let presenceRef = idToPresence[rpcParams.refId] else { + print("Presence with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let bool = try await presenceRef.isUserPresent(clientID: clientID) // Bool + return jsonRpcResult(rpcParams.requestId, "{\"response\": \"\(bool)\"}") + + case "Presence.get": + let params = RealtimePresenceParams.from(rpcParams["params"]) + guard let presenceRef = idToPresence[rpcParams.refId] else { + print("Presence with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let presenceMember = try await presenceRef.get(params: params) // PresenceMember + return jsonRpcResult(rpcParams.requestId, "{\"response\": \"\(JSON.from(presenceMember))\"}") + + case "Presence.enter": + let data = PresenceDataWrapper.from(rpcParams["data"]) + guard let presenceRef = idToPresence[rpcParams.refId] else { + print("Presence with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + try await presenceRef.enter(data: data) // Void + return jsonRpcResult(rpcParams.requestId, "{}") + + case "Presence.subscribe_listener": + guard let presenceRef = idToPresence[rpcParams.refId] else { + print("Presence with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let subscription = await presenceRef.subscribeAll() + let callback: (PresenceEvent) -> Void = { + self.webSocket.send(text: jsonRpcCallback(rpcParams.callbackId, "\($0.json())")) + } + Task { + for await event in subscription { + callback(event) + } + } + let resultRefId = generateId() + idToPresenceSubscription[resultRefId] = subscription + return jsonRpcResult(rpcParams.requestId, "{\"refId\":\"\(resultRefId)\"}") + + case "Presence.onDiscontinuity": + guard let presenceRef = idToPresence[rpcParams.refId] else { + print("Presence with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let subscription = await presenceRef.subscribeToDiscontinuities() + let callback: (AblyErrorInfo?) -> Void = { + if let param = $0 { + self.webSocket.send(text: jsonRpcCallback(rpcParams.callbackId, "\(param.json())")) + } else { + self.webSocket.send(text: jsonRpcCallback(rpcParams.callbackId, "{}")) + } + } + Task { + for await reason in subscription { + callback(reason) + } + } + let resultRefId = generateId() + idToOnDiscontinuitySubscription[resultRefId] = subscription + return jsonRpcResult(rpcParams.requestId, "{\"refId\":\"\(resultRefId)\"}") + + case "RoomReactions.send": + let params = SendReactionParams.from(rpcParams["params"]) + guard let roomReactionsRef = idToRoomReactions[rpcParams.refId] else { + print("RoomReactions with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + try await roomReactionsRef.send(params: params) // Void + return jsonRpcResult(rpcParams.requestId, "{}") + + case "RoomReactions#channel": + guard let roomReactionsRef = idToRoomReactions[rpcParams.refId] else { + print("RoomReactions with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let channel = roomReactionsRef.channel // RealtimeChannel + let fieldRefId = generateId() + idToRealtimeChannel[fieldRefId] = channel + return jsonRpcResult(rpcParams.requestId, "{\"refId\":\"\(fieldRefId)\"}") + + case "RoomReactions.subscribe": + guard let roomReactionsRef = idToRoomReactions[rpcParams.refId] else { + print("RoomReactions with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let subscription = await roomReactionsRef.subscribe(bufferingPolicy: .unbounded) + let callback: (Reaction) -> Void = { + self.webSocket.send(text: jsonRpcCallback(rpcParams.callbackId, "\($0.json())")) + } + Task { + for await reaction in subscription { + callback(reaction) + } + } + let resultRefId = generateId() + idToRoomReactionsSubscription[resultRefId] = subscription + return jsonRpcResult(rpcParams.requestId, "{\"refId\":\"\(resultRefId)\"}") + + case "RoomReactions.onDiscontinuity": + guard let roomReactionsRef = idToRoomReactions[rpcParams.refId] else { + print("RoomReactions with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let subscription = await roomReactionsRef.subscribeToDiscontinuities() + let callback: (AblyErrorInfo?) -> Void = { + if let param = $0 { + self.webSocket.send(text: jsonRpcCallback(rpcParams.callbackId, "\(param.json())")) + } else { + self.webSocket.send(text: jsonRpcCallback(rpcParams.callbackId, "{}")) + } + } + Task { + for await reason in subscription { + callback(reason) + } + } + let resultRefId = generateId() + idToOnDiscontinuitySubscription[resultRefId] = subscription + return jsonRpcResult(rpcParams.requestId, "{\"refId\":\"\(resultRefId)\"}") + + case "RoomStatus#current": + guard let roomStatusRef = idToRoomStatus[rpcParams.refId] else { + print("RoomStatus with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let current = await roomStatusRef.current // string + return jsonRpcResult(rpcParams.requestId, "{\"response\": \"\(current)\"}") + + case "RoomStatus.onChange": + guard let roomStatusRef = idToRoomStatus[rpcParams.refId] else { + print("RoomStatus with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let subscription = await roomStatusRef.onChange(bufferingPolicy: .unbounded) + let callback: (RoomStatusChange) -> Void = { + self.webSocket.send(text: jsonRpcCallback(rpcParams.callbackId, "\($0.json())")) + } + Task { + for await change in subscription { + callback(change) + } + } + let resultRefId = generateId() + idToOnRoomStatusChange[resultRefId] = subscription + return jsonRpcResult(rpcParams.requestId, "{\"refId\":\"\(resultRefId)\"}") + + case "Room.options": + guard let roomRef = idToRoom[rpcParams.refId] else { + print("Room with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let roomOptions = roomRef.options() // RoomOptions + return jsonRpcResult(rpcParams.requestId, "{\"response\": \"\(JSON.from(roomOptions))\"}") + + case "Room.detach": + guard let roomRef = idToRoom[rpcParams.refId] else { + print("Room with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + try await roomRef.detach() // Void + return jsonRpcResult(rpcParams.requestId, "{}") + + case "Room.attach": + guard let roomRef = idToRoom[rpcParams.refId] else { + print("Room with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + try await roomRef.attach() // Void + return jsonRpcResult(rpcParams.requestId, "{}") + + case "Room#typing": + guard let roomRef = idToRoom[rpcParams.refId] else { + print("Room with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let typing = roomRef.typing // Typing + let fieldRefId = generateId() + idToTyping[fieldRefId] = typing + return jsonRpcResult(rpcParams.requestId, "{\"refId\":\"\(fieldRefId)\"}") + + case "Room#status": + guard let roomRef = idToRoom[rpcParams.refId] else { + print("Room with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let status = roomRef.status // RoomStatus + let fieldRefId = generateId() + idToRoomStatus[fieldRefId] = status + return jsonRpcResult(rpcParams.requestId, "{\"refId\":\"\(fieldRefId)\"}") + + case "Room#roomId": + guard let roomRef = idToRoom[rpcParams.refId] else { + print("Room with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let roomID = roomRef.roomID // string + return jsonRpcResult(rpcParams.requestId, "{\"response\": \"\(roomID)\"}") + + case "Room#reactions": + guard let roomRef = idToRoom[rpcParams.refId] else { + print("Room with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let reactions = roomRef.reactions // RoomReactions + let fieldRefId = generateId() + idToRoomReactions[fieldRefId] = reactions + return jsonRpcResult(rpcParams.requestId, "{\"refId\":\"\(fieldRefId)\"}") + + case "Room#presence": + guard let roomRef = idToRoom[rpcParams.refId] else { + print("Room with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let presence = roomRef.presence // Presence + let fieldRefId = generateId() + idToPresence[fieldRefId] = presence + return jsonRpcResult(rpcParams.requestId, "{\"refId\":\"\(fieldRefId)\"}") + + case "Room#occupancy": + guard let roomRef = idToRoom[rpcParams.refId] else { + print("Room with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let occupancy = roomRef.occupancy // Occupancy + let fieldRefId = generateId() + idToOccupancy[fieldRefId] = occupancy + return jsonRpcResult(rpcParams.requestId, "{\"refId\":\"\(fieldRefId)\"}") + + case "Room#messages": + guard let roomRef = idToRoom[rpcParams.refId] else { + print("Room with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let messages = roomRef.messages // Messages + let fieldRefId = generateId() + idToMessages[fieldRefId] = messages + return jsonRpcResult(rpcParams.requestId, "{\"refId\":\"\(fieldRefId)\"}") + + case "Rooms.get": + let roomID = String.from(rpcParams["roomId"]) + let options = RoomOptions.from(rpcParams["options"]) + guard let roomsRef = idToRooms[rpcParams.refId] else { + print("Rooms with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let room = try await roomsRef.get(roomID: roomID, options: options) // Room + let resultRefId = generateId() + idToRoom[resultRefId] = room + return jsonRpcResult(rpcParams.requestId, "{\"refId\":\"\(resultRefId)\"}") + + case "Rooms.release": + let roomID = String.from(rpcParams["roomId"]) + guard let roomsRef = idToRooms[rpcParams.refId] else { + print("Rooms with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + try await roomsRef.release(roomID: roomID) // Void + return jsonRpcResult(rpcParams.requestId, "{}") + + case "Rooms#clientOptions": + guard let roomsRef = idToRooms[rpcParams.refId] else { + print("Rooms with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let clientOptions = roomsRef.clientOptions // ClientOptions + return jsonRpcResult(rpcParams.requestId, "{\"response\": \"\(JSON.from(clientOptions))\"}") + + case "Typing.stop": + guard let typingRef = idToTyping[rpcParams.refId] else { + print("Typing with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + try await typingRef.stop() // Void + return jsonRpcResult(rpcParams.requestId, "{}") + + case "Typing.start": + guard let typingRef = idToTyping[rpcParams.refId] else { + print("Typing with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + try await typingRef.start() // Void + return jsonRpcResult(rpcParams.requestId, "{}") + + case "Typing.get": + guard let typingRef = idToTyping[rpcParams.refId] else { + print("Typing with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let string = try await typingRef.get() // String + return jsonRpcResult(rpcParams.requestId, "{\"response\": \"\(string)\"}") + + case "Typing#channel": + guard let typingRef = idToTyping[rpcParams.refId] else { + print("Typing with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let channel = typingRef.channel // RealtimeChannel + let fieldRefId = generateId() + idToRealtimeChannel[fieldRefId] = channel + return jsonRpcResult(rpcParams.requestId, "{\"refId\":\"\(fieldRefId)\"}") + + case "Typing.subscribe": + guard let typingRef = idToTyping[rpcParams.refId] else { + print("Typing with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let subscription = await typingRef.subscribe(bufferingPolicy: .unbounded) + let callback: (TypingEvent) -> Void = { + self.webSocket.send(text: jsonRpcCallback(rpcParams.callbackId, "\($0.json())")) + } + Task { + for await event in subscription { + callback(event) + } + } + let resultRefId = generateId() + idToTypingSubscription[resultRefId] = subscription + return jsonRpcResult(rpcParams.requestId, "{\"refId\":\"\(resultRefId)\"}") + + case "Typing.onDiscontinuity": + guard let typingRef = idToTyping[rpcParams.refId] else { + print("Typing with `refId == \(rpcParams.refId)` doesn't exist.") + return nil + } + let subscription = await typingRef.subscribeToDiscontinuities() + let callback: (AblyErrorInfo?) -> Void = { + if let param = $0 { + self.webSocket.send(text: jsonRpcCallback(rpcParams.callbackId, "\(param.json())")) + } else { + self.webSocket.send(text: jsonRpcCallback(rpcParams.callbackId, "{}")) + } + } + Task { + for await reason in subscription { + callback(reason) + } + } + let resultRefId = generateId() + idToOnDiscontinuitySubscription[resultRefId] = subscription + return jsonRpcResult(rpcParams.requestId, "{\"refId\":\"\(resultRefId)\"}") + // GENERATED CONTENT END // Custom fields implementation (see `Schema.skipPaths` for reasons):