diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwift+Client.swift b/Sources/GRPCCodeGen/Internal/StructuredSwift+Client.swift new file mode 100644 index 000000000..b4e62194e --- /dev/null +++ b/Sources/GRPCCodeGen/Internal/StructuredSwift+Client.swift @@ -0,0 +1,704 @@ +/* + * Copyright 2024, gRPC Authors All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +extension ClosureInvocationDescription { + /// ``` + /// { response in + /// try response.message + /// } + /// ``` + static var defaultClientUnaryResponseHandler: Self { + ClosureInvocationDescription( + argumentNames: ["response"], + body: [.expression(.try(.identifierPattern("response").dot("message")))] + ) + } +} + +extension FunctionSignatureDescription { + /// ``` + /// func ( + /// request: GRPCCore.ClientRequest, + /// serializer: some GRPCCore.MessageSerializer, + /// deserializer: some GRPCCore.MessageDeserializer, + /// options: GRPCCore.CallOptions, + /// onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + /// ) async throws -> Result where Result: Sendable + /// ``` + static func clientMethod( + accessLevel: AccessModifier? = nil, + name: String, + input: String, + output: String, + streamingInput: Bool, + streamingOutput: Bool, + includeDefaults: Bool, + includeSerializers: Bool + ) -> Self { + var signature = FunctionSignatureDescription( + accessModifier: accessLevel, + kind: .function(name: name, isStatic: false), + generics: [.member("Result")], + parameters: [], // Populated below. + keywords: [.async, .throws], + returnType: .identifierPattern("Result"), + whereClause: WhereClause(requirements: [.conformance("Result", "Sendable")]) + ) + + signature.parameters.append( + ParameterDescription( + label: "request", + type: .clientRequest(forType: input, streaming: streamingInput) + ) + ) + + if includeSerializers { + signature.parameters.append( + ParameterDescription( + label: "serializer", + // Type is optional, so be explicit about which 'some' to use + type: ExistingTypeDescription.some(.serializer(forType: input)) + ) + ) + signature.parameters.append( + ParameterDescription( + label: "deserializer", + // Type is optional, so be explicit about which 'some' to use + type: ExistingTypeDescription.some(.deserializer(forType: output)) + ) + ) + } + + signature.parameters.append( + ParameterDescription( + label: "options", + type: .callOptions, + defaultValue: includeDefaults ? .memberAccess(.dot("defaults")) : nil + ) + ) + + signature.parameters.append( + ParameterDescription( + label: "onResponse", + name: "handleResponse", + type: .closure( + ClosureSignatureDescription( + parameters: [ + ParameterDescription( + type: .clientResponse(forType: output, streaming: streamingOutput) + ) + ], + keywords: [.async, .throws], + returnType: .identifierPattern("Result"), + sendable: true, + escaping: true + ) + ), + defaultValue: includeDefaults && !streamingOutput + ? .closureInvocation(.defaultClientUnaryResponseHandler) + : nil + ) + ) + + return signature + } +} + +extension FunctionDescription { + /// ``` + /// func ( + /// request: GRPCCore.ClientRequest, + /// options: GRPCCore.CallOptions = .defaults, + /// onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + /// ) async throws -> Result where Result: Sendable { + /// try await self.( + /// request: request, + /// serializer: , + /// deserializer: , + /// options: options + /// onResponse: handleResponse, + /// ) + /// } + /// ``` + static func clientMethodWithDefaults( + accessLevel: AccessModifier? = nil, + name: String, + input: String, + output: String, + streamingInput: Bool, + streamingOutput: Bool, + serializer: Expression, + deserializer: Expression + ) -> Self { + FunctionDescription( + signature: .clientMethod( + accessLevel: accessLevel, + name: name, + input: input, + output: output, + streamingInput: streamingInput, + streamingOutput: streamingOutput, + includeDefaults: true, + includeSerializers: false + ), + body: [ + .expression( + .try( + .await( + .functionCall( + calledExpression: .identifierPattern("self").dot(name), + arguments: [ + FunctionArgumentDescription( + label: "request", + expression: .identifierPattern("request") + ), + FunctionArgumentDescription( + label: "serializer", + expression: serializer + ), + FunctionArgumentDescription( + label: "deserializer", + expression: deserializer + ), + FunctionArgumentDescription( + label: "options", + expression: .identifierPattern("options") + ), + FunctionArgumentDescription( + label: "onResponse", + expression: .identifierPattern("handleResponse") + ), + ] + ) + ) + ) + ) + ] + ) + } +} + +extension ProtocolDescription { + /// ``` + /// protocol : Sendable { + /// func foo( + /// ... + /// ) async throws -> Result + /// } + /// ``` + static func clientProtocol( + accessLevel: AccessModifier? = nil, + name: String, + methods: [MethodDescriptor] + ) -> Self { + ProtocolDescription( + accessModifier: accessLevel, + name: name, + conformances: ["Sendable"], + members: methods.map { method in + .commentable( + .preFormatted(method.documentation), + .function( + signature: .clientMethod( + name: method.name.generatedLowerCase, + input: method.inputType, + output: method.outputType, + streamingInput: method.isInputStreaming, + streamingOutput: method.isOutputStreaming, + includeDefaults: false, + includeSerializers: true + ) + ) + ) + } + ) + } +} + +extension ExtensionDescription { + /// ``` + /// extension { + /// func foo( + /// request: GRPCCore.ClientRequest, + /// options: GRPCCore.CallOptions = .defaults, + /// onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + /// ) async throws -> Result where Result: Sendable { + /// // ... + /// } + /// // ... + /// } + /// ``` + static func clientMethodSignatureWithDefaults( + accessLevel: AccessModifier? = nil, + name: String, + methods: [MethodDescriptor], + serializer: (String) -> String, + deserializer: (String) -> String + ) -> Self { + ExtensionDescription( + onType: name, + declarations: methods.map { method in + .function( + .clientMethodWithDefaults( + accessLevel: accessLevel, + name: method.name.generatedLowerCase, + input: method.inputType, + output: method.outputType, + streamingInput: method.isInputStreaming, + streamingOutput: method.isOutputStreaming, + serializer: .identifierPattern(serializer(method.inputType)), + deserializer: .identifierPattern(deserializer(method.outputType)) + ) + ) + } + ) + } +} + +extension FunctionSignatureDescription { + /// ``` + /// func foo( + /// _ message: , + /// metadata: GRPCCore.Metadata = [:], + /// options: GRPCCore.CallOptions = .defaults, + /// onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + /// try response.message + /// } + /// ) async throws -> Result where Result: Sendable + /// ``` + static func clientMethodExploded( + accessLevel: AccessModifier? = nil, + name: String, + input: String, + output: String, + streamingInput: Bool, + streamingOutput: Bool + ) -> Self { + var signature = FunctionSignatureDescription( + accessModifier: accessLevel, + kind: .function(name: name), + generics: [.member("Result")], + parameters: [], // Populated below + keywords: [.async, .throws], + returnType: .identifierPattern("Result"), + whereClause: WhereClause(requirements: [.conformance("Result", "Sendable")]) + ) + + if !streamingInput { + signature.parameters.append( + ParameterDescription(label: "_", name: "message", type: .member(input)) + ) + } + + // metadata: GRPCCore.Metadata = [:] + signature.parameters.append( + ParameterDescription( + label: "metadata", + type: .metadata, + defaultValue: .literal(.dictionary([])) + ) + ) + + // options: GRPCCore.CallOptions = .defaults + signature.parameters.append( + ParameterDescription( + label: "options", + type: .callOptions, + defaultValue: .dot("defaults") + ) + ) + + if streamingInput { + signature.parameters.append( + ParameterDescription( + label: "requestProducer", + name: "producer", + type: .closure( + ClosureSignatureDescription( + parameters: [ParameterDescription(type: .rpcWriter(forType: input))], + keywords: [.async, .throws], + returnType: .identifierPattern("Void"), + sendable: true, + escaping: true + ) + ) + ) + ) + } + + signature.parameters.append( + ParameterDescription( + label: "onResponse", + name: "handleResponse", + type: .closure( + ClosureSignatureDescription( + parameters: [ + ParameterDescription( + type: .clientResponse(forType: output, streaming: streamingOutput) + ) + ], + keywords: [.async, .throws], + returnType: .identifierPattern("Result"), + sendable: true, + escaping: true + ) + ), + defaultValue: streamingOutput ? nil : .closureInvocation(.defaultClientUnaryResponseHandler) + ) + ) + + return signature + } +} + +extension [CodeBlock] { + /// ``` + /// let request = GRPCCore.StreamingClientRequest( + /// metadata: metadata, + /// producer: producer + /// ) + /// return try await self.foo( + /// request: request, + /// options: options, + /// onResponse: handleResponse + /// ) + /// ``` + static func clientMethodExploded( + name: String, + input: String, + streamingInput: Bool + ) -> Self { + func arguments(streaming: Bool) -> [FunctionArgumentDescription] { + let metadata = FunctionArgumentDescription( + label: "metadata", + expression: .identifierPattern("metadata") + ) + + if streaming { + return [ + metadata, + FunctionArgumentDescription( + label: "producer", + expression: .identifierPattern("producer") + ), + ] + } else { + return [ + FunctionArgumentDescription(label: "message", expression: .identifierPattern("message")), + metadata, + ] + } + } + + return [ + CodeBlock( + item: .declaration( + .variable( + kind: .let, + left: .identifierPattern("request"), + right: .functionCall( + calledExpression: .identifierType( + .clientRequest(forType: input, streaming: streamingInput) + ), + arguments: arguments(streaming: streamingInput) + ) + ) + ) + ), + CodeBlock( + item: .expression( + .return( + .try( + .await( + .functionCall( + calledExpression: .identifierPattern("self").dot(name), + arguments: [ + FunctionArgumentDescription( + label: "request", + expression: .identifierPattern("request") + ), + FunctionArgumentDescription( + label: "options", + expression: .identifierPattern("options") + ), + FunctionArgumentDescription( + label: "onResponse", + expression: .identifierPattern("handleResponse") + ), + ] + ) + ) + ) + ) + ) + ), + ] + } +} + +extension FunctionDescription { + /// ``` + /// func foo( + /// _ message: , + /// metadata: GRPCCore.Metadata = [:], + /// options: GRPCCore.CallOptions = .defaults, + /// onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + /// try response.message + /// } + /// ) async throws -> Result where Result: Sendable { + /// // ... + /// } + /// ``` + static func clientMethodExploded( + accessLevel: AccessModifier? = nil, + name: String, + input: String, + output: String, + streamingInput: Bool, + streamingOutput: Bool + ) -> Self { + FunctionDescription( + signature: .clientMethodExploded( + accessLevel: accessLevel, + name: name, + input: input, + output: output, + streamingInput: streamingInput, + streamingOutput: streamingOutput + ), + body: .clientMethodExploded(name: name, input: input, streamingInput: streamingInput) + ) + } +} + +extension ExtensionDescription { + /// ``` + /// extension { + /// // (exploded client methods) + /// } + /// ``` + static func explodedClientMethods( + accessLevel: AccessModifier? = nil, + on extensionName: String, + methods: [MethodDescriptor] + ) -> ExtensionDescription { + ExtensionDescription( + onType: extensionName, + declarations: methods.map { method in + .commentable( + .preFormatted(method.documentation), + .function( + .clientMethodExploded( + accessLevel: accessLevel, + name: method.name.generatedLowerCase, + input: method.inputType, + output: method.outputType, + streamingInput: method.isInputStreaming, + streamingOutput: method.isOutputStreaming + ) + ) + ) + } + ) + } +} + +extension FunctionDescription { + /// ``` + /// func ( + /// request: GRPCCore.ClientRequest, + /// serializer: some GRPCCore.MessageSerializer, + /// deserializer: some GRPCCore.MessageDeserializer, + /// options: GRPCCore.CallOptions = .default, + /// onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + /// ) async throws -> Result where Result: Sendable { + /// try await self.(...) + /// } + /// ``` + static func clientMethod( + accessLevel: AccessModifier, + name: String, + input: String, + output: String, + serviceEnum: String, + methodEnum: String, + streamingInput: Bool, + streamingOutput: Bool + ) -> Self { + let underlyingMethod: String + switch (streamingInput, streamingOutput) { + case (false, false): + underlyingMethod = "unary" + case (true, false): + underlyingMethod = "clientStreaming" + case (false, true): + underlyingMethod = "serverStreaming" + case (true, true): + underlyingMethod = "bidirectionalStreaming" + } + + return FunctionDescription( + accessModifier: accessLevel, + kind: .function(name: name), + generics: [.member("Result")], + parameters: [ + ParameterDescription( + label: "request", + type: .clientRequest(forType: input, streaming: streamingInput) + ), + ParameterDescription( + label: "serializer", + // Be explicit: 'type' is optional and '.some' resolves to Optional.some by default. + type: ExistingTypeDescription.some(.serializer(forType: input)) + ), + ParameterDescription( + label: "deserializer", + // Be explicit: 'type' is optional and '.some' resolves to Optional.some by default. + type: ExistingTypeDescription.some(.deserializer(forType: output)) + ), + ParameterDescription( + label: "options", + type: .callOptions, + defaultValue: .dot("defaults") + ), + ParameterDescription( + label: "onResponse", + name: "handleResponse", + type: .closure( + ClosureSignatureDescription( + parameters: [ + ParameterDescription( + type: .clientResponse(forType: output, streaming: streamingOutput) + ) + ], + keywords: [.async, .throws], + returnType: .identifierPattern("Result"), + sendable: true, + escaping: true + ) + ), + defaultValue: streamingOutput + ? nil + : .closureInvocation(.defaultClientUnaryResponseHandler) + ), + ], + keywords: [.async, .throws], + returnType: .identifierPattern("Result"), + whereClause: WhereClause(requirements: [.conformance("Result", "Sendable")]), + body: [ + .try( + .await( + .functionCall( + calledExpression: .identifierPattern("self").dot("client").dot(underlyingMethod), + arguments: [ + FunctionArgumentDescription( + label: "request", + expression: .identifierPattern("request") + ), + FunctionArgumentDescription( + label: "descriptor", + expression: .identifierPattern(serviceEnum) + .dot("Method") + .dot(methodEnum) + .dot("descriptor") + ), + FunctionArgumentDescription( + label: "serializer", + expression: .identifierPattern("serializer") + ), + FunctionArgumentDescription( + label: "deserializer", + expression: .identifierPattern("deserializer") + ), + FunctionArgumentDescription( + label: "options", + expression: .identifierPattern("options") + ), + FunctionArgumentDescription( + label: "onResponse", + expression: .identifierPattern("handleResponse") + ), + ] + ) + ) + ) + ] + ) + } +} + +extension StructDescription { + /// ``` + /// struct : { + /// private let client: GRPCCore.GRPCClient + /// + /// init(wrapping client: GRPCCore.GRPCClient) { + /// self.client = client + /// } + /// + /// // ... + /// } + /// ``` + static func client( + accessLevel: AccessModifier, + name: String, + serviceEnum: String, + clientProtocol: String, + methods: [MethodDescriptor] + ) -> Self { + StructDescription( + accessModifier: accessLevel, + name: name, + conformances: [clientProtocol], + members: [ + .variable(accessModifier: .private, kind: .let, left: "client", type: .grpcClient), + .function( + accessModifier: accessLevel, + kind: .initializer, + parameters: [ + ParameterDescription(label: "wrapping", name: "client", type: .grpcClient) + ], + whereClause: nil, + body: [ + .expression( + .assignment( + left: .identifierPattern("self").dot("client"), + right: .identifierPattern("client") + ) + ) + ] + ), + ] + + methods.map { method in + .commentable( + .preFormatted(method.documentation), + .function( + .clientMethod( + accessLevel: accessLevel, + name: method.name.generatedLowerCase, + input: method.inputType, + output: method.outputType, + serviceEnum: serviceEnum, + methodEnum: method.name.generatedUpperCase, + streamingInput: method.isInputStreaming, + streamingOutput: method.isOutputStreaming + ) + ) + ) + } + ) + } +} diff --git a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ClientTests.swift b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ClientTests.swift new file mode 100644 index 000000000..a4abc0ee3 --- /dev/null +++ b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ClientTests.swift @@ -0,0 +1,544 @@ +/* + * Copyright 2024, gRPC Authors All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Testing + +@testable import GRPCCodeGen + +extension StructuedSwiftTests { + @Suite("Client") + struct Client { + @Test( + "func (request:serializer:deserializer:options:onResponse:)", + arguments: AccessModifier.allCases, + RPCKind.allCases + ) + func clientMethodSignature(access: AccessModifier, kind: RPCKind) { + let decl: FunctionSignatureDescription = .clientMethod( + accessLevel: access, + name: "foo", + input: "FooInput", + output: "FooOutput", + streamingInput: kind.streamsInput, + streamingOutput: kind.streamsOutput, + includeDefaults: false, + includeSerializers: true + ) + + let requestType = kind.streamsInput ? "StreamingClientRequest" : "ClientRequest" + let responseType = kind.streamsOutput ? "StreamingClientResponse" : "ClientResponse" + + let expected = """ + \(access) func foo( + request: GRPCCore.\(requestType), + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.\(responseType)) async throws -> Result + ) async throws -> Result where Result: Sendable + """ + + #expect(render(.function(signature: decl)) == expected) + } + + @Test( + "func (request:serializer:deserializer:options:onResponse:) (with defaults)", + arguments: AccessModifier.allCases, + [true, false] + ) + func clientMethodSignatureWithDefaults(access: AccessModifier, streamsOutput: Bool) { + let decl: FunctionSignatureDescription = .clientMethod( + accessLevel: access, + name: "foo", + input: "FooInput", + output: "FooOutput", + streamingInput: false, + streamingOutput: streamsOutput, + includeDefaults: true, + includeSerializers: false + ) + + let expected: String + if streamsOutput { + expected = """ + \(access) func foo( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + """ + } else { + expected = """ + \(access) func foo( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable + """ + } + + #expect(render(.function(signature: decl)) == expected) + } + + @Test("protocol Foo_ClientProtocol: Sendable { ... }", arguments: AccessModifier.allCases) + func clientProtocol(access: AccessModifier) { + let decl: ProtocolDescription = .clientProtocol( + accessLevel: access, + name: "Foo_ClientProtocol", + methods: [ + .init( + documentation: "/// Some docs", + name: .init(base: "Bar", generatedUpperCase: "Bar", generatedLowerCase: "bar"), + isInputStreaming: false, + isOutputStreaming: false, + inputType: "BarInput", + outputType: "BarOutput" + ) + ] + ) + + let expected = """ + \(access) protocol Foo_ClientProtocol: Sendable { + /// Some docs + func bar( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + } + """ + + #expect(render(.protocol(decl)) == expected) + } + + @Test("func foo(...) { try await self.foo(...) }", arguments: AccessModifier.allCases) + func clientMethodFunctionWithDefaults(access: AccessModifier) { + let decl: FunctionDescription = .clientMethodWithDefaults( + accessLevel: access, + name: "foo", + input: "FooInput", + output: "FooOutput", + streamingInput: false, + streamingOutput: false, + serializer: .identifierPattern("Serialize()"), + deserializer: .identifierPattern("Deserialize()") + ) + + let expected = """ + \(access) func foo( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.foo( + request: request, + serializer: Serialize(), + deserializer: Deserialize(), + options: options, + onResponse: handleResponse + ) + } + """ + + #expect(render(.function(decl)) == expected) + } + + @Test( + "extension Foo_ClientProtocol { ... } (methods with defaults)", + arguments: AccessModifier.allCases + ) + func extensionWithDefaultClientMethods(access: AccessModifier) { + let decl: ExtensionDescription = .clientMethodSignatureWithDefaults( + accessLevel: access, + name: "Foo_ClientProtocol", + methods: [ + MethodDescriptor( + documentation: "", + name: .init(base: "Bar", generatedUpperCase: "Bar", generatedLowerCase: "bar"), + isInputStreaming: false, + isOutputStreaming: false, + inputType: "BarInput", + outputType: "BarOutput" + ) + ], + serializer: { "Serialize<\($0)>()" }, + deserializer: { "Deserialize<\($0)>()" } + ) + + let expected = """ + extension Foo_ClientProtocol { + \(access) func bar( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.bar( + request: request, + serializer: Serialize(), + deserializer: Deserialize(), + options: options, + onResponse: handleResponse + ) + } + } + """ + + #expect(render(.extension(decl)) == expected) + } + + @Test( + "func foo(_:metadata:options:onResponse:) -> Result (exploded signature)", + arguments: AccessModifier.allCases, + RPCKind.allCases + ) + func explodedClientMethodSignature(access: AccessModifier, kind: RPCKind) { + let decl: FunctionSignatureDescription = .clientMethodExploded( + accessLevel: access, + name: "foo", + input: "Input", + output: "Output", + streamingInput: kind.streamsInput, + streamingOutput: kind.streamsOutput + ) + + let expected: String + switch kind { + case .unary: + expected = """ + \(access) func foo( + _ message: Input, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable + """ + case .clientStreaming: + expected = """ + \(access) func foo( + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + requestProducer producer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable + """ + case .serverStreaming: + expected = """ + \(access) func foo( + _ message: Input, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + """ + case .bidirectionalStreaming: + expected = """ + \(access) func foo( + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + requestProducer producer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable + """ + } + + #expect(render(.function(signature: decl)) == expected) + } + + @Test( + "func foo(_:metadata:options:onResponse:) -> Result (exploded body)", + arguments: [true, false] + ) + func explodedClientMethodBody(streamingInput: Bool) { + let blocks: [CodeBlock] = .clientMethodExploded( + name: "foo", + input: "Input", + streamingInput: streamingInput + ) + + let expected: String + if streamingInput { + expected = """ + let request = GRPCCore.StreamingClientRequest( + metadata: metadata, + producer: producer + ) + return try await self.foo( + request: request, + options: options, + onResponse: handleResponse + ) + """ + + } else { + expected = """ + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.foo( + request: request, + options: options, + onResponse: handleResponse + ) + """ + } + + #expect(render(blocks) == expected) + } + + @Test("extension Foo_ClientProtocol { ... } (exploded)", arguments: AccessModifier.allCases) + func explodedClientMethodExtension(access: AccessModifier) { + let decl: ExtensionDescription = .explodedClientMethods( + accessLevel: access, + on: "Foo_ClientProtocol", + methods: [ + .init( + documentation: "/// Some docs", + name: .init(base: "Bar", generatedUpperCase: "Bar", generatedLowerCase: "bar"), + isInputStreaming: false, + isOutputStreaming: true, + inputType: "Input", + outputType: "Output" + ) + ] + ) + + let expected = """ + extension Foo_ClientProtocol { + /// Some docs + \(access) func bar( + _ message: Input, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.bar( + request: request, + options: options, + onResponse: handleResponse + ) + } + } + """ + + #expect(render(.extension(decl)) == expected) + } + + @Test( + "func foo(request:serializer:deserializer:options:onResponse:) (client method impl.)", + arguments: AccessModifier.allCases + ) + func clientMethodImplementation(access: AccessModifier) { + let decl: FunctionDescription = .clientMethod( + accessLevel: access, + name: "foo", + input: "Input", + output: "Output", + serviceEnum: "BarService", + methodEnum: "Foo", + streamingInput: false, + streamingOutput: false + ) + + let expected = """ + \(access) func foo( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: BarService.Method.Foo.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + """ + + #expect(render(.function(decl)) == expected) + } + + @Test("struct FooClient: Foo_ClientProtocol { ... }", arguments: AccessModifier.allCases) + func client(access: AccessModifier) { + let decl: StructDescription = .client( + accessLevel: access, + name: "FooClient", + serviceEnum: "BarService", + clientProtocol: "Foo_ClientProtocol", + methods: [ + .init( + documentation: "/// Unary docs", + name: .init( + base: "Unary", + generatedUpperCase: "Unary", + generatedLowerCase: "unary" + ), + isInputStreaming: false, + isOutputStreaming: false, + inputType: "Input", + outputType: "Output" + ), + .init( + documentation: "/// ClientStreaming docs", + name: .init( + base: "ClientStreaming", + generatedUpperCase: "ClientStreaming", + generatedLowerCase: "clientStreaming" + ), + isInputStreaming: true, + isOutputStreaming: false, + inputType: "Input", + outputType: "Output" + ), + .init( + documentation: "/// ServerStreaming docs", + name: .init( + base: "ServerStreaming", + generatedUpperCase: "ServerStreaming", + generatedLowerCase: "serverStreaming" + ), + isInputStreaming: false, + isOutputStreaming: true, + inputType: "Input", + outputType: "Output" + ), + .init( + documentation: "/// BidiStreaming docs", + name: .init( + base: "BidiStreaming", + generatedUpperCase: "BidiStreaming", + generatedLowerCase: "bidiStreaming" + ), + isInputStreaming: true, + isOutputStreaming: true, + inputType: "Input", + outputType: "Output" + ), + ] + ) + + let expected = """ + \(access) struct FooClient: Foo_ClientProtocol { + private let client: GRPCCore.GRPCClient + + \(access) init(wrapping client: GRPCCore.GRPCClient) { + self.client = client + } + + /// Unary docs + \(access) func unary( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.unary( + request: request, + descriptor: BarService.Method.Unary.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// ClientStreaming docs + \(access) func clientStreaming( + request: GRPCCore.StreamingClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.client.clientStreaming( + request: request, + descriptor: BarService.Method.ClientStreaming.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// ServerStreaming docs + \(access) func serverStreaming( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { + try await self.client.serverStreaming( + request: request, + descriptor: BarService.Method.ServerStreaming.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + + /// BidiStreaming docs + \(access) func bidiStreaming( + request: GRPCCore.StreamingClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { + try await self.client.bidirectionalStreaming( + request: request, + descriptor: BarService.Method.BidiStreaming.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) + } + } + """ + + #expect(render(.struct(decl)) == expected) + } + } +}