diff --git a/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift b/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift index 3fd683720..3c9b1845c 100644 --- a/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift +++ b/Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift @@ -136,8 +136,14 @@ struct TextBasedRenderer: RendererProtocol { /// Renders the specified Swift file. func renderFile(_ description: FileDescription) { - if let topComment = description.topComment { renderComment(topComment) } - if let imports = description.imports { renderImports(imports) } + if let topComment = description.topComment { + renderComment(topComment) + writer.writeLine("") + } + if let imports = description.imports { + renderImports(imports) + writer.writeLine("") + } for codeBlock in description.codeBlocks { renderCodeBlock(codeBlock) writer.writeLine("") @@ -165,15 +171,18 @@ struct TextBasedRenderer: RendererProtocol { prefix = "" commentString = string } - if prefix.isEmpty { - writer.writeLine(commentString) - } else { - let lines = commentString.transformingLines { line in - if line.isEmpty { return prefix } - return "\(prefix) \(line)" + + let lines = commentString.transformingLines { line, isLast in + // The last line of a comment that is blank should be dropped. + // Pre formatted documentation might contain such lines. + if line.isEmpty && prefix.isEmpty && isLast { + return nil + } else { + let formattedPrefix = !prefix.isEmpty && !line.isEmpty ? "\(prefix) " : prefix + return "\(formattedPrefix)\(line)" } - lines.forEach(writer.writeLine) } + lines.forEach(writer.writeLine) } /// Renders the specified import statements. @@ -1095,7 +1104,9 @@ extension String { /// The closure takes a string representing one line as a parameter. /// - Parameter work: The closure that transforms each line. /// - Returns: A new string where each line has been transformed using the given closure. - fileprivate func transformingLines(_ work: (String) -> String) -> [String] { asLines().map(work) } + fileprivate func transformingLines(_ work: (String, Bool) -> String?) -> [String] { + asLines().enumeratedWithLastMarker().compactMap(work) + } } extension TextBasedRenderer { diff --git a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift index b35a85d1b..e195b45b4 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift @@ -38,7 +38,7 @@ struct IDLToStructuredSwiftTranslator: Translator { try partialResult.append(translateImport(dependency: newDependency)) } - var codeBlocks: [CodeBlock] = [] + var codeBlocks = [CodeBlock]() codeBlocks.append( contentsOf: try typealiasTranslator.translate(from: codeGenerationRequest) ) diff --git a/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift index 308aa83a9..49a92fa33 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/TypealiasTranslator.swift @@ -64,7 +64,7 @@ struct TypealiasTranslator: SpecializedTranslator { } func translate(from codeGenerationRequest: CodeGenerationRequest) throws -> [CodeBlock] { - var codeBlocks: [CodeBlock] = [] + var codeBlocks = [CodeBlock]() let services = codeGenerationRequest.services let servicesByNamespace = Dictionary( grouping: services, diff --git a/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift b/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift index bae3b2632..6d40aa56e 100644 --- a/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift @@ -89,6 +89,22 @@ final class Test_TextBasedRenderer: XCTestCase { // Also, bar """# ) + try _test( + .preFormatted("/// Lorem ipsum\n"), + renderedBy: TextBasedRenderer.renderComment, + rendersAs: """ + /// Lorem ipsum + """ + ) + try _test( + .preFormatted("/// Lorem ipsum\n\n/// Lorem ipsum\n"), + renderedBy: TextBasedRenderer.renderComment, + rendersAs: """ + /// Lorem ipsum + + /// Lorem ipsum + """ + ) } func testImports() throws { @@ -825,7 +841,9 @@ final class Test_TextBasedRenderer: XCTestCase { renderedBy: TextBasedRenderer.renderFile, rendersAs: #""" // hi + import Foo + struct Bar {} """# @@ -847,7 +865,9 @@ final class Test_TextBasedRenderer: XCTestCase { renderedBy: TextBasedRenderer.renderFile, rendersAs: #""" // hi + import Foo + struct Bar { struct Baz {} } diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift index 3dfec1358..58a10cb3d 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift @@ -56,6 +56,7 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ /// Some really exciting license header 2023. + import GRPCCore import Foo import typealias Foo.Bar @@ -66,6 +67,7 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { import let Foo.Baq import var Foo.Bag import func Foo.Bak + """ try self.assertIDLToStructuredSwiftTranslation( codeGenerationRequest: makeCodeGenerationRequest(dependencies: dependencies), @@ -93,6 +95,7 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ /// Some really exciting license header 2023. + import GRPCCore @preconcurrency import Foo @preconcurrency import enum Foo.Bar @@ -101,6 +104,7 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { #else import Baz #endif + """ try self.assertIDLToStructuredSwiftTranslation( codeGenerationRequest: makeCodeGenerationRequest(dependencies: dependencies), @@ -123,9 +127,11 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { let expectedSwift = """ /// Some really exciting license header 2023. + import GRPCCore @_spi(Secret) import Foo @_spi(Secret) import enum Foo.Bar + """ try self.assertIDLToStructuredSwiftTranslation( codeGenerationRequest: makeCodeGenerationRequest(dependencies: dependencies), @@ -134,17 +140,84 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { ) } + func testGeneration() throws { + var dependencies = [CodeGenerationRequest.Dependency]() + dependencies.append(CodeGenerationRequest.Dependency(module: "Foo", spi: "Secret")) + dependencies.append( + CodeGenerationRequest.Dependency( + item: .init(kind: .enum, name: "Bar"), + module: "Foo", + spi: "Secret" + ) + ) + + let serviceA = ServiceDescriptor( + documentation: "/// Documentation for AService\n", + name: Name(base: "ServiceA", generatedUpperCase: "ServiceA", generatedLowerCase: "serviceA"), + namespace: Name( + base: "namespaceA", + generatedUpperCase: "NamespaceA", + generatedLowerCase: "namespaceA" + ), + methods: [] + ) + + let expectedSwift = + """ + /// Some really exciting license header 2023. + + import GRPCCore + @_spi(Secret) import Foo + @_spi(Secret) import enum Foo.Bar + + public enum NamespaceA { + public enum ServiceA { + public enum Methods {} + public static let methods: [MethodDescriptor] = [] + public typealias StreamingServiceProtocol = NamespaceA_ServiceAServiceStreamingProtocol + public typealias ServiceProtocol = NamespaceA_ServiceAServiceProtocol + } + } + + /// Documentation for AService + public protocol NamespaceA_ServiceAStreamingServiceProtocol: GRPCCore.RegistrableRPCService {} + + /// Conformance to `GRPCCore.RegistrableRPCService`. + extension NamespaceA.ServiceA.StreamingServiceProtocol { + public func registerMethods(with router: inout GRPCCore.RPCRouter) {} + } + + /// Documentation for AService + public protocol NamespaceA_ServiceAServiceProtocol: NamespaceA.ServiceA.StreamingServiceProtocol {} + + /// Partial conformance to `NamespaceA_ServiceAStreamingServiceProtocol`. + extension NamespaceA.ServiceA.ServiceProtocol { + } + + """ + try self.assertIDLToStructuredSwiftTranslation( + codeGenerationRequest: makeCodeGenerationRequest( + services: [serviceA], + dependencies: dependencies + ), + expectedSwift: expectedSwift, + accessLevel: .public, + server: true + ) + } + private func assertIDLToStructuredSwiftTranslation( codeGenerationRequest: CodeGenerationRequest, expectedSwift: String, - accessLevel: SourceGenerator.Configuration.AccessLevel + accessLevel: SourceGenerator.Configuration.AccessLevel, + server: Bool = false ) throws { let translator = IDLToStructuredSwiftTranslator() let structuredSwift = try translator.translate( codeGenerationRequest: codeGenerationRequest, accessLevel: accessLevel, client: false, - server: false + server: server ) let renderer = TextBasedRenderer.default let sourceFile = try renderer.render(structured: structuredSwift) @@ -262,7 +335,7 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { func testSameGeneratedNameServicesSameNamespaceError() throws { let serviceA = ServiceDescriptor( - documentation: "Documentation for AService", + documentation: "/// Documentation for AService\n", name: Name(base: "AService", generatedUpperCase: "AService", generatedLowerCase: "aService"), namespace: Name( base: "namespacea", @@ -272,7 +345,7 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { methods: [] ) let serviceB = ServiceDescriptor( - documentation: "Documentation for BService", + documentation: "/// Documentation for BService\n", name: Name(base: "BService", generatedUpperCase: "AService", generatedLowerCase: "aService"), namespace: Name( base: "namespacea", diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift b/Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift index 800ff7393..fd5808816 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift @@ -72,30 +72,14 @@ internal func XCTAssertEqualWithDiff( } internal func makeCodeGenerationRequest( - services: [CodeGenerationRequest.ServiceDescriptor] + services: [CodeGenerationRequest.ServiceDescriptor] = [], + dependencies: [CodeGenerationRequest.Dependency] = [] ) -> CodeGenerationRequest { return CodeGenerationRequest( fileName: "test.grpc", - leadingTrivia: "/// Some really exciting license header 2023.", - dependencies: [], - services: services, - lookupSerializer: { - "ProtobufSerializer<\($0)>()" - }, - lookupDeserializer: { - "ProtobufDeserializer<\($0)>()" - } - ) -} - -internal func makeCodeGenerationRequest( - dependencies: [CodeGenerationRequest.Dependency] -) -> CodeGenerationRequest { - return CodeGenerationRequest( - fileName: "test.grpc", - leadingTrivia: "/// Some really exciting license header 2023.", + leadingTrivia: "/// Some really exciting license header 2023.\n", dependencies: dependencies, - services: [], + services: services, lookupSerializer: { "ProtobufSerializer<\($0)>()" },