Kaynağa Gözat

[CodeGenLib] Translating dependencies into StructuredSwiftRepresentation (#1752)

Translating dependencies into StructuredSwiftRepresentation

Motivation:

Dependency representations within CodeGeneratorRequest and StructuredSwiftRepresentation don't match (each contains fields that the other one doesn't have). The CodeGenerationRequest representation of a dependency doesn't contain preconcurrency requirements and spi, while the StructuredSwiftRepresentation doesn't allow items imports (func, class etc.). We need to add the missing fields to both representation in  order to translate the imports, without losing information.

Modifications:

- Added the preconcurrency ans spi parameters to the CodeGenerationRequest Dependency struct (and the PreconcurrencyRequirement struct to represent that encapsulates the possible requirement types)
- Added the item property to the StructuredSwiftRepresentation ImportDescription struct (and the Item struct backed up by an item representing the possible cases).
- Created the function that translates a CodeGenerationRequest.Dependency into a StructuredSwiftRepresentation.ImportDescription.
- Added the item rendering in the TextBasedRenderer.
- Added tests for the TextRenderer and the IDLToStructuredSwiftRepresentationTranslator that check the imports.

Result:

Imports can now be translated and added in the generated code.
Stefana-Ioana Dranca 2 yıl önce
ebeveyn
işleme
f7641a0be5

+ 6 - 0
Sources/GRPCCodeGen/CodeGenError.swift

@@ -38,6 +38,7 @@ extension CodeGenError {
     private enum Value {
       case nonUniqueServiceName
       case nonUniqueMethodName
+      case invalidKind
     }
 
     private var value: Value
@@ -54,6 +55,11 @@ extension CodeGenError {
     public static var nonUniqueMethodName: Self {
       Self(.nonUniqueMethodName)
     }
+
+    /// An invalid kind name is used for an import.
+    public static var invalidKind: Self {
+      Self(.invalidKind)
+    }
   }
 }
 

+ 48 - 2
Sources/GRPCCodeGen/CodeGenerationRequest.swift

