diff --git a/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift b/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift index 3a0f1ee59..ddb340c66 100644 --- a/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift +++ b/Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift @@ -30,7 +30,7 @@ /// A description of an import declaration. /// /// For example: `import Foo`. -struct ImportDescription: Equatable, Codable, Sendable { +package struct ImportDescription: Equatable, Codable, Sendable { /// The access level of the imported module. /// /// For example, the `public` in `public import Foo`. diff --git a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift index 839ef0fa1..612768cdd 100644 --- a/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift +++ b/Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift @@ -17,7 +17,9 @@ /// Creates a representation for the server and client code, as well as for the enums containing useful type aliases and properties. /// The representation is generated based on the ``CodeGenerationRequest`` object and user specifications, /// using types from ``StructuredSwiftRepresentation``. -struct IDLToStructuredSwiftTranslator: Translator { +package struct IDLToStructuredSwiftTranslator: Translator { + package init() {} + func translate( codeGenerationRequest: CodeGenerationRequest, accessLevel: SourceGenerator.Config.AccessLevel, @@ -72,13 +74,23 @@ struct IDLToStructuredSwiftTranslator: Translator { } } - let fileDescription = FileDescription( - topComment: .preFormatted(codeGenerationRequest.leadingTrivia), - imports: try self.makeImports( + let imports: [ImportDescription] + if codeGenerationRequest.services.isEmpty { + imports = [] + codeBlocks.append( + CodeBlock(comment: .inline("This file contained no services.")) + ) + } else { + imports = try self.makeImports( dependencies: codeGenerationRequest.dependencies, accessLevel: accessLevel, accessLevelOnImports: accessLevelOnImports - ), + ) + } + + let fileDescription = FileDescription( + topComment: .preFormatted(codeGenerationRequest.leadingTrivia), + imports: imports, codeBlocks: codeBlocks ) @@ -87,7 +99,7 @@ struct IDLToStructuredSwiftTranslator: Translator { return StructuredSwiftRepresentation(file: file) } - private func makeImports( + package func makeImports( dependencies: [Dependency], accessLevel: SourceGenerator.Config.AccessLevel, accessLevelOnImports: Bool diff --git a/Sources/GRPCCodeGen/SourceGenerator.swift b/Sources/GRPCCodeGen/SourceGenerator.swift index 454258bc6..e8a92ec5f 100644 --- a/Sources/GRPCCodeGen/SourceGenerator.swift +++ b/Sources/GRPCCodeGen/SourceGenerator.swift @@ -61,8 +61,8 @@ public struct SourceGenerator: Sendable { /// The possible access levels for the generated code. public struct AccessLevel: Sendable, Hashable { - internal var level: Level - internal enum Level { + package var level: Level + package enum Level { case `internal` case `public` case `package` @@ -94,6 +94,7 @@ public struct SourceGenerator: Sendable { client: self.config.client, server: self.config.server ) + let sourceFile = try textRenderer.render(structured: structuredSwiftRepresentation) return sourceFile diff --git a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ClientTests.swift b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ClientTests.swift index 50d6f7ada..caa8c9bc5 100644 --- a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ClientTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ClientTests.swift @@ -18,7 +18,7 @@ import Testing @testable import GRPCCodeGen -extension StructuedSwiftTests { +extension StructuredSwiftTests { @Suite("Client") struct Client { @Test( diff --git a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ImportTests.swift b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ImportTests.swift new file mode 100644 index 000000000..ff1d34dbe --- /dev/null +++ b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ImportTests.swift @@ -0,0 +1,201 @@ +/* + * Copyright 2025, 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 + +extension StructuredSwiftTests { + @Suite("Import") + struct Import { + static let translator = IDLToStructuredSwiftTranslator() + + static let allAccessLevels: [SourceGenerator.Config.AccessLevel] = [ + .internal, .public, .package, + ] + + @Test( + "import rendering", + arguments: allAccessLevels + ) + func imports(accessLevel: SourceGenerator.Config.AccessLevel) throws { + var dependencies = [Dependency]() + dependencies.append(Dependency(module: "Foo", accessLevel: .public)) + dependencies.append( + Dependency( + item: .init(kind: .typealias, name: "Bar"), + module: "Foo", + accessLevel: .internal + ) + ) + dependencies.append( + Dependency( + item: .init(kind: .struct, name: "Baz"), + module: "Foo", + accessLevel: .package + ) + ) + dependencies.append( + Dependency( + item: .init(kind: .class, name: "Bac"), + module: "Foo", + accessLevel: .package + ) + ) + dependencies.append( + Dependency( + item: .init(kind: .enum, name: "Bap"), + module: "Foo", + accessLevel: .package + ) + ) + dependencies.append( + Dependency( + item: .init(kind: .protocol, name: "Bat"), + module: "Foo", + accessLevel: .package + ) + ) + dependencies.append( + Dependency( + item: .init(kind: .let, name: "Baq"), + module: "Foo", + accessLevel: .package + ) + ) + dependencies.append( + Dependency( + item: .init(kind: .var, name: "Bag"), + module: "Foo", + accessLevel: .package + ) + ) + dependencies.append( + Dependency( + item: .init(kind: .func, name: "Bak"), + module: "Foo", + accessLevel: .package + ) + ) + + let expected = + """ + \(accessLevel.level) import GRPCCore + public import Foo + internal import typealias Foo.Bar + package import struct Foo.Baz + package import class Foo.Bac + package import enum Foo.Bap + package import protocol Foo.Bat + package import let Foo.Baq + package import var Foo.Bag + package import func Foo.Bak + """ + + let imports = try StructuredSwiftTests.Import.translator.makeImports( + dependencies: dependencies, + accessLevel: accessLevel, + accessLevelOnImports: true + ) + + #expect(render(imports) == expected) + } + + @Test( + "preconcurrency import rendering", + arguments: allAccessLevels + ) + func preconcurrencyImports(accessLevel: SourceGenerator.Config.AccessLevel) throws { + var dependencies = [Dependency]() + dependencies.append( + Dependency( + module: "Foo", + preconcurrency: .required, + accessLevel: .internal + ) + ) + dependencies.append( + Dependency( + item: .init(kind: .enum, name: "Bar"), + module: "Foo", + preconcurrency: .required, + accessLevel: .internal + ) + ) + dependencies.append( + Dependency( + module: "Baz", + preconcurrency: .requiredOnOS(["Deq", "Der"]), + accessLevel: .internal + ) + ) + + let expected = + """ + \(accessLevel.level) import GRPCCore + @preconcurrency internal import Foo + @preconcurrency internal import enum Foo.Bar + #if os(Deq) || os(Der) + @preconcurrency internal import Baz + #else + internal import Baz + #endif + """ + + let imports = try StructuredSwiftTests.Import.translator.makeImports( + dependencies: dependencies, + accessLevel: accessLevel, + accessLevelOnImports: true + ) + + #expect(render(imports) == expected) + } + + @Test( + "SPI import rendering", + arguments: allAccessLevels + ) + func spiImports(accessLevel: SourceGenerator.Config.AccessLevel) throws { + var dependencies = [Dependency]() + dependencies.append( + Dependency(module: "Foo", spi: "Secret", accessLevel: .internal) + ) + dependencies.append( + Dependency( + item: .init(kind: .enum, name: "Bar"), + module: "Foo", + spi: "Secret", + accessLevel: .internal + ) + ) + + let expected = + """ + \(accessLevel.level) import GRPCCore + @_spi(Secret) internal import Foo + @_spi(Secret) internal import enum Foo.Bar + """ + + let imports = try StructuredSwiftTests.Import.translator.makeImports( + dependencies: dependencies, + accessLevel: accessLevel, + accessLevelOnImports: true + ) + + #expect(render(imports) == expected) + } + + } +} diff --git a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+MetadataTests.swift b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+MetadataTests.swift index 8107b159c..1b8d9afd2 100644 --- a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+MetadataTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+MetadataTests.swift @@ -18,7 +18,7 @@ import Testing @testable import GRPCCodeGen -extension StructuedSwiftTests { +extension StructuredSwiftTests { @Suite("Metadata") struct Metadata { @Test("typealias Input = ", arguments: AccessModifier.allCases) diff --git a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ServerTests.swift b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ServerTests.swift index 45567bafd..a415307a6 100644 --- a/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ServerTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/StructuredSwift+ServerTests.swift @@ -18,7 +18,7 @@ import Testing @testable import GRPCCodeGen -extension StructuedSwiftTests { +extension StructuredSwiftTests { @Suite("Server") struct Server { @Test( diff --git a/Tests/GRPCCodeGenTests/Internal/StructuredSwiftTestHelpers.swift b/Tests/GRPCCodeGenTests/Internal/StructuredSwiftTestHelpers.swift index d364e843f..b2c0d2847 100644 --- a/Tests/GRPCCodeGenTests/Internal/StructuredSwiftTestHelpers.swift +++ b/Tests/GRPCCodeGenTests/Internal/StructuredSwiftTestHelpers.swift @@ -19,8 +19,8 @@ import Testing @testable import GRPCCodeGen // Used as a namespace for organising other structured swift tests. -@Suite("Structued Swift") -struct StructuedSwiftTests {} +@Suite("Structured Swift") +struct StructuredSwiftTests {} func render(_ declaration: Declaration) -> String { let renderer = TextBasedRenderer(indentation: 2) @@ -40,6 +40,12 @@ func render(_ blocks: [CodeBlock]) -> String { return renderer.renderedContents() } +func render(_ imports: [ImportDescription]) -> String { + let renderer = TextBasedRenderer(indentation: 2) + renderer.renderImports(imports) + return renderer.renderedContents() +} + enum RPCKind: Hashable, Sendable, CaseIterable { case unary case clientStreaming diff --git a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift index a64fe0451..d30fe8954 100644 --- a/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift +++ b/Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift @@ -21,164 +21,6 @@ import XCTest @testable import GRPCCodeGen final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { - func testImports() throws { - var dependencies = [Dependency]() - dependencies.append(Dependency(module: "Foo", accessLevel: .public)) - dependencies.append( - Dependency( - item: .init(kind: .typealias, name: "Bar"), - module: "Foo", - accessLevel: .internal - ) - ) - dependencies.append( - Dependency( - item: .init(kind: .struct, name: "Baz"), - module: "Foo", - accessLevel: .package - ) - ) - dependencies.append( - Dependency( - item: .init(kind: .class, name: "Bac"), - module: "Foo", - accessLevel: .package - ) - ) - dependencies.append( - Dependency( - item: .init(kind: .enum, name: "Bap"), - module: "Foo", - accessLevel: .package - ) - ) - dependencies.append( - Dependency( - item: .init(kind: .protocol, name: "Bat"), - module: "Foo", - accessLevel: .package - ) - ) - dependencies.append( - Dependency( - item: .init(kind: .let, name: "Baq"), - module: "Foo", - accessLevel: .package - ) - ) - dependencies.append( - Dependency( - item: .init(kind: .var, name: "Bag"), - module: "Foo", - accessLevel: .package - ) - ) - dependencies.append( - Dependency( - item: .init(kind: .func, name: "Bak"), - module: "Foo", - accessLevel: .package - ) - ) - - let expectedSwift = - """ - /// Some really exciting license header 2023. - - public import GRPCCore - public import Foo - internal import typealias Foo.Bar - package import struct Foo.Baz - package import class Foo.Bac - package import enum Foo.Bap - package import protocol Foo.Bat - package import let Foo.Baq - package import var Foo.Bag - package import func Foo.Bak - - """ - try self.assertIDLToStructuredSwiftTranslation( - codeGenerationRequest: makeCodeGenerationRequest(dependencies: dependencies), - expectedSwift: expectedSwift, - accessLevel: .public - ) - } - - func testPreconcurrencyImports() throws { - var dependencies = [Dependency]() - dependencies.append( - Dependency( - module: "Foo", - preconcurrency: .required, - accessLevel: .internal - ) - ) - dependencies.append( - Dependency( - item: .init(kind: .enum, name: "Bar"), - module: "Foo", - preconcurrency: .required, - accessLevel: .internal - ) - ) - dependencies.append( - Dependency( - module: "Baz", - preconcurrency: .requiredOnOS(["Deq", "Der"]), - accessLevel: .internal - ) - ) - let expectedSwift = - """ - /// Some really exciting license header 2023. - - public import GRPCCore - @preconcurrency internal import Foo - @preconcurrency internal import enum Foo.Bar - #if os(Deq) || os(Der) - @preconcurrency internal import Baz - #else - internal import Baz - #endif - - """ - try self.assertIDLToStructuredSwiftTranslation( - codeGenerationRequest: makeCodeGenerationRequest(dependencies: dependencies), - expectedSwift: expectedSwift, - accessLevel: .public - ) - } - - func testSPIImports() throws { - var dependencies = [Dependency]() - dependencies.append( - Dependency(module: "Foo", spi: "Secret", accessLevel: .internal) - ) - dependencies.append( - Dependency( - item: .init(kind: .enum, name: "Bar"), - module: "Foo", - spi: "Secret", - accessLevel: .internal - ) - ) - - let expectedSwift = - """ - /// Some really exciting license header 2023. - - public import GRPCCore - @_spi(Secret) internal import Foo - @_spi(Secret) internal import enum Foo.Bar - - """ - try self.assertIDLToStructuredSwiftTranslation( - codeGenerationRequest: makeCodeGenerationRequest(dependencies: dependencies), - expectedSwift: expectedSwift, - accessLevel: .public - ) - } - func testGeneration() throws { var dependencies = [Dependency]() dependencies.append( @@ -298,6 +140,25 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase { ) } + func testEmptyFileGeneration() throws { + let expectedSwift = + """ + /// Some really exciting license header 2023. + + + // This file contained no services. + """ + try self.assertIDLToStructuredSwiftTranslation( + codeGenerationRequest: makeCodeGenerationRequest( + services: [], + dependencies: [] + ), + expectedSwift: expectedSwift, + accessLevel: .public, + server: true + ) + } + private func assertIDLToStructuredSwiftTranslation( codeGenerationRequest: CodeGenerationRequest, expectedSwift: String,