From ff78d187da904a50bd9829ec2f0e944c714ff6a2 Mon Sep 17 00:00:00 2001 From: Simon Pilkington Date: Wed, 4 Jan 2023 15:07:31 +1100 Subject: [PATCH 1/3] Add the ability to generate Swift Concurrency implementations. --- .../ModelClientDelegate.swift | 3 + .../ClientProtocolDelegate.swift | 50 +++++-- .../MockClientDelegate.swift | 58 +++++++-- ...iceModelCodeGenerator+generateClient.swift | 123 +++++++++++++++--- 4 files changed, 194 insertions(+), 40 deletions(-) diff --git a/Sources/ServiceModelCodeGeneration/ModelClientDelegate.swift b/Sources/ServiceModelCodeGeneration/ModelClientDelegate.swift index 2dd92ca..c0df22c 100644 --- a/Sources/ServiceModelCodeGeneration/ModelClientDelegate.swift +++ b/Sources/ServiceModelCodeGeneration/ModelClientDelegate.swift @@ -86,6 +86,8 @@ public protocol ModelClientDelegate { public enum ClientType { /// A protocol with the specified name case `protocol`(name: String) + /// A protocol with the specified name, also conforming to the specified protocol + case `protocolWithConformance`(name: String, conformingProtocolName: String) /// A struct with the specified name and conforming to the specified protocol case `struct`(name: String, genericParameters: [(typeName: String, conformingTypeName: String?)], conformingProtocolName: String) } @@ -111,6 +113,7 @@ public struct AsyncResultType { public enum InvokeType: String { case sync = "Sync" case async = "Async" + case asyncFunction = "AsyncFunction" } public extension ModelClientDelegate { diff --git a/Sources/ServiceModelGenerate/ClientProtocolDelegate.swift b/Sources/ServiceModelGenerate/ClientProtocolDelegate.swift index 53bb0b1..e12c2bc 100644 --- a/Sources/ServiceModelGenerate/ClientProtocolDelegate.swift +++ b/Sources/ServiceModelGenerate/ClientProtocolDelegate.swift @@ -19,6 +19,11 @@ import Foundation import ServiceModelCodeGeneration import ServiceModelEntities +public enum APIShape { + case syncAndCallback + case structuredConcurrency +} + /** A ModelClientDelegate that can be used to generate a Client protocol from a Service Model. @@ -28,19 +33,29 @@ public struct ClientProtocolDelegate: ModelClientDelegate { public let asyncResultType: AsyncResultType? public let baseName: String public let typeDescription: String + public let protocolAPIShape: APIShape /** Initializer. - Parameters: - baseName: The base name of the Service. + - protocolAPIShape: The shape of the APIs to create with this protocol - asyncResultType: The name of the result type to use for async functions. */ - public init(baseName: String, asyncResultType: AsyncResultType? = nil) { + public init(baseName: String, protocolAPIShape: APIShape = .syncAndCallback, + asyncResultType: AsyncResultType? = nil) { self.baseName = baseName self.asyncResultType = asyncResultType - self.clientType = .protocol(name: "\(baseName)ClientProtocol") + switch protocolAPIShape { + case .syncAndCallback: + self.clientType = .protocolWithConformance(name: "\(baseName)ClientProtocol", + conformingProtocolName: "\(baseName)ClientProtocolV2") + case .structuredConcurrency: + self.clientType = .protocol(name: "\(baseName)ClientProtocolV2") + } self.typeDescription = "Client Protocol for the \(baseName) service." + self.protocolAPIShape = protocolAPIShape } public func getFileDescription(isGenerator: Bool) -> String { @@ -59,16 +74,27 @@ public struct ClientProtocolDelegate: ModelClientDelegate { fileBuilder: FileBuilder, sortedOperations: [(String, OperationDescription)], isGenerator: Bool) { - // for each of the operations - for (name, operationDescription) in sortedOperations { - codeGenerator.addOperation(fileBuilder: fileBuilder, name: name, - operationDescription: operationDescription, - delegate: delegate, invokeType: .sync, - forTypeAlias: true, isGenerator: isGenerator) - codeGenerator.addOperation(fileBuilder: fileBuilder, name: name, - operationDescription: operationDescription, - delegate: delegate, invokeType: .async, - forTypeAlias: true, isGenerator: isGenerator) + switch self.protocolAPIShape { + case .syncAndCallback: + // for each of the operations + for (name, operationDescription) in sortedOperations { + codeGenerator.addOperation(fileBuilder: fileBuilder, name: name, + operationDescription: operationDescription, + delegate: delegate, invokeType: .sync, + forTypeAlias: true, isGenerator: isGenerator) + codeGenerator.addOperation(fileBuilder: fileBuilder, name: name, + operationDescription: operationDescription, + delegate: delegate, invokeType: .async, + forTypeAlias: true, isGenerator: isGenerator) + } + case .structuredConcurrency: + // for each of the operations + for (name, operationDescription) in sortedOperations { + codeGenerator.addOperation(fileBuilder: fileBuilder, name: name, + operationDescription: operationDescription, + delegate: delegate, invokeType: .asyncFunction, + forTypeAlias: true, isGenerator: isGenerator) + } } } diff --git a/Sources/ServiceModelGenerate/MockClientDelegate.swift b/Sources/ServiceModelGenerate/MockClientDelegate.swift index 2b8fac1..4a4c0f2 100644 --- a/Sources/ServiceModelGenerate/MockClientDelegate.swift +++ b/Sources/ServiceModelGenerate/MockClientDelegate.swift @@ -29,6 +29,7 @@ public struct MockClientDelegate: ModelClientDelegate { public let isThrowingMock: Bool public let clientType: ClientType public let typeDescription: String + public let clientAPIShape: APIShape /** Initializer. @@ -36,9 +37,11 @@ public struct MockClientDelegate: ModelClientDelegate { - Parameters: - baseName: The base name of the Service. - isThrowingMock: true to generate a throwing mock; false for a normal mock + - clientAPIShape: The shape of the APIs to create with this mock - asyncResultType: The name of the result type to use for async functions. */ public init(baseName: String, isThrowingMock: Bool, + clientAPIShape: APIShape = .syncAndCallback, asyncResultType: AsyncResultType? = nil) { self.baseName = baseName self.isThrowingMock = isThrowingMock @@ -54,8 +57,16 @@ public struct MockClientDelegate: ModelClientDelegate { + "returns the `__default` property of its return type." } - self.clientType = .struct(name: name, genericParameters: [], - conformingProtocolName: "\(baseName)ClientProtocol") + switch clientAPIShape { + case .syncAndCallback: + self.clientType = .struct(name: name, genericParameters: [], + conformingProtocolName: "\(baseName)ClientProtocol") + case .structuredConcurrency: + self.clientType = .struct(name: name + "V2", genericParameters: [], + conformingProtocolName: "\(baseName)ClientProtocolV2") + } + + self.clientAPIShape = clientAPIShape } public func getFileDescription(isGenerator: Bool) -> String { @@ -79,8 +90,13 @@ public struct MockClientDelegate: ModelClientDelegate { } let variableName = name.upperToLowerCamelCase - fileBuilder.appendLine("\(variableName)Async: \(name.startingWithUppercase)AsyncType? = nil,") - fileBuilder.appendLine("\(variableName)Sync: \(name.startingWithUppercase)SyncType? = nil\(postfix)") + switch self.clientAPIShape { + case .syncAndCallback: + fileBuilder.appendLine("\(variableName)Async: \(name.startingWithUppercase)AsyncType? = nil,") + fileBuilder.appendLine("\(variableName)Sync: \(name.startingWithUppercase)SyncType? = nil\(postfix)") + case .structuredConcurrency: + fileBuilder.appendLine("\(variableName): \(name.startingWithUppercase)FunctionType? = nil\(postfix)") + } } public func addCommonFunctions(codeGenerator: ServiceModelCodeGenerator, @@ -95,8 +111,14 @@ public struct MockClientDelegate: ModelClientDelegate { // for each of the operations for (name, _) in sortedOperations { let variableName = name.upperToLowerCamelCase - fileBuilder.appendLine("let \(variableName)AsyncOverride: \(name.startingWithUppercase)AsyncType?") - fileBuilder.appendLine("let \(variableName)SyncOverride: \(name.startingWithUppercase)SyncType?") + + switch self.clientAPIShape { + case .syncAndCallback: + fileBuilder.appendLine("let \(variableName)AsyncOverride: \(name.startingWithUppercase)AsyncType?") + fileBuilder.appendLine("let \(variableName)SyncOverride: \(name.startingWithUppercase)SyncType?") + case .structuredConcurrency: + fileBuilder.appendLine("let \(variableName)Override: \(name.startingWithUppercase)FunctionType?") + } } fileBuilder.appendEmptyLine() @@ -144,8 +166,13 @@ public struct MockClientDelegate: ModelClientDelegate { // for each of the operations for (name, _) in sortedOperations { let variableName = name.upperToLowerCamelCase - fileBuilder.appendLine("self.\(variableName)AsyncOverride = \(variableName)Async") - fileBuilder.appendLine("self.\(variableName)SyncOverride = \(variableName)Sync") + switch self.clientAPIShape { + case .syncAndCallback: + fileBuilder.appendLine("self.\(variableName)AsyncOverride = \(variableName)Async") + fileBuilder.appendLine("self.\(variableName)SyncOverride = \(variableName)Sync") + case .structuredConcurrency: + fileBuilder.appendLine("self.\(variableName)Override = \(variableName)") + } } fileBuilder.decIndent() @@ -194,7 +221,7 @@ public struct MockClientDelegate: ModelClientDelegate { let declarationPrefix: String switch invokeType { - case .sync: + case .sync, .asyncFunction: declarationPrefix = "return" case .async: declarationPrefix = "let result =" @@ -225,7 +252,7 @@ public struct MockClientDelegate: ModelClientDelegate { operationName: operationName, hasInput: hasInput) switch invokeType { - case .sync: + case .sync, .asyncFunction: fileBuilder.appendLine("throw error") case .async: if hasOutput { @@ -244,18 +271,25 @@ public struct MockClientDelegate: ModelClientDelegate { let customFunctionParameters: String let customFunctionPostfix: String + let asyncPrefix: String switch invokeType { case .async: customFunctionPostfix = "Async" customFunctionParameters = hasInput ? "input, completion" : "completion" + asyncPrefix = "" case .sync: customFunctionPostfix = "Sync" customFunctionParameters = hasInput ? "input" : "" + asyncPrefix = "" + case .asyncFunction: + customFunctionPostfix = "" + customFunctionParameters = hasInput ? "input" : "" + asyncPrefix = "await " } fileBuilder.appendLine(""" if let \(variableName)\(customFunctionPostfix)Override = \(variableName)\(customFunctionPostfix)Override { - return try \(variableName)\(customFunctionPostfix)Override(\(customFunctionParameters)) + return try \(asyncPrefix)\(variableName)\(customFunctionPostfix)Override(\(customFunctionParameters)) } """) fileBuilder.appendEmptyLine() @@ -267,6 +301,8 @@ public struct MockClientDelegate: ModelClientDelegate { return name case .struct(name: _, genericParameters: _, conformingProtocolName: let conformingProtocolName): return conformingProtocolName + case .protocolWithConformance(name: let name, conformingProtocolName: _): + return name } } } diff --git a/Sources/ServiceModelGenerate/ServiceModelCodeGenerator+generateClient.swift b/Sources/ServiceModelGenerate/ServiceModelCodeGenerator+generateClient.swift index d9e2b48..37ceeb7 100644 --- a/Sources/ServiceModelGenerate/ServiceModelCodeGenerator+generateClient.swift +++ b/Sources/ServiceModelGenerate/ServiceModelCodeGenerator+generateClient.swift @@ -19,6 +19,29 @@ import Foundation import ServiceModelCodeGeneration import ServiceModelEntities +public struct ClientAPISupport: OptionSet { + public let rawValue: Int + + public init(rawValue: Int) { + self.rawValue = rawValue + } + + public static let syncAndCallback = ClientAPISupport(rawValue: 1) + public static let structuredConcurrency = ClientAPISupport(rawValue: 2) + + public var isOnlySyncAndCallback: Bool { + return self == [.syncAndCallback] + } + + public var isOnlyStructuredConcurrency: Bool { + return self == [.structuredConcurrency] + } + + public var isSyncAndCallbackAndStructuredConcurrency: Bool { + return self == [.syncAndCallback, .structuredConcurrency] + } +} + public extension ServiceModelCodeGenerator { private struct OperationSignature { let input: String @@ -34,7 +57,8 @@ public extension ServiceModelCodeGenerator { - Parameters: - delegate: The delegate to use when generating this client. */ - func generateClient(delegate: ModelClientDelegate, isGenerator: Bool) { + func generateClient(delegate: ModelClientDelegate, isGenerator: Bool, + clientAPISupport: ClientAPISupport = .syncAndCallback) { let fileBuilder = FileBuilder() let baseName = applicationDescription.baseName @@ -70,6 +94,9 @@ public extension ServiceModelCodeGenerator { } typeDecaration = "struct \(typeName)\(genericParametersString): \(protocolTypeName)" } + case .protocolWithConformance(name: let protocolTypeName, conformingProtocolName: let conformingProtocolName): + typeName = protocolTypeName + typePostfix + typeDecaration = "protocol \(typeName): \(conformingProtocolName)" } addFileHeader(fileBuilder: fileBuilder, typeName: typeName, @@ -86,6 +113,12 @@ public extension ServiceModelCodeGenerator { public \(typeDecaration) { """) + if clientAPISupport.isOnlyStructuredConcurrency { + fileBuilder.appendLine(""" + #if (os(Linux) && compiler(>=5.5)) || (!os(Linux) && compiler(>=5.5.2)) && canImport(_Concurrency) + """) + } + fileBuilder.incIndent() let sortedOperations = model.operationDescriptions.sorted { (left, right) in left.key < right.key } @@ -95,19 +128,52 @@ public extension ServiceModelCodeGenerator { sortedOperations: sortedOperations, isGenerator: isGenerator) if !isGenerator { - // for each of the operations - for (name, operationDescription) in sortedOperations { - addOperation(fileBuilder: fileBuilder, name: name, - operationDescription: operationDescription, - delegate: delegate, invokeType: .async, - forTypeAlias: false, isGenerator: isGenerator) - addOperation(fileBuilder: fileBuilder, name: name, - operationDescription: operationDescription, - delegate: delegate, invokeType: .sync, - forTypeAlias: false, isGenerator: isGenerator) + if clientAPISupport.contains(.syncAndCallback) { + // for each of the operations + for (name, operationDescription) in sortedOperations { + addOperation(fileBuilder: fileBuilder, name: name, + operationDescription: operationDescription, + delegate: delegate, invokeType: .async, + forTypeAlias: false, isGenerator: isGenerator) + addOperation(fileBuilder: fileBuilder, name: name, + operationDescription: operationDescription, + delegate: delegate, invokeType: .sync, + forTypeAlias: false, isGenerator: isGenerator) + } + } + + if clientAPISupport.isSyncAndCallbackAndStructuredConcurrency { + fileBuilder.appendLine(""" + + #if (os(Linux) && compiler(>=5.5)) || (!os(Linux) && compiler(>=5.5.2)) && canImport(_Concurrency) + """) + } + + if clientAPISupport.contains(.structuredConcurrency) { + // for each of the operations + for (name, operationDescription) in sortedOperations { + addOperation(fileBuilder: fileBuilder, name: name, + operationDescription: operationDescription, + delegate: delegate, invokeType: .asyncFunction, + forTypeAlias: false, isGenerator: isGenerator) + } } + + if clientAPISupport.isSyncAndCallbackAndStructuredConcurrency { + fileBuilder.appendLine(""" + #endif + """) + } + } + + fileBuilder.decIndent() + if clientAPISupport.isOnlyStructuredConcurrency { + fileBuilder.appendLine(""" + #endif + """) } - fileBuilder.appendLine("}", preDec: true) + + fileBuilder.appendLine("}") let baseFilePath = applicationDescription.baseFilePath let fileName = "\(typeName).swift" @@ -157,7 +223,7 @@ public extension ServiceModelCodeGenerator { let type = outputType.getNormalizedTypeName(forModel: model) switch invokeType { - case .sync: + case .sync, .asyncFunction: output = " -> \(baseName)Model.\(type)" if !forTypeAlias { fileBuilder.appendLine(" - Returns: The \(type) object to be passed back from the caller of this operation.") @@ -178,7 +244,7 @@ public extension ServiceModelCodeGenerator { functionOutputType = type } else { switch invokeType { - case .sync: + case .sync, .asyncFunction: if !forTypeAlias { output = "" } else { @@ -206,7 +272,7 @@ public extension ServiceModelCodeGenerator { var description: String switch invokeType { - case .sync: + case .sync, .asyncFunction: description = " - Throws: " case .async: description = " The possible errors are: " @@ -238,6 +304,10 @@ public extension ServiceModelCodeGenerator { \(declarationPrefix)func \(functionName)\(invokeType.rawValue)( \(output))\(errors)\(declarationPostfix) """) + case .asyncFunction: + fileBuilder.appendLine(""" + \(declarationPrefix)func \(functionName)() async\(errors)\(output)\(declarationPostfix) + """) } } else { switch invokeType { @@ -250,6 +320,10 @@ public extension ServiceModelCodeGenerator { \(declarationPrefix)typealias \(functionName)\(invokeType.rawValue)Type = ( \(output))\(errors)\(declarationPostfix) -> () """) + case .asyncFunction: + fileBuilder.appendLine(""" + \(declarationPrefix)typealias \(functionName)FunctionType = () async\(errors)\(output)\(declarationPostfix) + """) } } } @@ -270,6 +344,11 @@ public extension ServiceModelCodeGenerator { \(input) \(output))\(errors)\(declarationPostfix) """) + case .asyncFunction: + fileBuilder.appendLine(""" + \(declarationPrefix)func \(functionName)( + \(input)) async\(errors)\(output)\(declarationPostfix) + """) } } else { switch invokeType { @@ -284,6 +363,11 @@ public extension ServiceModelCodeGenerator { \(input) \(output))\(errors)\(declarationPostfix) -> () """) + case .asyncFunction: + fileBuilder.appendLine(""" + \(declarationPrefix)typealias \(functionName)FunctionType = ( + \(input)) async\(errors)\(output)\(declarationPostfix) + """) } } } @@ -308,13 +392,16 @@ public extension ServiceModelCodeGenerator { let declarationPrefix: String let declarationPostfix: String - if case .protocol = delegate.clientType { + switch delegate.clientType { + + case .protocol, .protocolWithConformance: declarationPrefix = "" declarationPostfix = "" - } else { + case .struct: declarationPrefix = "public " declarationPostfix = " {" } + if input.isEmpty { addFunctionDeclarationWithNoInput(forTypeAlias: forTypeAlias, invokeType: invokeType, fileBuilder: fileBuilder, declarationPrefix: declarationPrefix, functionName: functionName, errors: errors, @@ -359,6 +446,8 @@ public extension ServiceModelCodeGenerator { invokeDescription = "waiting for the response before returning" case .async: invokeDescription = "returning immediately and passing the response to a callback" + case .asyncFunction: + invokeDescription = "suspending until the response is available before returning" } fileBuilder.appendEmptyLine() fileBuilder.appendLine(""" From af9c2127d287739f76a64a493b28c7018da00da3 Mon Sep 17 00:00:00 2001 From: Simon Pilkington Date: Wed, 4 Jan 2023 15:52:06 +1100 Subject: [PATCH 2/3] Add CI. --- .github/workflows/swift.yml | 23 +++++++++++++++++++++++ README.md | 9 +++------ 2 files changed, 26 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/swift.yml diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml new file mode 100644 index 0000000..3e9bb35 --- /dev/null +++ b/.github/workflows/swift.yml @@ -0,0 +1,23 @@ +name: build + +on: + push: + branches: [ main, service-model-swift-code-generate-2.x, service-model-swift-code-generate-1.x ] + pull_request: + branches: [ main, service-model-swift-code-generate-2.x, service-model-swift-code-generate-1.x ] + +jobs: + Build: + name: Swift ${{ matrix.swift }} on ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-22.04] + swift: ["5.7.2", "5.6.3", "5.5.3"] + runs-on: ${{ matrix.os }} + steps: + - uses: swift-actions/setup-swift@v1.21.0 + with: + swift-version: ${{ matrix.swift }} + - uses: actions/checkout@v2 + - name: Build + run: swift build -c release diff --git a/README.md b/README.md index 9c38f6d..a34fe26 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,10 @@

- -Build - Master Branch + +Build - service-model-swift-code-generate-2.x Branch -Swift 5.1, 5.2 and 5.3 Tested +Swift 5.5, 5.6 and 5.7 Tested -Ubuntu 16.04, 18.04 and 20.04 Tested -CentOS 8 Tested -Amazon Linux 2 Tested Join the Smoke Server Side community on gitter From 9841c684f61f804dde2b36dfef22979cf1c86676 Mon Sep 17 00:00:00 2001 From: Simon Pilkington Date: Wed, 4 Jan 2023 15:57:43 +1100 Subject: [PATCH 3/3] Fix CI for 5.6 and 5.5. --- .github/workflows/swift.yml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 3e9bb35..776b0b7 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -12,7 +12,21 @@ jobs: strategy: matrix: os: [ubuntu-22.04] - swift: ["5.7.2", "5.6.3", "5.5.3"] + swift: ["5.7.2"] + runs-on: ${{ matrix.os }} + steps: + - uses: swift-actions/setup-swift@v1.21.0 + with: + swift-version: ${{ matrix.swift }} + - uses: actions/checkout@v2 + - name: Build + run: swift build -c release + Build20: + name: Swift ${{ matrix.swift }} on ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-20.04] + swift: ["5.6.3", "5.5.3"] runs-on: ${{ matrix.os }} steps: - uses: swift-actions/setup-swift@v1.21.0