Browse Source

[CodeGen Protobuf support] SwiftProtobuf parser to CodeGenerationRequest (#1772)

Motivation:

We need the parser to transform a FileDescriptor object into a CodeGenerationRequest objet which can be used as input in the new code generation process.

Modifications:

- Created the parser struct
- Created a test for the type

Result:

FileDescriptors can now be transformed into CodeGenerationRequest, which will
be useful in implementing the `protoc-gen-grpc-swift` v2.


---------

Co-authored-by: George Barnett <gbarnett@apple.com>
Stefana-Ioana Dranca 2 years ago
parent
commit
373d9f4598

+ 26 - 3
Package.swift

@@ -94,6 +94,7 @@ extension Target.Dependency {
   static let reflectionService: Self = .target(name: "GRPCReflectionService")
   static let grpcCodeGen: Self = .target(name: "GRPCCodeGen")
   static let grpcProtobuf: Self = .target(name: "GRPCProtobuf")
+  static let grpcProtobufCodeGen: Self = .target(name: "GRPCProtobufCodeGen")
 
   // Target dependencies; internal
   static let grpcSampleData: Self = .target(name: "GRPCSampleData")
@@ -235,6 +236,7 @@ extension Target {
     dependencies: [
       .protobuf,
       .protobufPluginLibrary,
+      .grpcCodeGen
     ],
     exclude: [
       "README.md",
@@ -341,12 +343,22 @@ extension Target {
   static let grpcProtobufTests: Target = .testTarget(
     name: "GRPCProtobufTests",
     dependencies: [
-      .grpcCore,
       .grpcProtobuf,
+      .grpcCore,
       .protobuf
     ]
   )
   
+  static let grpcProtobufCodeGenTests: Target = .testTarget(
+    name: "GRPCProtobufCodeGenTests",
+    dependencies: [
+      .grpcCodeGen,
+      .grpcProtobufCodeGen,
+      .protobuf,
+      .protobufPluginLibrary
+    ]
+  )
+  
   static let interopTestModels: Target = .target(
     name: "GRPCInteroperabilityTestModels",
     dependencies: [
@@ -601,10 +613,19 @@ extension Target {
     name: "GRPCProtobuf",
     dependencies: [
       .grpcCore,
-      .protobuf
+      .protobuf,
     ],
     path: "Sources/GRPCProtobuf"
   )
+  static let grpcProtobufCodeGen: Target = .target(
+    name: "GRPCProtobufCodeGen",
+    dependencies: [
+      .protobuf,
+      .protobufPluginLibrary,
+      .grpcCodeGen
+    ],
+    path: "Sources/GRPCProtobufCodeGen"
+  )
 }
 
 // MARK: - Products
@@ -693,6 +714,7 @@ let package = Package(
     .grpcHTTP2TransportNIOPosix,
     .grpcHTTP2TransportNIOTransportServices,
     .grpcProtobuf,
+    .grpcProtobufCodeGen,
 
     // v2 tests
     .grpcCoreTests,
@@ -702,7 +724,8 @@ let package = Package(
     .grpcHTTP2CoreTests,
     .grpcHTTP2TransportNIOPosixTests,
     .grpcHTTP2TransportNIOTransportServicesTests,
-    .grpcProtobufTests
+    .grpcProtobufTests,
+    .grpcProtobufCodeGenTests
   ]
 )
 

+ 5 - 5
Sources/GRPCCodeGen/CodeGenerationRequest.swift

@@ -81,7 +81,7 @@ public struct CodeGenerationRequest {
   }
 
   /// Represents an import: a module or a specific item from a module.
-  public struct Dependency {
+  public struct Dependency: Equatable {
     /// If the dependency is an item, the property's value is the item representation.
     /// If the dependency is a module, this property is nil.
     public var item: Item? = nil
@@ -111,7 +111,7 @@ public struct CodeGenerationRequest {
     }
 
     /// Represents an item imported from a module.
-    public struct Item {
+    public struct Item: Equatable {
       /// The keyword that specifies the item's kind (e.g. `func`, `struct`).
       public var kind: Kind
 
@@ -124,7 +124,7 @@ public struct CodeGenerationRequest {
       }
 
       /// Represents the imported item's kind.
-      public struct Kind {
+      public struct Kind: Equatable {
         /// Describes the keyword associated with the imported item.
         internal enum Value: String {
           case `typealias`
@@ -186,8 +186,8 @@ public struct CodeGenerationRequest {
     }
 
     /// Describes any requirement for the `@preconcurrency` attribute.
-    public struct PreconcurrencyRequirement {
-      internal enum Value {
+    public struct PreconcurrencyRequirement: Equatable {
+      internal enum Value: Equatable {
         case required
         case notRequired
         case requiredOnOS([String])

+ 123 - 0
Sources/GRPCProtobufCodeGen/ProtobufCodeGenParser.swift

@@ -0,0 +1,123 @@
+/*
+ * 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 Foundation
+import SwiftProtobuf
+import SwiftProtobufPluginLibrary
+
+import struct GRPCCodeGen.CodeGenerationRequest
+
+/// Parses a ``FileDescriptor`` object into a ``CodeGenerationRequest`` object.
+internal struct ProtobufCodeGenParser {
+  internal init() {}
+  internal func parse(input: FileDescriptor) throws -> CodeGenerationRequest {
+    var header = input.header
+    // Ensuring there is a blank line after the header.
+    if !header.isEmpty && !header.hasSuffix("\n\n") {
+      header.append("\n")
+    }
+    let leadingTrivia = """
+      // DO NOT EDIT.
+      // swift-format-ignore-file
+      //
+      // Generated by the gRPC Swift generator plugin for the protocol buffer compiler.
+      // Source: \(input.name)
+      //
+      // For information on using the generated types, please see the documentation:
+      //   https://github.com/grpc/grpc-swift
+
+      """
+    var dependencies = input.dependencies.map {
+      CodeGenerationRequest.Dependency(module: $0.name)
+    }
+    dependencies.append(CodeGenerationRequest.Dependency(module: "GRPCProtobuf"))
+    let lookupSerializer: (String) -> String = { messageType in
+      "ProtobufSerializer<\(messageType)>()"
+    }
+    let lookupDeserializer: (String) -> String = { messageType in
+      "ProtobufDeserializer<\(messageType)>()"
+    }
+    let services = input.services.map {
+      CodeGenerationRequest.ServiceDescriptor(descriptor: $0, package: input.package)
+    }
+
+    return CodeGenerationRequest(
+      fileName: input.name,
+      leadingTrivia: header + leadingTrivia,
+      dependencies: dependencies,
+      services: services,
+      lookupSerializer: lookupSerializer,
+      lookupDeserializer: lookupDeserializer
+    )
+  }
+}
+
+extension CodeGenerationRequest.ServiceDescriptor {
+  fileprivate init(descriptor: ServiceDescriptor, package: String) {
+    let methods = descriptor.methods.map {
+      CodeGenerationRequest.ServiceDescriptor.MethodDescriptor(descriptor: $0)
+    }
+    let name = CodeGenerationRequest.Name(
+      base: descriptor.name,
+      generatedUpperCase: NamingUtils.toUpperCamelCase(descriptor.name),
+      generatedLowerCase: NamingUtils.toLowerCamelCase(descriptor.name)
+    )
+    let namespace = CodeGenerationRequest.Name(
+      base: package,
+      generatedUpperCase: NamingUtils.toUpperCamelCase(package),
+      generatedLowerCase: NamingUtils.toLowerCamelCase(package)
+    )
+    let documentation = descriptor.protoSourceComments()
+    self.init(documentation: documentation, name: name, namespace: namespace, methods: methods)
+  }
+}
+
+extension CodeGenerationRequest.ServiceDescriptor.MethodDescriptor {
+  fileprivate init(descriptor: MethodDescriptor) {
+    let name = CodeGenerationRequest.Name(
+      base: descriptor.name,
+      generatedUpperCase: NamingUtils.toUpperCamelCase(descriptor.name),
+      generatedLowerCase: NamingUtils.toLowerCamelCase(descriptor.name)
+    )
+    let documentation = descriptor.protoSourceComments()
+    self.init(
+      documentation: documentation,
+      name: name,
+      isInputStreaming: descriptor.clientStreaming,
+      isOutputStreaming: descriptor.serverStreaming,
+      inputType: descriptor.inputType.name,
+      outputType: descriptor.outputType.name
+    )
+  }
+}
+
+extension FileDescriptor {
+  fileprivate var header: String {
+    var header = String()
+    // Field number used to collect the syntax field which is usually the first
+    // declaration in a.proto file.
+    // See more here:
+    // https://github.com/apple/swift-protobuf/blob/main/Protos/SwiftProtobuf/google/protobuf/descriptor.proto
+    let syntaxPath = IndexPath(index: 12)
+    if let syntaxLocation = self.sourceCodeInfoLocation(path: syntaxPath) {
+      header = syntaxLocation.asSourceComment(
+        commentPrefix: "///",
+        leadingDetachedPrefix: "//"
+      )
+    }
+    return header
+  }
+}

+ 193 - 0
Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift

@@ -0,0 +1,193 @@
+/*
+ * 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 SwiftProtobuf
+import SwiftProtobufPluginLibrary
+import XCTest
+
+@testable import GRPCProtobufCodeGen
+
+final class ProtobufCodeGenParserTests: XCTestCase {
+  func testParser() throws {
+    let parsedCodeGenRequest = try ProtobufCodeGenParser().parse(
+      input: self.helloWorldFileDescriptor
+    )
+    XCTAssertEqual(parsedCodeGenRequest.fileName, "helloworld.proto")
+    XCTAssertEqual(
+      parsedCodeGenRequest.leadingTrivia,
+      """
+      // Copyright 2015 gRPC authors.
+      //
+      // 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.
+
+      // DO NOT EDIT.
+      // swift-format-ignore-file
+      //
+      // Generated by the gRPC Swift generator plugin for the protocol buffer compiler.
+      // Source: helloworld.proto
+      //
+      // For information on using the generated types, please see the documentation:
+      //   https://github.com/grpc/grpc-swift
+
+      """
+    )
+
+    XCTAssertEqual(parsedCodeGenRequest.services.count, 1)
+
+    let expectedMethod = CodeGenerationRequest.ServiceDescriptor.MethodDescriptor(
+      documentation: "/// Sends a greeting.\n",
+      name: CodeGenerationRequest.Name(
+        base: "SayHello",
+        generatedUpperCase: "SayHello",
+        generatedLowerCase: "sayHello"
+      ),
+      isInputStreaming: false,
+      isOutputStreaming: false,
+      inputType: "HelloRequest",
+      outputType: "HelloReply"
+    )
+    guard let method = parsedCodeGenRequest.services.first?.methods.first else { return XCTFail() }
+    XCTAssertEqual(method, expectedMethod)
+
+    let expectedService = CodeGenerationRequest.ServiceDescriptor(
+      documentation: "/// The greeting service definition.\n",
+      name: CodeGenerationRequest.Name(
+        base: "Greeter",
+        generatedUpperCase: "Greeter",
+        generatedLowerCase: "greeter"
+      ),
+      namespace: CodeGenerationRequest.Name(
+        base: "helloworld",
+        generatedUpperCase: "Helloworld",
+        generatedLowerCase: "helloworld"
+      ),
+      methods: [expectedMethod]
+    )
+    guard let service = parsedCodeGenRequest.services.first else { return XCTFail() }
+    XCTAssertEqual(service, expectedService)
+    XCTAssertEqual(service.methods.count, 1)
+
+    XCTAssertEqual(
+      parsedCodeGenRequest.lookupSerializer("HelloRequest"),
+      "ProtobufSerializer<HelloRequest>()"
+    )
+    XCTAssertEqual(
+      parsedCodeGenRequest.lookupDeserializer("HelloRequest"),
+      "ProtobufDeserializer<HelloRequest>()"
+    )
+    XCTAssertEqual(parsedCodeGenRequest.dependencies.count, 1)
+    XCTAssertEqual(
+      parsedCodeGenRequest.dependencies[0],
+      CodeGenerationRequest.Dependency(module: "GRPCProtobuf")
+    )
+  }
+
+  var helloWorldFileDescriptor: FileDescriptor {
+    let requestType = Google_Protobuf_DescriptorProto.with {
+      $0.name = "HelloRequest"
+      $0.field = [
+        Google_Protobuf_FieldDescriptorProto.with {
+          $0.name = "name"
+          $0.number = 1
+          $0.label = .optional
+          $0.type = .string
+          $0.jsonName = "name"
+        }
+      ]
+    }
+    let responseType = Google_Protobuf_DescriptorProto.with {
+      $0.name = "HelloReply"
+      $0.field = [
+        Google_Protobuf_FieldDescriptorProto.with {
+          $0.name = "message"
+          $0.number = 1
+          $0.label = .optional
+          $0.type = .string
+          $0.jsonName = "message"
+        }
+      ]
+    }
+
+    let service = Google_Protobuf_ServiceDescriptorProto.with {
+      $0.name = "Greeter"
+      $0.method = [
+        Google_Protobuf_MethodDescriptorProto.with {
+          $0.name = "SayHello"
+          $0.inputType = ".helloworld.HelloRequest"
+          $0.outputType = ".helloworld.HelloReply"
+          $0.clientStreaming = false
+          $0.serverStreaming = false
+        }
+      ]
+    }
+    let protoDescriptor = Google_Protobuf_FileDescriptorProto.with {
+      $0.name = "helloworld.proto"
+      $0.package = "helloworld"
+      $0.messageType = [requestType, responseType]
+      $0.service = [service]
+      $0.sourceCodeInfo = Google_Protobuf_SourceCodeInfo.with {
+        $0.location = [
+          Google_Protobuf_SourceCodeInfo.Location.with {
+            $0.path = [12]
+            $0.span = [14, 0, 18]
+            $0.leadingDetachedComments = [
+              """
+               Copyright 2015 gRPC authors.
+
+               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.
+
+              """
+            ]
+          },
+          Google_Protobuf_SourceCodeInfo.Location.with {
+            $0.path = [6, 0]
+            $0.span = [19, 0, 22, 1]
+            $0.leadingComments = " The greeting service definition.\n"
+          },
+          Google_Protobuf_SourceCodeInfo.Location.with {
+            $0.path = [6, 0, 2, 0]
+            $0.span = [21, 2, 53]
+            $0.leadingComments = " Sends a greeting.\n"
+          },
+        ]
+      }
+      $0.syntax = "proto3"
+    }
+    let descriptorSet = DescriptorSet(protos: [protoDescriptor])
+    return descriptorSet.fileDescriptor(named: "helloworld.proto")!
+  }
+}