@@ -88,9 +88,25 @@ public struct CodeGenerationRequest {
     /// The name of the imported module or of the module an item is imported from.
     public var module: String
 
-    public init(item: Item? = nil, module: String) {
+    /// The name of the private interface for an `@_spi` import.
+    ///
+    /// For example, if `spi` was "Secret" and the module name was "Foo" then the import
+    /// would be `@_spi(Secret) import Foo`.
+    public var spi: String?
+
+    /// Requirements for the `@preconcurrency` attribute.
+    public var preconcurrency: PreconcurrencyRequirement
+
+    public init(
+      item: Item? = nil,
+      module: String,
+      spi: String? = nil,
+      preconcurrency: PreconcurrencyRequirement = .notRequired
+    ) {
       self.item = item
       self.module = module
+      self.spi = spi
+      self.preconcurrency = preconcurrency
     }
 
     /// Represents an item imported from a module.
@@ -109,7 +125,7 @@ public struct CodeGenerationRequest {
       /// Represents the imported item's kind.
       public struct Kind {
         /// Describes the keyword associated with the imported item.
-        internal enum Value {
+        internal enum Value: String {
           case `typealias`
           case `struct`
           case `class`
@@ -167,6 +183,36 @@ public struct CodeGenerationRequest {
         }
       }
     }
+
+    /// Describes any requirement for the `@preconcurrency` attribute.
+    public struct PreconcurrencyRequirement {
+      internal enum Value {
+        case required
+        case notRequired
+        case requiredOnOS([String])
+      }
+
+      internal var value: Value
+
+      internal init(_ value: Value) {
+        self.value = value
+      }
+
+      /// The attribute is always required.
+      public static var required: Self {
+        Self(.required)
+      }
+
+      /// The attribute is not required.
+      public static var notRequired: Self {
+        Self(.notRequired)
+      }
+
+      /// The attribute is required only on the named operating systems.
+      public static func requiredOnOS(_ OSs: [String]) -> PreconcurrencyRequirement {
+        return Self(.requiredOnOS(OSs))
+      }
+    }
   }
 
   /// Represents a service described in an IDL file.

+ 6 - 1
Sources/GRPCCodeGen/Internal/Renderer/TextBasedRenderer.swift

@@ -169,7 +169,12 @@ struct TextBasedRenderer: RendererProtocol {
     func render(preconcurrency: Bool) {
       let spiPrefix = description.spi.map { "@_spi(\($0)) " } ?? ""
       let preconcurrencyPrefix = preconcurrency ? "@preconcurrency " : ""
-      if let moduleTypes = description.moduleTypes {
+
+      if let item = description.item {
+        writer.writeLine(
+          "\(preconcurrencyPrefix)\(spiPrefix)import \(item.kind) \(description.moduleName).\(item.name)"
+        )
+      } else if let moduleTypes = description.moduleTypes {
         for type in moduleTypes {
           writer.writeLine("\(preconcurrencyPrefix)\(spiPrefix)import \(type)")
         }

+ 29 - 1
Sources/GRPCCodeGen/Internal/StructuredSwiftRepresentation.swift

@@ -31,7 +31,6 @@
 ///
 /// For example: `import Foo`.
 struct ImportDescription: Equatable, Codable {
-
   /// The name of the imported module.
   ///
   /// For example, the `Foo` in `import Foo`.
@@ -51,6 +50,10 @@ struct ImportDescription: Equatable, Codable {
   /// Requirements for the `@preconcurrency` attribute.
   var preconcurrency: PreconcurrencyRequirement = .never
 
+  /// If the dependency is an item, the property's value is the item representation.
+  /// If the dependency is a module, this property is nil.
+  var item: Item? = nil
+
   /// Describes any requirement for the `@preconcurrency` attribute.
   enum PreconcurrencyRequirement: Equatable, Codable {
     /// The attribute is always required.
@@ -60,6 +63,31 @@ struct ImportDescription: Equatable, Codable {
     /// The attribute is required only on the named operating systems.
     case onOS([String])
   }
+
+  /// Represents an item imported from a module.
+  struct Item: Equatable, Codable {
+    /// The keyword that specifies the item's kind (e.g. `func`, `struct`).
+    var kind: Kind
+
+    /// The name of the imported item.
+    var name: String
+
+    init(kind: Kind, name: String) {
+      self.kind = kind
+      self.name = name
+    }
+  }
+
+  enum Kind: String, Equatable, Codable {
+    case `typealias`
+    case `struct`
+    case `class`
+    case `enum`
+    case `protocol`
+    case `let`
+    case `var`
+    case `func`
+  }
 }
 
 /// A description of an access modifier.

+ 34 - 3
Sources/GRPCCodeGen/Internal/Translator/IDLToStructuredSwiftTranslator.swift

@@ -23,9 +23,11 @@ struct IDLToStructuredSwiftTranslator: Translator {
     try self.validateInput(codeGenerationRequest)
     let typealiasTranslator = TypealiasTranslator(client: client, server: server)
     let topComment = Comment.doc(codeGenerationRequest.leadingTrivia)
-    let imports: [ImportDescription] = [
-      ImportDescription(moduleName: "GRPCCore")
-    ]
+    let imports = try codeGenerationRequest.dependencies.reduce(
+      into: [ImportDescription(moduleName: "GRPCCore")]
+    ) { partialResult, newDependency in
+      try partialResult.append(translateImport(dependency: newDependency))
+    }
 
     var codeBlocks: [CodeBlock] = []
     codeBlocks.append(
@@ -58,6 +60,35 @@ struct IDLToStructuredSwiftTranslator: Translator {
 }
 
 extension IDLToStructuredSwiftTranslator {
+  private func translateImport(
+    dependency: CodeGenerationRequest.Dependency
+  ) throws -> ImportDescription {
+    var importDescription = ImportDescription(moduleName: dependency.module)
+    if let item = dependency.item {
+      if let matchedKind = ImportDescription.Kind(rawValue: item.kind.value.rawValue) {
+        importDescription.item = ImportDescription.Item(kind: matchedKind, name: item.name)
+      } else {
+        throw CodeGenError(
+          code: .invalidKind,
+          message: "Invalid kind name for import: \(item.kind.value.rawValue)"
+        )
+      }
+    }
+    if let spi = dependency.spi {
+      importDescription.spi = spi
+    }
+
+    switch dependency.preconcurrency.value {
+    case .required:
+      importDescription.preconcurrency = .always
+    case .notRequired:
+      importDescription.preconcurrency = .never
+    case .requiredOnOS(let OSs):
+      importDescription.preconcurrency = .onOS(OSs)
+    }
+    return importDescription
+  }
+
   private func validateInput(_ codeGenerationRequest: CodeGenerationRequest) throws {
     let servicesByNamespace = Dictionary(
       grouping: codeGenerationRequest.services,

+ 54 - 0
Tests/GRPCCodeGenTests/Internal/Renderer/TextBasedRendererTests.swift

@@ -130,6 +130,60 @@ final class Test_TextBasedRenderer: XCTestCase {
         @preconcurrency @_spi(Secret) import Bar
         """#
     )
+
+    try _test(
+      [
+        ImportDescription(
+          moduleName: "Foo",
+          item: ImportDescription.Item(kind: .typealias, name: "Bar")
+        ),
+        ImportDescription(
+          moduleName: "Foo",
+          item: ImportDescription.Item(kind: .struct, name: "Baz")
+        ),
+        ImportDescription(
+          moduleName: "Foo",
+          item: ImportDescription.Item(kind: .class, name: "Bac")
+        ),
+        ImportDescription(
+          moduleName: "Foo",
+          item: ImportDescription.Item(kind: .enum, name: "Bap")
+        ),
+        ImportDescription(
+          moduleName: "Foo",
+          item: ImportDescription.Item(kind: .protocol, name: "Bat")
+        ),
+        ImportDescription(moduleName: "Foo", item: ImportDescription.Item(kind: .let, name: "Bam")),
+        ImportDescription(moduleName: "Foo", item: ImportDescription.Item(kind: .var, name: "Bag")),
+        ImportDescription(
+          moduleName: "Foo",
+          item: ImportDescription.Item(kind: .func, name: "Bak")
+        ),
+        ImportDescription(
+          moduleName: "Foo",
+          spi: "Secret",
+          item: ImportDescription.Item(kind: .func, name: "SecretBar")
+        ),
+        ImportDescription(
+          moduleName: "Foo",
+          preconcurrency: .always,
+          item: ImportDescription.Item(kind: .func, name: "PreconcurrencyBar")
+        ),
+      ],
+      renderedBy: TextBasedRenderer.renderImports,
+      rendersAs: #"""
+        import typealias Foo.Bar
+        import struct Foo.Baz
+        import class Foo.Bac
+        import enum Foo.Bap
+        import protocol Foo.Bat
+        import let Foo.Bam
+        import var Foo.Bag
+        import func Foo.Bak
+        @_spi(Secret) import func Foo.SecretBar
+        @preconcurrency import func Foo.PreconcurrencyBar
+        """#
+    )
   }
 
   func testAccessModifiers() throws {

+ 122 - 0
Tests/GRPCCodeGenTests/Internal/Translator/IDLToStructuredSwiftTranslatorSnippetBasedTests.swift

@@ -24,6 +24,128 @@ final class IDLToStructuredSwiftTranslatorSnippetBasedTests: XCTestCase {
   typealias MethodDescriptor = GRPCCodeGen.CodeGenerationRequest.ServiceDescriptor.MethodDescriptor
   typealias ServiceDescriptor = GRPCCodeGen.CodeGenerationRequest.ServiceDescriptor
 
+  func testImports() throws {
+    var dependencies = [CodeGenerationRequest.Dependency]()
+    dependencies.append(CodeGenerationRequest.Dependency(module: "Foo"))
+    dependencies.append(
+      CodeGenerationRequest.Dependency(item: .init(kind: .typealias, name: "Bar"), module: "Foo")
+    )
+    dependencies.append(
+      CodeGenerationRequest.Dependency(item: .init(kind: .struct, name: "Baz"), module: "Foo")
+    )
+    dependencies.append(
+      CodeGenerationRequest.Dependency(item: .init(kind: .class, name: "Bac"), module: "Foo")
+    )
+    dependencies.append(
+      CodeGenerationRequest.Dependency(item: .init(kind: .enum, name: "Bap"), module: "Foo")
+    )
+    dependencies.append(
+      CodeGenerationRequest.Dependency(item: .init(kind: .protocol, name: "Bat"), module: "Foo")
+    )
+    dependencies.append(
+      CodeGenerationRequest.Dependency(item: .init(kind: .let, name: "Baq"), module: "Foo")
+    )
+    dependencies.append(
+      CodeGenerationRequest.Dependency(item: .init(kind: .var, name: "Bag"), module: "Foo")
+    )
+    dependencies.append(
+      CodeGenerationRequest.Dependency(item: .init(kind: .func, name: "Bak"), module: "Foo")
+    )
+
+    let expectedSwift =
+      """
+      /// Some really exciting license header 2023.
+      import GRPCCore
+      import Foo
+      import typealias Foo.Bar
+      import struct Foo.Baz
+      import class Foo.Bac
+      import enum Foo.Bap
+      import protocol Foo.Bat
+      import let Foo.Baq
+      import var Foo.Bag
+      import func Foo.Bak
+      """
+    try self.assertIDLToStructuredSwiftTranslation(
+      codeGenerationRequest: makeCodeGenerationRequest(dependencies: dependencies),
+      expectedSwift: expectedSwift
+    )
+  }
+
+  func testPreconcurrencyImports() throws {
+    var dependencies = [CodeGenerationRequest.Dependency]()
+    dependencies.append(CodeGenerationRequest.Dependency(module: "Foo", preconcurrency: .required))
+    dependencies.append(
+      CodeGenerationRequest.Dependency(
+        item: .init(kind: .enum, name: "Bar"),
+        module: "Foo",
+        preconcurrency: .required
+      )
+    )
+    dependencies.append(
+      CodeGenerationRequest.Dependency(
+        module: "Baz",
+        preconcurrency: .requiredOnOS(["Deq", "Der"])
+      )
+    )
+    let expectedSwift =
+      """
+      /// Some really exciting license header 2023.
+      import GRPCCore
+      @preconcurrency import Foo
+      @preconcurrency import enum Foo.Bar
+      #if os(Deq) || os(Der)
+      @preconcurrency import Baz
+      #else
+      import Baz
+      #endif
+      """
+    try self.assertIDLToStructuredSwiftTranslation(
+      codeGenerationRequest: makeCodeGenerationRequest(dependencies: dependencies),
+      expectedSwift: expectedSwift
+    )
+  }
+
+  func testSPIImports() 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 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),
+      expectedSwift: expectedSwift
+    )
+  }
+
+  private func assertIDLToStructuredSwiftTranslation(
+    codeGenerationRequest: CodeGenerationRequest,
+    expectedSwift: String
+  ) throws {
+    let translator = IDLToStructuredSwiftTranslator()
+    let structuredSwift = try translator.translate(
+      codeGenerationRequest: codeGenerationRequest,
+      client: false,
+      server: false
+    )
+    let renderer = TextBasedRenderer.default
+    let sourceFile = try renderer.render(structured: structuredSwift)
+    let contents = sourceFile.contents
+    try XCTAssertEqualWithDiff(contents, expectedSwift)
+  }
+
   func testSameNameServicesNoNamespaceError() throws {
     let serviceA = ServiceDescriptor(
       documentation: "Documentation for AService",

+ 17 - 0
Tests/GRPCCodeGenTests/Internal/Translator/TestFunctions.swift

@@ -88,6 +88,23 @@ internal func makeCodeGenerationRequest(
   )
 }
 
+internal func makeCodeGenerationRequest(
+  dependencies: [CodeGenerationRequest.Dependency]
+) -> CodeGenerationRequest {
+  return CodeGenerationRequest(
+    fileName: "test.grpc",
+    leadingTrivia: "Some really exciting license header 2023.",
+    dependencies: dependencies,
+    services: [],
+    lookupSerializer: {
+      "ProtobufSerializer<\($0)>()"
+    },
+    lookupDeserializer: {
+      "ProtobufDeserializer<\($0)>()"
+    }
+  )
+}
+
 internal func XCTAssertThrowsError<T, E: Error>(
   ofType: E.Type,
   _ expression: @autoclosure () throws -> T,