diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwift+Client.swift b/Sources/GRPCCodeGen/Internal/StructuredSwift+Client.swift index b4e62194e..6c026f2d3 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwift+Client.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwift+Client.swift @@ -210,7 +210,7 @@ extension ProtocolDescription { conformances: ["Sendable"], members: methods.map { method in .commentable( - .preFormatted(method.documentation), + .preFormatted(docs(for: method)), .function( signature: .clientMethod( name: method.name.generatedLowerCase, @@ -251,16 +251,19 @@ extension ExtensionDescription { 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)) + .commentable( + .preFormatted(docs(for: method, serializers: false)), + .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)) + ) ) ) } @@ -495,11 +498,11 @@ extension ExtensionDescription { on extensionName: String, methods: [MethodDescriptor] ) -> ExtensionDescription { - ExtensionDescription( + return ExtensionDescription( onType: extensionName, declarations: methods.map { method in .commentable( - .preFormatted(method.documentation), + .preFormatted(explodedDocs(for: method)), .function( .clientMethodExploded( accessLevel: accessLevel, @@ -665,26 +668,36 @@ extension StructDescription { 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") + .commentable( + .preFormatted( + """ + /// Creates a new client wrapping the provided `GRPCCore.GRPCClient`. + /// + /// - Parameters: + /// - client: A `GRPCCore.GRPCClient` providing a communication channel to the service. + """ + ), + .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), + .preFormatted(docs(for: method)), .function( .clientMethod( accessLevel: accessLevel, @@ -702,3 +715,82 @@ extension StructDescription { ) } } + +private func docs( + for method: MethodDescriptor, + serializers includeSerializers: Bool = true +) -> String { + let summary = "/// Call the \"\(method.name.base)\" method." + + let request: String + if method.isInputStreaming { + request = "A streaming request producing `\(method.inputType)` messages." + } else { + request = "A request containing a single `\(method.inputType)` message." + } + + let parameters = """ + /// - Parameters: + /// - request: \(request) + """ + + let serializers = """ + /// - serializer: A serializer for `\(method.inputType)` messages. + /// - deserializer: A deserializer for `\(method.outputType)` messages. + """ + + let otherParameters = """ + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + """ + + let allParameters: String + if includeSerializers { + allParameters = parameters + "\n" + serializers + "\n" + otherParameters + } else { + allParameters = parameters + "\n" + otherParameters + } + + return Docs.interposeDocs(method.documentation, between: summary, and: allParameters) +} + +private func explodedDocs(for method: MethodDescriptor) -> String { + let summary = "/// Call the \"\(method.name.base)\" method." + var parameters = """ + /// - Parameters: + """ + + if !method.isInputStreaming { + parameters += "\n" + parameters += """ + /// - message: request message to send. + """ + } + + parameters += "\n" + parameters += """ + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + """ + + if method.isInputStreaming { + parameters += "\n" + parameters += """ + /// - producer: A closure producing request messages to send to the server. The request + /// stream is closed when the closure returns. + """ + } + + parameters += "\n" + parameters += """ + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + """ + + return Docs.interposeDocs(method.documentation, between: summary, and: parameters) +} diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwift+Server.swift b/Sources/GRPCCodeGen/Internal/StructuredSwift+Server.swift index c46986fa3..44093da6a 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwift+Server.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwift+Server.swift @@ -56,13 +56,31 @@ extension ProtocolDescription { name: String, methods: [MethodDescriptor] ) -> Self { + func docs(for method: MethodDescriptor) -> String { + let summary = """ + /// Handle the "\(method.name.normalizedBase)" method. + """ + + let parameters = """ + /// - Parameters: + /// - request: A streaming request of `\(method.inputType)` messages. + /// - 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 streaming response of `\(method.outputType)` messages. + """ + + return Docs.interposeDocs(method.documentation, between: summary, and: parameters) + } + return ProtocolDescription( accessModifier: accessLevel, name: name, conformances: ["GRPCCore.RegistrableRPCService"], members: methods.map { method in .commentable( - .preFormatted(method.documentation), + .preFormatted(docs(for: method)), .function( signature: .serverMethod( name: method.name.generatedLowerCase, @@ -123,13 +141,45 @@ extension ProtocolDescription { streamingProtocol: String, methods: [MethodDescriptor] ) -> Self { + func docs(for method: MethodDescriptor) -> String { + let summary = """ + /// Handle the "\(method.name.normalizedBase)" method. + """ + + let request: String + if method.isInputStreaming { + request = "A streaming request of `\(method.inputType)` messages." + } else { + request = "A request containing a single `\(method.inputType)` message." + } + + let returns: String + if method.isOutputStreaming { + returns = "A streaming response of `\(method.outputType)` messages." + } else { + returns = "A response containing a single `\(method.outputType)` message." + } + + let parameters = """ + /// - Parameters: + /// - request: \(request) + /// - 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: \(returns) + """ + + return Docs.interposeDocs(method.documentation, between: summary, and: parameters) + } + return ProtocolDescription( accessModifier: accessLevel, name: name, conformances: [streamingProtocol], members: methods.map { method in .commentable( - .preFormatted(method.documentation), + .preFormatted(docs(for: method)), .function( signature: .serverMethod( name: method.name.generatedLowerCase, diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift b/Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift index 5efa44749..ad7109a9d 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwift+ServiceMetadata.swift @@ -127,13 +127,16 @@ extension ExtensionDescription { return ExtensionDescription( onType: "GRPCCore.ServiceDescriptor", declarations: [ - .variable( - accessModifier: accessModifier, - isStatic: true, - kind: .let, - left: .identifier(.pattern(propertyName)), - right: .functionCall( - .serviceDescriptor(literalFullyQualifiedService: literalFullyQualifiedService) + .commentable( + .doc("Service descriptor for the \"\(literalFullyQualifiedService)\" service."), + .variable( + accessModifier: accessModifier, + isStatic: true, + kind: .let, + left: .identifier(.pattern(propertyName)), + right: .functionCall( + .serviceDescriptor(literalFullyQualifiedService: literalFullyQualifiedService) + ) ) ) ] @@ -183,13 +186,22 @@ extension EnumDescription { accessModifier: accessModifier, name: name, members: [ - .typealias(.methodInput(accessModifier: accessModifier, name: inputType)), - .typealias(.methodOutput(accessModifier: accessModifier, name: outputType)), - .variable( - .methodDescriptor( - accessModifier: accessModifier, - literalFullyQualifiedService: literalFullyQualifiedService, - literalMethodName: literalMethod + .commentable( + .doc("Request type for \"\(literalMethod)\"."), + .typealias(.methodInput(accessModifier: accessModifier, name: inputType)) + ), + .commentable( + .doc("Response type for \"\(literalMethod)\"."), + .typealias(.methodOutput(accessModifier: accessModifier, name: outputType)) + ), + .commentable( + .doc("Descriptor for \"\(literalMethod)\"."), + .variable( + .methodDescriptor( + accessModifier: accessModifier, + literalFullyQualifiedService: literalFullyQualifiedService, + literalMethodName: literalMethod + ) ) ), ] @@ -222,14 +234,17 @@ extension EnumDescription { // Add a namespace for each method. let methodNamespaces: [Declaration] = methods.map { method in - return .enum( - .methodNamespace( - accessModifier: accessModifier, - name: method.name.base, - literalMethod: method.name.base, - literalFullyQualifiedService: literalFullyQualifiedService, - inputType: method.inputType, - outputType: method.outputType + return .commentable( + .doc("Namespace for \"\(method.name.base)\" metadata."), + .enum( + .methodNamespace( + accessModifier: accessModifier, + name: method.name.base, + literalMethod: method.name.base, + literalFullyQualifiedService: literalFullyQualifiedService, + inputType: method.inputType, + outputType: method.outputType + ) ) ) } @@ -240,7 +255,12 @@ extension EnumDescription { accessModifier: accessModifier, methodNamespaceNames: methods.map { $0.name.base } ) - description.members.append(.variable(methodDescriptorsArray)) + description.members.append( + .commentable( + .doc("Descriptors for all methods in the \"\(literalFullyQualifiedService)\" service."), + .variable(methodDescriptorsArray) + ) + ) return description } @@ -266,7 +286,12 @@ extension EnumDescription { accessModifier: accessModifier, literalFullyQualifiedService: literalFullyQualifiedService ) - description.members.append(.variable(descriptor)) + description.members.append( + .commentable( + .doc("Service descriptor for the \"\(literalFullyQualifiedService)\" service."), + .variable(descriptor) + ) + ) // enum Method { ... } let methodsNamespace: EnumDescription = .methodsNamespace( @@ -274,7 +299,12 @@ extension EnumDescription { literalFullyQualifiedService: literalFullyQualifiedService, methods: methods ) - description.members.append(.enum(methodsNamespace)) + description.members.append( + .commentable( + .doc("Namespace for method metadata."), + .enum(methodsNamespace) + ) + ) return description } @@ -302,7 +332,14 @@ extension [CodeBlock] { literalFullyQualifiedService: service.fullyQualifiedName, methods: service.methods ) - blocks.append(CodeBlock(item: .declaration(.enum(serviceNamespace)))) + blocks.append( + CodeBlock( + comment: .doc( + "Namespace containing generated types for the \"\(service.fullyQualifiedName)\" service." + ), + item: .declaration(.enum(serviceNamespace)) + ) + ) let descriptorExtension: ExtensionDescription = .serviceDescriptor( accessModifier: accessModifier, diff --git a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift index d994fb6a0..b77cddbb9 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ClientCodeTranslator.swift @@ -91,7 +91,12 @@ struct ClientCodeTranslator { declarations: [ // protocol ClientProtocol { ... } .commentable( - .preFormatted(service.documentation), + .preFormatted( + Docs.suffix( + self.clientProtocolDocs(serviceName: service.fullyQualifiedName), + withDocs: service.documentation + ) + ), .protocol( .clientProtocol( accessLevel: accessModifier, @@ -103,7 +108,12 @@ struct ClientCodeTranslator { // struct Client: ClientProtocol { ... } .commentable( - .preFormatted(service.documentation), + .preFormatted( + Docs.suffix( + self.clientDocs(serviceName: service.fullyQualifiedName), + withDocs: service.documentation + ) + ), .struct( .client( accessLevel: accessModifier, @@ -126,7 +136,10 @@ struct ClientCodeTranslator { deserializer: deserializer ) blocks.append( - CodeBlock(item: .declaration(.extension(extensionWithDefaults))) + CodeBlock( + comment: .inline("Helpers providing default arguments to 'ClientProtocol' methods."), + item: .declaration(.extension(extensionWithDefaults)) + ) ) let extensionWithExplodedAPI: ExtensionDescription = .explodedClientMethods( @@ -135,9 +148,31 @@ struct ClientCodeTranslator { methods: service.methods ) blocks.append( - CodeBlock(item: .declaration(.extension(extensionWithExplodedAPI))) + CodeBlock( + comment: .inline("Helpers providing sugared APIs for 'ClientProtocol' methods."), + item: .declaration(.extension(extensionWithExplodedAPI)) + ) ) return blocks } + + private func clientProtocolDocs(serviceName: String) -> String { + return """ + /// Generated client protocol for the "\(serviceName)" service. + /// + /// You don't need to implement this protocol directly, use the generated + /// implementation, ``Client``. + """ + } + + private func clientDocs(serviceName: String) -> String { + return """ + /// Generated client for the "\(serviceName)" service. + /// + /// The ``Client`` provides an implementation of ``ClientProtocol`` which wraps + /// a `GRPCCore.GRPCCClient`. The underlying `GRPCClient` provides the long-lived + /// means of communication with the remote peer. + """ + } } diff --git a/Sources/GRPCCodeGen/Internal/Translator/Docs.swift b/Sources/GRPCCodeGen/Internal/Translator/Docs.swift new file mode 100644 index 000000000..5e0e57a11 --- /dev/null +++ b/Sources/GRPCCodeGen/Internal/Translator/Docs.swift @@ -0,0 +1,64 @@ +/* + * 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. + */ + +package enum Docs { + package static func suffix(_ header: String, withDocs footer: String) -> String { + if footer.isEmpty { + return header + } else { + let docs = """ + /// + \(Self.inlineDocsAsNote(footer)) + """ + return header + "\n" + docs + } + } + + package static func interposeDocs( + _ docs: String, + between header: String, + and footer: String + ) -> String { + let middle: String + + if docs.isEmpty { + middle = """ + /// + """ + } else { + middle = """ + /// + \(Self.inlineDocsAsNote(docs)) + /// + """ + } + + return header + "\n" + middle + "\n" + footer + } + + private static func inlineDocsAsNote(_ docs: String) -> String { + let header = """ + /// > Source IDL Documentation: + /// > + """ + + let body = docs.split(separator: "\n").map { line in + "/// > " + line.dropFirst(4) + }.joined(separator: "\n") + + return header + "\n" + body + } +} diff --git a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift index 20f78d3b3..f272b374f 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/ServerCodeTranslator.swift @@ -73,7 +73,12 @@ struct ServerCodeTranslator { declarations: [ // protocol StreamingServiceProtocol { ... } .commentable( - .preFormatted(service.documentation), + .preFormatted( + Docs.suffix( + self.streamingServiceDocs(serviceName: service.fullyQualifiedName), + withDocs: service.documentation + ) + ), .protocol( .streamingService( accessLevel: accessModifier, @@ -85,7 +90,12 @@ struct ServerCodeTranslator { // protocol ServiceProtocol { ... } .commentable( - .preFormatted(service.documentation), + .preFormatted( + Docs.suffix( + self.serviceDocs(serviceName: service.fullyQualifiedName), + withDocs: service.documentation + ) + ), .protocol( .service( accessLevel: accessModifier, @@ -110,7 +120,7 @@ struct ServerCodeTranslator { ) blocks.append( CodeBlock( - comment: .doc("Conformance to `GRPCCore.RegistrableRPCService`."), + comment: .inline("Default implementation of 'registerMethods(with:)'."), item: .declaration(.extension(registerExtension)) ) ) @@ -122,8 +132,42 @@ struct ServerCodeTranslator { on: "\(service.namespacedGeneratedName).ServiceProtocol", methods: service.methods ) - blocks.append(.declaration(.extension(streamingServiceDefaultImplExtension))) + blocks.append( + CodeBlock( + comment: .inline( + "Default implementation of streaming methods from 'StreamingServiceProtocol'." + ), + item: .declaration(.extension(streamingServiceDefaultImplExtension)) + ) + ) return blocks } + + private func streamingServiceDocs(serviceName: String) -> String { + return """ + /// Streaming variant of the service protocol for the "\(serviceName)" service. + /// + /// This protocol is the lowest-level of the service protocols generated for this service + /// giving you the most flexibility over the implementation of your service. This comes at + /// the cost of more verbose and less strict APIs. Each RPC requires you to implement it in + /// terms of a request stream and response stream. Where only a single request or response + /// message is expected, you are responsible for enforcing this invariant is maintained. + /// + /// Where possible, prefer using the stricter, less-verbose ``ServiceProtocol`` + /// or ``SimpleServiceProtocol`` instead. + """ + } + + private func serviceDocs(serviceName: String) -> String { + return """ + /// Service protocol for the "\(serviceName)" service. + /// + /// This protocol is higher level than ``StreamingServiceProtocol`` but lower level than + /// the ``SimpleServiceProtocol``, it provides access to request and response metadata and + /// trailing response metadata. If you don't need these then consider using + /// the ``SimpleServiceProtocol``. If you need fine grained control over your RPCs then + /// use ``StreamingServiceProtocol``. + """ + } } diff --git a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ClientTests.swift b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ClientTests.swift index a4abc0ee3..50d6f7ada 100644 --- a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ClientTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ClientTests.swift @@ -114,7 +114,21 @@ extension StructuedSwiftTests { let expected = """ \(access) protocol Foo_ClientProtocol: Sendable { - /// Some docs + /// Call the "Bar" method. + /// + /// > Source IDL Documentation: + /// > + /// > Some docs + /// + /// - Parameters: + /// - request: A request containing a single `BarInput` message. + /// - serializer: A serializer for `BarInput` messages. + /// - deserializer: A deserializer for `BarOutput` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. func bar( request: GRPCCore.ClientRequest, serializer: some GRPCCore.MessageSerializer, @@ -186,6 +200,15 @@ extension StructuedSwiftTests { let expected = """ extension Foo_ClientProtocol { + /// Call the "Bar" method. + /// + /// - Parameters: + /// - request: A request containing a single `BarInput` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. \(access) func bar( request: GRPCCore.ClientRequest, options: GRPCCore.CallOptions = .defaults, @@ -330,7 +353,20 @@ extension StructuedSwiftTests { let expected = """ extension Foo_ClientProtocol { - /// Some docs + /// Call the "Bar" method. + /// + /// > Source IDL Documentation: + /// > + /// > Some docs + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. \(access) func bar( _ message: Input, metadata: GRPCCore.Metadata = [:], @@ -456,11 +492,29 @@ extension StructuedSwiftTests { \(access) struct FooClient: Foo_ClientProtocol { private let client: GRPCCore.GRPCClient + /// Creates a new client wrapping the provided `GRPCCore.GRPCClient`. + /// + /// - Parameters: + /// - client: A `GRPCCore.GRPCClient` providing a communication channel to the service. \(access) init(wrapping client: GRPCCore.GRPCClient) { self.client = client } - /// Unary docs + /// Call the "Unary" method. + /// + /// > Source IDL Documentation: + /// > + /// > Unary docs + /// + /// - Parameters: + /// - request: A request containing a single `Input` message. + /// - serializer: A serializer for `Input` messages. + /// - deserializer: A deserializer for `Output` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. \(access) func unary( request: GRPCCore.ClientRequest, serializer: some GRPCCore.MessageSerializer, @@ -480,7 +534,21 @@ extension StructuedSwiftTests { ) } - /// ClientStreaming docs + /// Call the "ClientStreaming" method. + /// + /// > Source IDL Documentation: + /// > + /// > ClientStreaming docs + /// + /// - Parameters: + /// - request: A streaming request producing `Input` messages. + /// - serializer: A serializer for `Input` messages. + /// - deserializer: A deserializer for `Output` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. \(access) func clientStreaming( request: GRPCCore.StreamingClientRequest, serializer: some GRPCCore.MessageSerializer, @@ -500,7 +568,21 @@ extension StructuedSwiftTests { ) } - /// ServerStreaming docs + /// Call the "ServerStreaming" method. + /// + /// > Source IDL Documentation: + /// > + /// > ServerStreaming docs + /// + /// - Parameters: + /// - request: A request containing a single `Input` message. + /// - serializer: A serializer for `Input` messages. + /// - deserializer: A deserializer for `Output` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. \(access) func serverStreaming( request: GRPCCore.ClientRequest, serializer: some GRPCCore.MessageSerializer, @@ -518,7 +600,21 @@ extension StructuedSwiftTests { ) } - /// BidiStreaming docs + /// Call the "BidiStreaming" method. + /// + /// > Source IDL Documentation: + /// > + /// > BidiStreaming docs + /// + /// - Parameters: + /// - request: A streaming request producing `Input` messages. + /// - serializer: A serializer for `Input` messages. + /// - deserializer: A deserializer for `Output` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. \(access) func bidiStreaming( request: GRPCCore.StreamingClientRequest, serializer: some GRPCCore.MessageSerializer, diff --git a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+MetadataTests.swift b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+MetadataTests.swift index 6169098e8..8107b159c 100644 --- a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+MetadataTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+MetadataTests.swift @@ -81,6 +81,7 @@ extension StructuedSwiftTests { let expected = """ extension GRPCCore.ServiceDescriptor { + /// Service descriptor for the "echo.EchoService" service. \(access) static let foo = GRPCCore.ServiceDescriptor(fullyQualifiedService: "echo.EchoService") } """ @@ -120,8 +121,11 @@ extension StructuedSwiftTests { let expected = """ \(access) enum Foo { + /// Request type for "Foo". \(access) typealias Input = FooInput + /// Response type for "Foo". \(access) typealias Output = FooOutput + /// Descriptor for "Foo". \(access) static let descriptor = GRPCCore.MethodDescriptor( service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "bar.Bar"), method: "Foo" @@ -150,14 +154,19 @@ extension StructuedSwiftTests { let expected = """ \(access) enum Method { + /// Namespace for "Foo" metadata. \(access) enum Foo { + /// Request type for "Foo". \(access) typealias Input = FooInput + /// Response type for "Foo". \(access) typealias Output = FooOutput + /// Descriptor for "Foo". \(access) static let descriptor = GRPCCore.MethodDescriptor( service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "bar.Bar"), method: "Foo" ) } + /// Descriptors for all methods in the "bar.Bar" service. \(access) static let descriptors: [GRPCCore.MethodDescriptor] = [ Foo.descriptor ] @@ -176,6 +185,7 @@ extension StructuedSwiftTests { let expected = """ \(access) enum Method { + /// Descriptors for all methods in the "bar.Bar" service. \(access) static let descriptors: [GRPCCore.MethodDescriptor] = [] } """ @@ -202,16 +212,23 @@ extension StructuedSwiftTests { let expected = """ \(access) enum Foo { + /// Service descriptor for the "Foo" service. \(access) static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: "Foo") + /// Namespace for method metadata. \(access) enum Method { + /// Namespace for "Bar" metadata. \(access) enum Bar { + /// Request type for "Bar". \(access) typealias Input = BarInput + /// Response type for "Bar". \(access) typealias Output = BarOutput + /// Descriptor for "Bar". \(access) static let descriptor = GRPCCore.MethodDescriptor( service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "Foo"), method: "Bar" ) } + /// Descriptors for all methods in the "Foo" service. \(access) static let descriptors: [GRPCCore.MethodDescriptor] = [ Bar.descriptor ] @@ -232,8 +249,11 @@ extension StructuedSwiftTests { let expected = """ \(access) enum Foo { + /// Service descriptor for the "Foo" service. \(access) static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: "Foo") + /// Namespace for method metadata. \(access) enum Method { + /// Descriptors for all methods in the "Foo" service. \(access) static let descriptors: [GRPCCore.MethodDescriptor] = [] } } diff --git a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ServerTests.swift b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ServerTests.swift index 78dbac42d..2edde05a3 100644 --- a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ServerTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ServerTests.swift @@ -91,7 +91,19 @@ extension StructuedSwiftTests { let expected = """ \(access) protocol FooService: GRPCCore.RegistrableRPCService { - /// Some docs + /// Handle the "Foo" method. + /// + /// > Source IDL Documentation: + /// > + /// > Some docs + /// + /// - Parameters: + /// - request: A streaming request of `FooInput` messages. + /// - 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 streaming response of `FooOutput` messages. func foo( request: GRPCCore.StreamingServerRequest, context: GRPCCore.ServerContext @@ -122,7 +134,19 @@ extension StructuedSwiftTests { let expected = """ \(access) protocol FooService: FooService_StreamingServiceProtocol { - /// Some docs + /// Handle the "Foo" method. + /// + /// > Source IDL Documentation: + /// > + /// > Some docs + /// + /// - Parameters: + /// - request: A request containing a single `FooInput` 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 response containing a single `FooOutput` message. func foo( request: GRPCCore.ServerRequest, context: GRPCCore.ServerContext diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift index 3516f4d67..8c40eb7f4 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ClientCodeTranslatorSnippetBasedTests.swift @@ -40,9 +40,30 @@ struct ClientCodeTranslatorSnippetBasedTests { let expectedSwift = """ extension NamespaceA_ServiceA { - /// Documentation for ServiceA + /// Generated client protocol for the "namespaceA.ServiceA" service. + /// + /// You don't need to implement this protocol directly, use the generated + /// implementation, ``Client``. + /// + /// > Source IDL Documentation: + /// > + /// > Documentation for ServiceA public protocol ClientProtocol: Sendable { - /// Documentation for MethodA + /// Call the "MethodA" method. + /// + /// > Source IDL Documentation: + /// > + /// > Documentation for MethodA + /// + /// - Parameters: + /// - request: A request containing a single `NamespaceA_ServiceARequest` message. + /// - serializer: A serializer for `NamespaceA_ServiceARequest` messages. + /// - deserializer: A deserializer for `NamespaceA_ServiceAResponse` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. func methodA( request: GRPCCore.ClientRequest, serializer: some GRPCCore.MessageSerializer, @@ -52,15 +73,41 @@ struct ClientCodeTranslatorSnippetBasedTests { ) async throws -> Result where Result: Sendable } - /// Documentation for ServiceA + /// Generated client for the "namespaceA.ServiceA" service. + /// + /// The ``Client`` provides an implementation of ``ClientProtocol`` which wraps + /// a `GRPCCore.GRPCCClient`. The underlying `GRPCClient` provides the long-lived + /// means of communication with the remote peer. + /// + /// > Source IDL Documentation: + /// > + /// > Documentation for ServiceA public struct Client: ClientProtocol { private let client: GRPCCore.GRPCClient + /// Creates a new client wrapping the provided `GRPCCore.GRPCClient`. + /// + /// - Parameters: + /// - client: A `GRPCCore.GRPCClient` providing a communication channel to the service. public init(wrapping client: GRPCCore.GRPCClient) { self.client = client } - /// Documentation for MethodA + /// Call the "MethodA" method. + /// + /// > Source IDL Documentation: + /// > + /// > Documentation for MethodA + /// + /// - Parameters: + /// - request: A request containing a single `NamespaceA_ServiceARequest` message. + /// - serializer: A serializer for `NamespaceA_ServiceARequest` messages. + /// - deserializer: A deserializer for `NamespaceA_ServiceAResponse` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. public func methodA( request: GRPCCore.ClientRequest, serializer: some GRPCCore.MessageSerializer, @@ -81,7 +128,21 @@ struct ClientCodeTranslatorSnippetBasedTests { } } } + // Helpers providing default arguments to 'ClientProtocol' methods. extension NamespaceA_ServiceA.ClientProtocol { + /// Call the "MethodA" method. + /// + /// > Source IDL Documentation: + /// > + /// > Documentation for MethodA + /// + /// - Parameters: + /// - request: A request containing a single `NamespaceA_ServiceARequest` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. public func methodA( request: GRPCCore.ClientRequest, options: GRPCCore.CallOptions = .defaults, @@ -98,8 +159,22 @@ struct ClientCodeTranslatorSnippetBasedTests { ) } } + // Helpers providing sugared APIs for 'ClientProtocol' methods. extension NamespaceA_ServiceA.ClientProtocol { - /// Documentation for MethodA + /// Call the "MethodA" method. + /// + /// > Source IDL Documentation: + /// > + /// > Documentation for MethodA + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. public func methodA( _ message: NamespaceA_ServiceARequest, metadata: GRPCCore.Metadata = [:], diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/DocsTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/DocsTests.swift new file mode 100644 index 000000000..69426663b --- /dev/null +++ b/Tests/GRPCCodeGenTests/Internal/Translator/DocsTests.swift @@ -0,0 +1,101 @@ +/* + * 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 GRPCCodeGen +import Testing + +@Suite("Docs tests") +struct DocsTests { + @Test("Suffix with additional docs") + func suffixWithAdditional() { + let foo = """ + /// Foo + """ + + let additional = """ + /// Some additional pre-formatted docs + /// split over multiple lines. + """ + + let expected = """ + /// Foo + /// + /// > Source IDL Documentation: + /// > + /// > Some additional pre-formatted docs + /// > split over multiple lines. + """ + #expect(Docs.suffix(foo, withDocs: additional) == expected) + } + + @Test("Suffix with empty additional docs") + func suffixWithEmptyAdditional() { + let foo = """ + /// Foo + """ + + let additional = "" + #expect(Docs.suffix(foo, withDocs: additional) == foo) + } + + @Test("Interpose additional docs") + func interposeDocs() { + let header = """ + /// Header + """ + + let footer = """ + /// Footer + """ + + let additionalDocs = """ + /// Additional docs + /// On multiple lines + """ + + let expected = """ + /// Header + /// + /// > Source IDL Documentation: + /// > + /// > Additional docs + /// > On multiple lines + /// + /// Footer + """ + + #expect(Docs.interposeDocs(additionalDocs, between: header, and: footer) == expected) + } + + @Test("Interpose empty additional docs") + func interposeEmpty() { + let header = """ + /// Header + """ + + let footer = """ + /// Footer + """ + + let expected = """ + /// Header + /// + /// Footer + """ + + #expect(Docs.interposeDocs("", between: header, and: footer) == expected) + } +} diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift index 7ed4727bb..778aed337 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift @@ -214,32 +214,61 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { // MARK: - namespaceA.ServiceA + /// Namespace containing generated types for the "namespaceA.ServiceA" service. public enum NamespaceA_ServiceA { + /// Service descriptor for the "namespaceA.ServiceA" service. public static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: "namespaceA.ServiceA") + /// Namespace for method metadata. public enum Method { + /// Descriptors for all methods in the "namespaceA.ServiceA" service. public static let descriptors: [GRPCCore.MethodDescriptor] = [] } } extension GRPCCore.ServiceDescriptor { + /// Service descriptor for the "namespaceA.ServiceA" service. public static let namespaceA_ServiceA = GRPCCore.ServiceDescriptor(fullyQualifiedService: "namespaceA.ServiceA") } // MARK: namespaceA.ServiceA (server) extension NamespaceA_ServiceA { - /// Documentation for AService + /// Streaming variant of the service protocol for the "namespaceA.ServiceA" service. + /// + /// This protocol is the lowest-level of the service protocols generated for this service + /// giving you the most flexibility over the implementation of your service. This comes at + /// the cost of more verbose and less strict APIs. Each RPC requires you to implement it in + /// terms of a request stream and response stream. Where only a single request or response + /// message is expected, you are responsible for enforcing this invariant is maintained. + /// + /// Where possible, prefer using the stricter, less-verbose ``ServiceProtocol`` + /// or ``SimpleServiceProtocol`` instead. + /// + /// > Source IDL Documentation: + /// > + /// > Documentation for AService public protocol StreamingServiceProtocol: GRPCCore.RegistrableRPCService {} - /// Documentation for AService + /// Service protocol for the "namespaceA.ServiceA" service. + /// + /// This protocol is higher level than ``StreamingServiceProtocol`` but lower level than + /// the ``SimpleServiceProtocol``, it provides access to request and response metadata and + /// trailing response metadata. If you don't need these then consider using + /// the ``SimpleServiceProtocol``. If you need fine grained control over your RPCs then + /// use ``StreamingServiceProtocol``. + /// + /// > Source IDL Documentation: + /// > + /// > Documentation for AService public protocol ServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol {} } - /// Conformance to `GRPCCore.RegistrableRPCService`. + // Default implementation of 'registerMethods(with:)'. extension NamespaceA_ServiceA.StreamingServiceProtocol { public func registerMethods(with router: inout GRPCCore.RPCRouter) {} } + // Default implementation of streaming methods from 'StreamingServiceProtocol'. extension NamespaceA_ServiceA.ServiceProtocol { } """ diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift index 7cb57cc18..099ff3934 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/ServerCodeTranslatorSnippetBasedTests.swift @@ -48,25 +48,72 @@ final class ServerCodeTranslatorSnippetBasedTests { let expectedSwift = """ extension NamespaceA_ServiceA { - /// Documentation for ServiceA + /// Streaming variant of the service protocol for the "namespaceA.AlongNameForServiceA" service. + /// + /// This protocol is the lowest-level of the service protocols generated for this service + /// giving you the most flexibility over the implementation of your service. This comes at + /// the cost of more verbose and less strict APIs. Each RPC requires you to implement it in + /// terms of a request stream and response stream. Where only a single request or response + /// message is expected, you are responsible for enforcing this invariant is maintained. + /// + /// Where possible, prefer using the stricter, less-verbose ``ServiceProtocol`` + /// or ``SimpleServiceProtocol`` instead. + /// + /// > Source IDL Documentation: + /// > + /// > Documentation for ServiceA public protocol StreamingServiceProtocol: GRPCCore.RegistrableRPCService { - /// Documentation for unaryMethod + /// Handle the "UnaryMethod" method. + /// + /// > Source IDL Documentation: + /// > + /// > Documentation for unaryMethod + /// + /// - Parameters: + /// - request: A streaming request of `NamespaceA_ServiceARequest` messages. + /// - 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 streaming response of `NamespaceA_ServiceAResponse` messages. func unary( request: GRPCCore.StreamingServerRequest, context: GRPCCore.ServerContext ) async throws -> GRPCCore.StreamingServerResponse } - /// Documentation for ServiceA + /// Service protocol for the "namespaceA.AlongNameForServiceA" service. + /// + /// This protocol is higher level than ``StreamingServiceProtocol`` but lower level than + /// the ``SimpleServiceProtocol``, it provides access to request and response metadata and + /// trailing response metadata. If you don't need these then consider using + /// the ``SimpleServiceProtocol``. If you need fine grained control over your RPCs then + /// use ``StreamingServiceProtocol``. + /// + /// > Source IDL Documentation: + /// > + /// > Documentation for ServiceA public protocol ServiceProtocol: NamespaceA_ServiceA.StreamingServiceProtocol { - /// Documentation for unaryMethod + /// Handle the "UnaryMethod" method. + /// + /// > Source IDL Documentation: + /// > + /// > Documentation for unaryMethod + /// + /// - Parameters: + /// - request: A request containing a single `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 response containing a single `NamespaceA_ServiceAResponse` message. func unary( request: GRPCCore.ServerRequest, context: GRPCCore.ServerContext ) async throws -> GRPCCore.ServerResponse } } - /// Conformance to `GRPCCore.RegistrableRPCService`. + // Default implementation of 'registerMethods(with:)'. extension NamespaceA_ServiceA.StreamingServiceProtocol { public func registerMethods(with router: inout GRPCCore.RPCRouter) { router.registerHandler( @@ -82,6 +129,7 @@ final class ServerCodeTranslatorSnippetBasedTests { ) } } + // Default implementation of streaming methods from 'StreamingServiceProtocol'. extension NamespaceA_ServiceA.ServiceProtocol { public func unary( request: GRPCCore.StreamingServerRequest, diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift index 504dbcec3..8713b784a 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/TypealiasTranslatorSnippetBasedTests.swift @@ -42,23 +42,32 @@ struct TypealiasTranslatorSnippetBasedTests { ) let expectedSwift = """ + /// Namespace containing generated types for the "namespaceA.ServiceA" service. public enum NamespaceA_ServiceA { + /// Service descriptor for the "namespaceA.ServiceA" service. public static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: "namespaceA.ServiceA") + /// Namespace for method metadata. public enum Method { + /// Namespace for "MethodA" metadata. public enum MethodA { + /// Request type for "MethodA". public typealias Input = NamespaceA_ServiceARequest + /// Response type for "MethodA". public typealias Output = NamespaceA_ServiceAResponse + /// Descriptor for "MethodA". public static let descriptor = GRPCCore.MethodDescriptor( service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "namespaceA.ServiceA"), method: "MethodA" ) } + /// Descriptors for all methods in the "namespaceA.ServiceA" service. public static let descriptors: [GRPCCore.MethodDescriptor] = [ MethodA.descriptor ] } } extension GRPCCore.ServiceDescriptor { + /// Service descriptor for the "namespaceA.ServiceA" service. public static let namespaceA_ServiceA = GRPCCore.ServiceDescriptor(fullyQualifiedService: "namespaceA.ServiceA") } """