diff --git a/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift b/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift index 3d25ee7f0..888d91fd3 100644 --- a/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift +++ b/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift @@ -613,7 +613,14 @@ struct TextBasedRenderer: RendererProtocol { writer.nextLineAppendsToLastLine() writer.writeLine("<") writer.nextLineAppendsToLastLine() - renderExistingTypeDescription(wrapped) + for (wrap, isLast) in wrapped.enumeratedWithLastMarker() { + renderExistingTypeDescription(wrap) + writer.nextLineAppendsToLastLine() + if !isLast { + writer.writeLine(", ") + writer.nextLineAppendsToLastLine() + } + } writer.nextLineAppendsToLastLine() writer.writeLine(">") case .optional(let existingTypeDescription): diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwift+Server.swift b/Sources/GRPCCodeGen/Internal/StructuredSwift+Server.swift index 44093da6a..95643d4d0 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwift+Server.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwift+Server.swift @@ -473,3 +473,289 @@ extension ExtensionDescription { ) } } + +extension FunctionSignatureDescription { + /// ``` + /// func ( + /// request: , + /// context: GRPCCore.ServerContext, + /// ) async throws -> + /// ``` + /// + /// ``` + /// func ( + /// request: GRPCCore.RPCAsyncSequence, + /// response: GRPCCore.RPCAsyncWriter + /// context: GRPCCore.ServerContext, + /// ) async throws + /// ``` + static func simpleServerMethod( + accessLevel: AccessModifier? = nil, + name: String, + input: String, + output: String, + streamingInput: Bool, + streamingOutput: Bool + ) -> Self { + var parameters: [ParameterDescription] = [ + ParameterDescription( + label: "request", + type: streamingInput ? .rpcAsyncSequence(forType: input) : .member(input) + ) + ] + + if streamingOutput { + parameters.append(ParameterDescription(label: "response", type: .rpcWriter(forType: output))) + } + + parameters.append(ParameterDescription(label: "context", type: .serverContext)) + + return FunctionSignatureDescription( + accessModifier: accessLevel, + kind: .function(name: name), + parameters: parameters, + keywords: [.async, .throws], + returnType: streamingOutput ? nil : .identifier(.pattern(output)) + ) + } +} + +extension ProtocolDescription { + /// ``` + /// protocol SimpleServiceProtocol: { + /// ... + /// } + /// ``` + static func simpleServiceProtocol( + accessModifier: AccessModifier? = nil, + name: String, + serviceProtocol: String, + methods: [MethodDescriptor] + ) -> Self { + func docs(for method: MethodDescriptor) -> String { + let summary = """ + /// Handle the "\(method.name.normalizedBase)" method. + """ + + let requestText = + method.isInputStreaming + ? "A stream of `\(method.inputType)` messages." + : "A `\(method.inputType)` message." + + var parameters = """ + /// - Parameters: + /// - request: \(requestText) + """ + + if method.isOutputStreaming { + parameters += "\n" + parameters += """ + /// - response: A response stream of `\(method.outputType)` messages. + """ + } + + parameters += "\n" + parameters += """ + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + """ + + if !method.isOutputStreaming { + parameters += "\n" + parameters += """ + /// - Returns: A `\(method.outputType)` to respond with. + """ + } + + return Docs.interposeDocs(method.documentation, between: summary, and: parameters) + } + + return ProtocolDescription( + accessModifier: accessModifier, + name: name, + conformances: [serviceProtocol], + members: methods.map { method in + .commentable( + .preFormatted(docs(for: method)), + .function( + signature: .simpleServerMethod( + name: method.name.generatedLowerCase, + input: method.inputType, + output: method.outputType, + streamingInput: method.isInputStreaming, + streamingOutput: method.isOutputStreaming + ) + ) + ) + } + ) + } +} + +extension FunctionCallDescription { + /// ``` + /// try await self.( + /// request: request.message, + /// response: writer, + /// context: context + /// ) + /// ``` + static func serviceMethodCallingSimpleMethod( + name: String, + input: String, + output: String, + streamingInput: Bool, + streamingOutput: Bool + ) -> Self { + var arguments: [FunctionArgumentDescription] = [ + FunctionArgumentDescription( + label: "request", + expression: .identifierPattern("request").dot(streamingInput ? "messages" : "message") + ) + ] + + if streamingOutput { + arguments.append( + FunctionArgumentDescription( + label: "response", + expression: .identifierPattern("writer") + ) + ) + } + + arguments.append( + FunctionArgumentDescription( + label: "context", + expression: .identifierPattern("context") + ) + ) + + return FunctionCallDescription( + calledExpression: .try(.await(.identifierPattern("self").dot(name))), + arguments: arguments + ) + } +} + +extension FunctionDescription { + /// ``` + /// func ( + /// request: GRPCCore.ServerRequest, + /// context: GRPCCore.ServerContext + /// ) async throws -> GRPCCore.ServerResponse { + /// return GRPCCore.ServerResponse( + /// message: try await self.( + /// request: request.message, + /// context: context + /// ) + /// metadata: [:] + /// ) + /// } + /// ``` + static func serviceProtocolDefaultImplementation( + accessModifier: AccessModifier? = nil, + name: String, + input: String, + output: String, + streamingInput: Bool, + streamingOutput: Bool + ) -> Self { + func makeUnaryOutputArguments() -> [FunctionArgumentDescription] { + return [ + FunctionArgumentDescription( + label: "message", + expression: .functionCall( + .serviceMethodCallingSimpleMethod( + name: name, + input: input, + output: output, + streamingInput: streamingInput, + streamingOutput: streamingOutput + ) + ) + ), + FunctionArgumentDescription(label: "metadata", expression: .literal(.dictionary([]))), + ] + } + + func makeStreamingOutputArguments() -> [FunctionArgumentDescription] { + return [ + FunctionArgumentDescription(label: "metadata", expression: .literal(.dictionary([]))), + FunctionArgumentDescription( + label: "producer", + expression: .closureInvocation( + argumentNames: ["writer"], + body: [ + .expression( + .functionCall( + .serviceMethodCallingSimpleMethod( + name: name, + input: input, + output: output, + streamingInput: streamingInput, + streamingOutput: streamingOutput + ) + ) + ), + .expression(.return(.literal(.dictionary([])))), + ] + ) + ), + ] + } + + return FunctionDescription( + signature: .serverMethod( + accessLevel: accessModifier, + name: name, + input: input, + output: output, + streamingInput: streamingInput, + streamingOutput: streamingOutput + ), + body: [ + .expression( + .functionCall( + calledExpression: .return( + .identifierType( + .serverResponse(forType: output, streaming: streamingOutput) + ) + ), + arguments: streamingOutput ? makeStreamingOutputArguments() : makeUnaryOutputArguments() + ) + ) + ] + ) + } +} + +extension ExtensionDescription { + /// ``` + /// extension ServiceProtocol { + /// ... + /// } + /// ``` + static func serviceProtocolDefaultImplementation( + accessModifier: AccessModifier? = nil, + on extensionName: String, + methods: [MethodDescriptor] + ) -> Self { + ExtensionDescription( + onType: extensionName, + declarations: methods.map { method in + .function( + .serviceProtocolDefaultImplementation( + accessModifier: accessModifier, + name: method.name.generatedLowerCase, + input: method.inputType, + output: method.outputType, + streamingInput: method.isInputStreaming, + streamingOutput: method.isOutputStreaming + ) + ) + } + ) + } +} diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwift+Types.swift b/Sources/GRPCCodeGen/Internal/StructuredSwift+Types.swift index ac3499037..4706e9888 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwift+Types.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwift+Types.swift @@ -70,6 +70,14 @@ extension ExistingTypeDescription { .generic(wrapper: .grpcCore("RPCWriter"), wrapped: .member(type)) } + package static func rpcAsyncSequence(forType type: String) -> Self { + .generic( + wrapper: .grpcCore("RPCAsyncSequence"), + wrapped: .member(type), + .any(.member(["Swift", "Error"])) + ) + } + package static let callOptions: Self = .grpcCore("CallOptions") package static let metadata: Self = .grpcCore("Metadata") package static let grpcClient: Self = .grpcCore("GRPCClient") diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift b/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift index ca44b7f79..c39be20d6 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift @@ -453,10 +453,10 @@ indirect enum ExistingTypeDescription: Equatable, Codable, Sendable { /// For example, `Foo?`. case optional(ExistingTypeDescription) - /// A wrapper type generic over a wrapped type. + /// A wrapper type generic over a list of wrapped types. /// /// For example, `Wrapper`. - case generic(wrapper: ExistingTypeDescription, wrapped: ExistingTypeDescription) + case generic(wrapper: ExistingTypeDescription, wrapped: [ExistingTypeDescription]) /// A type reference represented by the components. /// @@ -483,6 +483,16 @@ indirect enum ExistingTypeDescription: Equatable, Codable, Sendable { /// /// For example: `(String) async throws -> Int`. case closure(ClosureSignatureDescription) + + /// A wrapper type generic over a list of wrapped types. + /// + /// For example, `Wrapper`. + static func generic( + wrapper: ExistingTypeDescription, + wrapped: ExistingTypeDescription... + ) -> Self { + return .generic(wrapper: wrapper, wrapped: Array(wrapped)) + } } /// A description of a typealias declaration. diff --git a/Sources/GRPCCodeGen/Internal/Translator/Docs.swift b/Sources/GRPCCodeGen/Internal/Translator/Docs.swift index 5e0e57a11..1d4b1e1fd 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/Docs.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/Docs.swift @@ -56,7 +56,7 @@ package enum Docs { """ let body = docs.split(separator: "\n").map { line in - "/// > " + line.dropFirst(4) + "/// > " + line.dropFirst(4).trimmingCharacters(in: .whitespaces) }.joined(separator: "\n") return header + "\n" + body diff --git a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift index f272b374f..a2e91a83e 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift @@ -105,6 +105,24 @@ struct ServerCodeTranslator { ) ) ), + + // protocol SimpleServiceProtocol { ... } + .commentable( + .preFormatted( + Docs.suffix( + self.simpleServiceDocs(serviceName: service.fullyQualifiedName), + withDocs: service.documentation + ) + ), + .protocol( + .simpleServiceProtocol( + accessModifier: accessModifier, + name: "SimpleServiceProtocol", + serviceProtocol: "\(service.namespacedGeneratedName).ServiceProtocol", + methods: service.methods + ) + ) + ), ] ) blocks.append(.declaration(.extension(`extension`))) @@ -141,6 +159,19 @@ struct ServerCodeTranslator { ) ) + // extension _SimpleServiceProtocol { ... } + let serviceDefaultImplExtension: ExtensionDescription = .serviceProtocolDefaultImplementation( + accessModifier: accessModifier, + on: "\(service.namespacedGeneratedName).SimpleServiceProtocol", + methods: service.methods + ) + blocks.append( + CodeBlock( + comment: .inline("Default implementation of methods from 'ServiceProtocol'."), + item: .declaration(.extension(serviceDefaultImplExtension)) + ) + ) + return blocks } @@ -170,4 +201,14 @@ struct ServerCodeTranslator { /// use ``StreamingServiceProtocol``. """ } + + private func simpleServiceDocs(serviceName: String) -> String { + return """ + /// Simple service protocol for the "\(serviceName)" service. + /// + /// This is the highest level protocol for the service. The API is the easiest to use but + /// doesn't provide access to request or response metadata. If you need access to these + /// then use ``ServiceProtocol`` instead. + """ + } } diff --git a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ServerTests.swift b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ServerTests.swift index 2edde05a3..45567bafd 100644 --- a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ServerTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ServerTests.swift @@ -356,5 +356,99 @@ extension StructuedSwiftTests { #expect(render(.extension(decl)) == expected) } + + @Test( + "func (request:response:context:) (simple)", + arguments: AccessModifier.allCases, + RPCKind.allCases + ) + func simpleServerMethod(access: AccessModifier, kind: RPCKind) { + let decl: FunctionSignatureDescription = .simpleServerMethod( + accessLevel: access, + name: "foo", + input: "FooInput", + output: "FooOutput", + streamingInput: kind.streamsInput, + streamingOutput: kind.streamsOutput + ) + + let expected: String + switch kind { + case .unary: + expected = """ + \(access) func foo( + request: FooInput, + context: GRPCCore.ServerContext + ) async throws -> FooOutput + """ + + case .clientStreaming: + expected = """ + \(access) func foo( + request: GRPCCore.RPCAsyncSequence, + context: GRPCCore.ServerContext + ) async throws -> FooOutput + """ + + case .serverStreaming: + expected = """ + \(access) func foo( + request: FooInput, + response: GRPCCore.RPCWriter, + context: GRPCCore.ServerContext + ) async throws + """ + + case .bidirectionalStreaming: + expected = """ + \(access) func foo( + request: GRPCCore.RPCAsyncSequence, + response: GRPCCore.RPCWriter, + context: GRPCCore.ServerContext + ) async throws + """ + } + + #expect(render(.function(signature: decl)) == expected) + } + + @Test("protocol SimpleServiceProtocol { ... }", arguments: AccessModifier.allCases) + func simpleServiceProtocol(access: AccessModifier) { + let decl: ProtocolDescription = .simpleServiceProtocol( + accessModifier: access, + name: "SimpleServiceProtocol", + serviceProtocol: "ServiceProtocol", + methods: [ + .init( + documentation: "", + name: .init(base: "Foo", generatedUpperCase: "Foo", generatedLowerCase: "foo"), + isInputStreaming: false, + isOutputStreaming: false, + inputType: "Input", + outputType: "Output" + ) + ] + ) + + let expected = """ + \(access) protocol SimpleServiceProtocol: ServiceProtocol { + /// Handle the "Foo" method. + /// + /// - Parameters: + /// - request: A `Input` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `Output` to respond with. + func foo( + request: Input, + context: GRPCCore.ServerContext + ) async throws -> Output + } + """ + + #expect(render(.protocol(decl)) == expected) + } } } diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift index 778aed337..a64fe0451 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift @@ -261,6 +261,17 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { /// > /// > Documentation for AService public protocol ServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol {} + + /// Simple service protocol for the "namespaceA.ServiceA" service. + /// + /// This is the highest level protocol for the service. The API is the easiest to use but + /// doesn't provide access to request or response metadata. If you need access to these + /// then use ``ServiceProtocol`` instead. + /// + /// > Source IDL Documentation: + /// > + /// > Documentation for AService + public protocol SimpleServiceProtocol: NamespaceA_ServiceA.ServiceProtocol {} } // Default implementation of 'registerMethods(with:)'. @@ -271,6 +282,10 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { // Default implementation of streaming methods from 'StreamingServiceProtocol'. extension NamespaceA_ServiceA.ServiceProtocol { } + + // Default implementation of methods from 'ServiceProtocol'. + extension NamespaceA_ServiceA.SimpleServiceProtocol { + } """ try self.assertIDLToStructuredSwiftTranslation( codeGenerationRequest: makeCodeGenerationRequest( diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift index 099ff3934..1b48ddadb 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift @@ -112,6 +112,35 @@ final class ServerCodeTranslatorSnippetBasedTests { context: GRPCCore.ServerContext ) async throws -> GRPCCore.ServerResponse } + + /// Simple service protocol for the "namespaceA.AlongNameForServiceA" service. + /// + /// This is the highest level protocol for the service. The API is the easiest to use but + /// doesn't provide access to request or response metadata. If you need access to these + /// then use ``ServiceProtocol`` instead. + /// + /// > Source IDL Documentation: + /// > + /// > Documentation for ServiceA + public protocol SimpleServiceProtocol: NamespaceA_ServiceA.ServiceProtocol { + /// Handle the "UnaryMethod" method. + /// + /// > Source IDL Documentation: + /// > + /// > Documentation for unaryMethod + /// + /// - Parameters: + /// - request: A `NamespaceA_ServiceARequest` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `NamespaceA_ServiceAResponse` to respond with. + func unary( + request: NamespaceA_ServiceARequest, + context: GRPCCore.ServerContext + ) async throws -> NamespaceA_ServiceAResponse + } } // Default implementation of 'registerMethods(with:)'. extension NamespaceA_ServiceA.StreamingServiceProtocol { @@ -142,6 +171,21 @@ final class ServerCodeTranslatorSnippetBasedTests { return GRPCCore.StreamingServerResponse(single: response) } } + // Default implementation of methods from 'ServiceProtocol'. + extension NamespaceA_ServiceA.SimpleServiceProtocol { + public func unary( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.unary( + request: request.message, + context: context + ), + metadata: [:] + ) + } + } """ let rendered = self.render(accessLevel: .public, service: service)