Browse Source

[CodeGen Protobuf support] protoc-gen-grpc-swift v2 (#1778)

Motivation:

We want to have a protoc plugin for grpc-swift v2 which builds on top of the code generator pipeline.

Modifications:

- Created the ProtobufCodeGenerator struct that encapsulates all setps needed to generate code for a given file descriptor
- Created tests for the ProtobufCodeGenerator
- added a new option for selecting v2 for the plugin
- modified main() accordingly

Result:

The protoc plugin for grpc-swift will support v2 and will use the CodeGen library.
Stefana-Ioana Dranca 2 years ago
parent
commit
a706a0b8cc

+ 35 - 0
Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift

@@ -0,0 +1,35 @@
+/*
+ * 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 SwiftProtobufPluginLibrary
+
+public struct ProtobufCodeGenerator {
+  internal var configuration: SourceGenerator.Configuration
+
+  public init(configuration: SourceGenerator.Configuration) {
+    self.configuration = configuration
+  }
+
+  public func generateCode(from fileDescriptor: FileDescriptor) throws -> String {
+    let parser = ProtobufCodeGenParser()
+    let sourceGenerator = SourceGenerator(configuration: self.configuration)
+
+    let codeGenerationRequest = try parser.parse(input: fileDescriptor)
+    let sourceFile = try sourceGenerator.generate(codeGenerationRequest)
+    return sourceFile.contents
+  }
+}

+ 30 - 2
Sources/protoc-gen-grpc-swift/main.swift

@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 import Foundation
+import GRPCCodeGen
+import GRPCProtobufCodeGen
 import SwiftProtobuf
 import SwiftProtobufPluginLibrary
 
@@ -166,9 +168,16 @@ func main(args: [String]) throws {
           fileNamingOption: options.fileNaming,
           generatedFiles: &generatedFiles
         )
-        let grpcGenerator = Generator(fileDescriptor, options: options)
+        if options.v2 {
+          let grpcGenerator = ProtobufCodeGenerator(
+            configuration: SourceGenerator.Configuration(options: options)
+          )
+          grpcFile.content = try grpcGenerator.generateCode(from: fileDescriptor)
+        } else {
+          let grpcGenerator = Generator(fileDescriptor, options: options)
+          grpcFile.content = grpcGenerator.code
+        }
         grpcFile.name = grpcFileName
-        grpcFile.content = grpcGenerator.code
         response.file.append(grpcFile)
       }
     }
@@ -184,3 +193,22 @@ do {
 } catch {
   Log("ERROR: \(error)")
 }
+
+extension SourceGenerator.Configuration {
+  init(options: GeneratorOptions) {
+    let accessLevel: SourceGenerator.Configuration.AccessLevel
+    switch options.visibility {
+    case .internal:
+      accessLevel = .internal
+    case .package:
+      accessLevel = .package
+    case .public:
+      accessLevel = .public
+    }
+    self.init(
+      accessLevel: accessLevel,
+      client: options.generateClient,
+      server: options.generateServer
+    )
+  }
+}

+ 8 - 0
Sources/protoc-gen-grpc-swift/options.swift

@@ -68,6 +68,7 @@ final class GeneratorOptions {
   private(set) var gRPCModuleName = "GRPC"
   private(set) var swiftProtobufModuleName = "SwiftProtobuf"
   private(set) var generateReflectionData = false
+  private(set) var v2 = false
 
   init(parameter: String?) throws {
     for pair in GeneratorOptions.parseParameter(string: parameter) {
@@ -154,6 +155,13 @@ final class GeneratorOptions {
           throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value)
         }
 
+      case "_V2":
+        if let value = Bool(pair.value) {
+          self.v2 = value
+        } else {
+          throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value)
+        }
+
       default:
         throw GenerationError.unknownParameter(name: pair.key)
       }

+ 13 - 5
Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift

@@ -23,8 +23,16 @@ import XCTest
 
 final class ProtobufCodeGenParserTests: XCTestCase {
   func testParser() throws {
+    let descriptorSet = DescriptorSet(protos: [Google_Protobuf_FileDescriptorProto.helloWorld])
+    guard let fileDescriptor = descriptorSet.fileDescriptor(named: "helloworld.proto") else {
+      return XCTFail(
+        """
+        Could not find the file descriptor of "helloworld.proto".
+        """
+      )
+    }
     let parsedCodeGenRequest = try ProtobufCodeGenParser().parse(
-      input: self.helloWorldFileDescriptor
+      input: fileDescriptor
     )
     XCTAssertEqual(parsedCodeGenRequest.fileName, "helloworld.proto")
     XCTAssertEqual(
@@ -105,8 +113,10 @@ final class ProtobufCodeGenParserTests: XCTestCase {
       CodeGenerationRequest.Dependency(module: "GRPCProtobuf")
     )
   }
+}
 
-  var helloWorldFileDescriptor: FileDescriptor {
+extension Google_Protobuf_FileDescriptorProto {
+  static var helloWorld: Google_Protobuf_FileDescriptorProto {
     let requestType = Google_Protobuf_DescriptorProto.with {
       $0.name = "HelloRequest"
       $0.field = [
@@ -144,7 +154,7 @@ final class ProtobufCodeGenParserTests: XCTestCase {
         }
       ]
     }
-    let protoDescriptor = Google_Protobuf_FileDescriptorProto.with {
+    return Google_Protobuf_FileDescriptorProto.with {
       $0.name = "helloworld.proto"
       $0.package = "helloworld"
       $0.messageType = [requestType, responseType]
@@ -187,7 +197,5 @@ final class ProtobufCodeGenParserTests: XCTestCase {
       }
       $0.syntax = "proto3"
     }
-    let descriptorSet = DescriptorSet(protos: [protoDescriptor])
-    return descriptorSet.fileDescriptor(named: "helloworld.proto")!
   }
 }

+ 422 - 0
Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift

@@ -0,0 +1,422 @@
+/*
+ * 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.
+ */
+
+#if os(macOS) || os(Linux)  // swift-format doesn't like canImport(Foundation.Process)
+
+import GRPCCodeGen
+import GRPCProtobufCodeGen
+import SwiftProtobuf
+import SwiftProtobufPluginLibrary
+import XCTest
+
+final class ProtobufCodeGeneratorTests: XCTestCase {
+  func testProtobufCodeGenerator() throws {
+    try testCodeGeneration(
+      indentation: 4,
+      visibility: .internal,
+      client: true,
+      server: false,
+      expectedCode: """
+        // 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
+
+        import GRPCCore
+        import GRPCProtobuf
+
+        internal enum Helloworld {
+            internal enum Greeter {
+                internal enum Methods {
+                    internal enum SayHello {
+                        internal typealias Input = HelloRequest
+                        internal typealias Output = HelloReply
+                        internal static let descriptor = MethodDescriptor(
+                            service: "helloworld.Greeter",
+                            method: "SayHello"
+                        )
+                    }
+                }
+                internal static let methods: [MethodDescriptor] = [
+                    Methods.SayHello.descriptor
+                ]
+                internal typealias ClientProtocol = Helloworld_GreeterClientProtocol
+                internal typealias Client = Helloworld_GreeterClient
+            }
+        }
+
+        /// The greeting service definition.
+        internal protocol Helloworld_GreeterClientProtocol: Sendable {
+            /// Sends a greeting.
+            func sayHello<R>(
+                request: ClientRequest.Single<Helloworld.Greeter.Methods.SayHello.Input>,
+                serializer: some MessageSerializer<Helloworld.Greeter.Methods.SayHello.Input>,
+                deserializer: some MessageDeserializer<Helloworld.Greeter.Methods.SayHello.Output>,
+                _ body: @Sendable @escaping (ClientResponse.Single<Helloworld.Greeter.Methods.SayHello.Output>) async throws -> R
+            ) async throws -> R where R: Sendable
+        }
+
+        extension Helloworld.Greeter.ClientProtocol {
+            internal func sayHello<R>(
+                request: ClientRequest.Single<Helloworld.Greeter.Methods.SayHello.Input>,
+                _ body: @Sendable @escaping (ClientResponse.Single<Helloworld.Greeter.Methods.SayHello.Output>) async throws -> R
+            ) async throws -> R where R: Sendable {
+                try await self.sayHello(
+                    request: request,
+                    serializer: ProtobufSerializer<Helloworld.Greeter.Methods.SayHello.Input>(),
+                    deserializer: ProtobufDeserializer<Helloworld.Greeter.Methods.SayHello.Output>(),
+                    body
+                )
+            }
+        }
+
+        /// The greeting service definition.
+        internal struct Helloworld_GreeterClient: Helloworld.Greeter.ClientProtocol {
+            private let client: GRPCCore.GRPCClient
+            internal init(client: GRPCCore.GRPCClient) {
+                self.client = client
+            }
+            /// Sends a greeting.
+            internal func sayHello<R>(
+                request: ClientRequest.Single<Helloworld.Greeter.Methods.SayHello.Input>,
+                serializer: some MessageSerializer<Helloworld.Greeter.Methods.SayHello.Input>,
+                deserializer: some MessageDeserializer<Helloworld.Greeter.Methods.SayHello.Output>,
+                _ body: @Sendable @escaping (ClientResponse.Single<Helloworld.Greeter.Methods.SayHello.Output>) async throws -> R
+            ) async throws -> R where R: Sendable {
+                try await self.client.unary(
+                    request: request,
+                    descriptor: Helloworld.Greeter.Methods.SayHello.descriptor,
+                    serializer: serializer,
+                    deserializer: deserializer,
+                    handler: body
+                )
+            }
+        }
+
+        """
+    )
+
+    try testCodeGeneration(
+      indentation: 2,
+      visibility: .public,
+      client: false,
+      server: true,
+      expectedCode: """
+        // 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
+
+        import GRPCCore
+        import GRPCProtobuf
+
+        public enum Helloworld {
+          public enum Greeter {
+            public enum Methods {
+              public enum SayHello {
+                public typealias Input = HelloRequest
+                public typealias Output = HelloReply
+                public static let descriptor = MethodDescriptor(
+                  service: "helloworld.Greeter",
+                  method: "SayHello"
+                )
+              }
+            }
+            public static let methods: [MethodDescriptor] = [
+              Methods.SayHello.descriptor
+            ]
+            public typealias StreamingServiceProtocol = Helloworld_GreeterServiceStreamingProtocol
+            public typealias ServiceProtocol = Helloworld_GreeterServiceProtocol
+          }
+        }
+
+        /// The greeting service definition.
+        public protocol Helloworld_GreeterStreamingServiceProtocol: GRPCCore.RegistrableRPCService {
+          /// Sends a greeting.
+          func sayHello(request: ServerRequest.Stream<Helloworld.Greeter.Methods.SayHello.Input>) async throws -> ServerResponse.Stream<Helloworld.Greeter.Methods.SayHello.Output>
+        }
+
+        /// Conformance to `GRPCCore.RegistrableRPCService`.
+        extension Helloworld.Greeter.StreamingServiceProtocol {
+          public func registerMethods(with router: inout GRPCCore.RPCRouter) {
+            router.registerHandler(
+              for: Helloworld.Greeter.Methods.SayHello.descriptor,
+              deserializer: ProtobufDeserializer<Helloworld.Greeter.Methods.SayHello.Input>(),
+              serializer: ProtobufSerializer<Helloworld.Greeter.Methods.SayHello.Output>(),
+              handler: { request in
+                try await self.sayHello(request: request)
+              }
+            )
+          }
+        }
+
+        /// The greeting service definition.
+        public protocol Helloworld_GreeterServiceProtocol: Helloworld.Greeter.StreamingServiceProtocol {
+          /// Sends a greeting.
+          func sayHello(request: ServerRequest.Single<Helloworld.Greeter.Methods.SayHello.Input>) async throws -> ServerResponse.Single<Helloworld.Greeter.Methods.SayHello.Output>
+        }
+
+        /// Partial conformance to `Helloworld_GreeterStreamingServiceProtocol`.
+        extension Helloworld.Greeter.ServiceProtocol {
+          public func sayHello(request: ServerRequest.Stream<Helloworld.Greeter.Methods.SayHello.Input>) async throws -> ServerResponse.Stream<Helloworld.Greeter.Methods.SayHello.Output> {
+            let response = try await self.sayHello(request: ServerRequest.Single(stream: request))
+            return ServerResponse.Stream(single: response)
+          }
+        }
+
+        """
+    )
+    try testCodeGeneration(
+      indentation: 2,
+      visibility: .package,
+      client: true,
+      server: true,
+      expectedCode: """
+        // 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
+
+        import GRPCCore
+        import GRPCProtobuf
+
+        package enum Helloworld {
+          package enum Greeter {
+            package enum Methods {
+              package enum SayHello {
+                package typealias Input = HelloRequest
+                package typealias Output = HelloReply
+                package static let descriptor = MethodDescriptor(
+                  service: "helloworld.Greeter",
+                  method: "SayHello"
+                )
+              }
+            }
+            package static let methods: [MethodDescriptor] = [
+              Methods.SayHello.descriptor
+            ]
+            package typealias StreamingServiceProtocol = Helloworld_GreeterServiceStreamingProtocol
+            package typealias ServiceProtocol = Helloworld_GreeterServiceProtocol
+            package typealias ClientProtocol = Helloworld_GreeterClientProtocol
+            package typealias Client = Helloworld_GreeterClient
+          }
+        }
+
+        /// The greeting service definition.
+        package protocol Helloworld_GreeterStreamingServiceProtocol: GRPCCore.RegistrableRPCService {
+          /// Sends a greeting.
+          func sayHello(request: ServerRequest.Stream<Helloworld.Greeter.Methods.SayHello.Input>) async throws -> ServerResponse.Stream<Helloworld.Greeter.Methods.SayHello.Output>
+        }
+
+        /// Conformance to `GRPCCore.RegistrableRPCService`.
+        extension Helloworld.Greeter.StreamingServiceProtocol {
+          package func registerMethods(with router: inout GRPCCore.RPCRouter) {
+            router.registerHandler(
+              for: Helloworld.Greeter.Methods.SayHello.descriptor,
+              deserializer: ProtobufDeserializer<Helloworld.Greeter.Methods.SayHello.Input>(),
+              serializer: ProtobufSerializer<Helloworld.Greeter.Methods.SayHello.Output>(),
+              handler: { request in
+                try await self.sayHello(request: request)
+              }
+            )
+          }
+        }
+
+        /// The greeting service definition.
+        package protocol Helloworld_GreeterServiceProtocol: Helloworld.Greeter.StreamingServiceProtocol {
+          /// Sends a greeting.
+          func sayHello(request: ServerRequest.Single<Helloworld.Greeter.Methods.SayHello.Input>) async throws -> ServerResponse.Single<Helloworld.Greeter.Methods.SayHello.Output>
+        }
+
+        /// Partial conformance to `Helloworld_GreeterStreamingServiceProtocol`.
+        extension Helloworld.Greeter.ServiceProtocol {
+          package func sayHello(request: ServerRequest.Stream<Helloworld.Greeter.Methods.SayHello.Input>) async throws -> ServerResponse.Stream<Helloworld.Greeter.Methods.SayHello.Output> {
+            let response = try await self.sayHello(request: ServerRequest.Single(stream: request))
+            return ServerResponse.Stream(single: response)
+          }
+        }
+
+        /// The greeting service definition.
+        package protocol Helloworld_GreeterClientProtocol: Sendable {
+          /// Sends a greeting.
+          func sayHello<R>(
+            request: ClientRequest.Single<Helloworld.Greeter.Methods.SayHello.Input>,
+            serializer: some MessageSerializer<Helloworld.Greeter.Methods.SayHello.Input>,
+            deserializer: some MessageDeserializer<Helloworld.Greeter.Methods.SayHello.Output>,
+            _ body: @Sendable @escaping (ClientResponse.Single<Helloworld.Greeter.Methods.SayHello.Output>) async throws -> R
+          ) async throws -> R where R: Sendable
+        }
+
+        extension Helloworld.Greeter.ClientProtocol {
+          package func sayHello<R>(
+            request: ClientRequest.Single<Helloworld.Greeter.Methods.SayHello.Input>,
+            _ body: @Sendable @escaping (ClientResponse.Single<Helloworld.Greeter.Methods.SayHello.Output>) async throws -> R
+          ) async throws -> R where R: Sendable {
+            try await self.sayHello(
+              request: request,
+              serializer: ProtobufSerializer<Helloworld.Greeter.Methods.SayHello.Input>(),
+              deserializer: ProtobufDeserializer<Helloworld.Greeter.Methods.SayHello.Output>(),
+              body
+            )
+          }
+        }
+
+        /// The greeting service definition.
+        package struct Helloworld_GreeterClient: Helloworld.Greeter.ClientProtocol {
+          private let client: GRPCCore.GRPCClient
+          package init(client: GRPCCore.GRPCClient) {
+            self.client = client
+          }
+          /// Sends a greeting.
+          package func sayHello<R>(
+            request: ClientRequest.Single<Helloworld.Greeter.Methods.SayHello.Input>,
+            serializer: some MessageSerializer<Helloworld.Greeter.Methods.SayHello.Input>,
+            deserializer: some MessageDeserializer<Helloworld.Greeter.Methods.SayHello.Output>,
+            _ body: @Sendable @escaping (ClientResponse.Single<Helloworld.Greeter.Methods.SayHello.Output>) async throws -> R
+          ) async throws -> R where R: Sendable {
+            try await self.client.unary(
+              request: request,
+              descriptor: Helloworld.Greeter.Methods.SayHello.descriptor,
+              serializer: serializer,
+              deserializer: deserializer,
+              handler: body
+            )
+          }
+        }
+
+        """
+    )
+  }
+
+  func testCodeGeneration(
+    indentation: Int,
+    visibility: SourceGenerator.Configuration.AccessLevel,
+    client: Bool,
+    server: Bool,
+    expectedCode: String
+  ) throws {
+    let configs = SourceGenerator.Configuration(
+      accessLevel: visibility,
+      client: client,
+      server: server,
+      indentation: indentation
+    )
+    let descriptorSet = DescriptorSet(protos: [Google_Protobuf_FileDescriptorProto.helloWorld])
+    guard let fileDescriptor = descriptorSet.fileDescriptor(named: "helloworld.proto") else {
+      return XCTFail(
+        """
+        Could not find the file descriptor of "helloworld.proto".
+        """
+      )
+    }
+    let generator = ProtobufCodeGenerator(configuration: configs)
+    try XCTAssertEqualWithDiff(try generator.generateCode(from: fileDescriptor), expectedCode)
+  }
+}
+
+private func diff(expected: String, actual: String) throws -> String {
+  let process = Process()
+  process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
+  process.arguments = [
+    "bash", "-c",
+    "diff -U5 --label=expected <(echo '\(expected)') --label=actual <(echo '\(actual)')",
+  ]
+  let pipe = Pipe()
+  process.standardOutput = pipe
+  try process.run()
+  process.waitUntilExit()
+  let pipeData = try XCTUnwrap(
+    pipe.fileHandleForReading.readToEnd(),
+    """
+    No output from command:
+    \(process.executableURL!.path) \(process.arguments!.joined(separator: " "))
+    """
+  )
+  return String(decoding: pipeData, as: UTF8.self)
+}
+
+internal func XCTAssertEqualWithDiff(
+  _ actual: String,
+  _ expected: String,
+  file: StaticString = #filePath,
+  line: UInt = #line
+) throws {
+  if actual == expected { return }
+  XCTFail(
+    """
+    XCTAssertEqualWithDiff failed (click for diff)
+    \(try diff(expected: expected, actual: actual))
+    """,
+    file: file,
+    line: line
+  )
+}
+
+#endif  // os(macOS) || os(Linux)