From 1024d944da1190c2b4692a492bc9cd0eb468a336 Mon Sep 17 00:00:00 2001 From: Stefana Dranca Date: Mon, 29 Jan 2024 16:02:44 +0000 Subject: [PATCH 1/4] [CodeGen Protobuf support] protoc-gen-grpc-swift v2 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. --- Package.swift | 4 +- Sources/GRPCCodeGen/SourceGenerator.swift | 7 + .../ProtobufCodeGenerator.swift | 38 ++ Sources/protoc-gen-grpc-swift/main.swift | 33 +- Sources/protoc-gen-grpc-swift/options.swift | 8 + .../ProtobufCodeGenParserTests.swift | 156 +++---- .../ProtobufCodeGeneratorTests.swift | 421 ++++++++++++++++++ 7 files changed, 586 insertions(+), 81 deletions(-) create mode 100644 Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift create mode 100644 Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift diff --git a/Package.swift b/Package.swift index d0db22a72..b5522f404 100644 --- a/Package.swift +++ b/Package.swift @@ -236,7 +236,9 @@ extension Target { dependencies: [ .protobuf, .protobufPluginLibrary, - .grpcCodeGen + .grpcCodeGen, + .grpcProtobuf, + .grpcProtobufCodeGen ], exclude: [ "README.md", diff --git a/Sources/GRPCCodeGen/SourceGenerator.swift b/Sources/GRPCCodeGen/SourceGenerator.swift index 98c934e5b..d410b1e32 100644 --- a/Sources/GRPCCodeGen/SourceGenerator.swift +++ b/Sources/GRPCCodeGen/SourceGenerator.swift @@ -35,6 +35,13 @@ public struct SourceGenerator: Sendable { /// Whether or not server code should be generated. public var server: Bool + public init(accessLevel: AccessLevel, indentation: Int = 4, client: Bool, server: Bool) { + self.accessLevel = accessLevel + self.indentation = indentation + self.client = client + self.server = server + } + /// The possible access levels for the generated code. public struct AccessLevel: Sendable, Hashable { internal var level: Level diff --git a/Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift b/Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift new file mode 100644 index 000000000..9b7377b5e --- /dev/null +++ b/Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift @@ -0,0 +1,38 @@ +/* + * 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 GRPCProtobuf +import SwiftProtobufPluginLibrary + +public struct ProtobufCodeGenerator { + internal var configs: SourceGenerator.Configuration + internal var file: FileDescriptor + + public init(configs: SourceGenerator.Configuration, file: FileDescriptor) { + self.configs = configs + self.file = file + } + + public func generateCode() throws -> String { + let parser = ProtobufCodeGenParser() + let sourceGenerator = SourceGenerator(configuration: self.configs) + + let codeGenerationRequest = try parser.parse(input: file) + let sourceFile = try sourceGenerator.generate(codeGenerationRequest) + return sourceFile.contents + } +} diff --git a/Sources/protoc-gen-grpc-swift/main.swift b/Sources/protoc-gen-grpc-swift/main.swift index 05d7c3ecb..9263d1cb9 100644 --- a/Sources/protoc-gen-grpc-swift/main.swift +++ b/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 @@ -114,6 +116,25 @@ func printVersion(args: [String]) { print("\(program) \(Version.versionString)") } +func convertOptionsToSourceGeneratortConfiguration( + _ options: GeneratorOptions +) -> SourceGenerator.Configuration { + let accessLevel: SourceGenerator.Configuration.AccessLevel + switch options.visibility { + case .internal: + accessLevel = .internal + case .package: + accessLevel = .package + case .public: + accessLevel = .public + } + return SourceGenerator.Configuration( + accessLevel: accessLevel, + client: options.generateClient, + server: options.generateServer + ) +} + func main(args: [String]) throws { if args.dropFirst().contains("--version") { printVersion(args: args) @@ -166,9 +187,17 @@ func main(args: [String]) throws { fileNamingOption: options.fileNaming, generatedFiles: &generatedFiles ) - let grpcGenerator = Generator(fileDescriptor, options: options) + if options.v2 { + let grpcGenerator = ProtobufCodeGenerator( + configs: convertOptionsToSourceGeneratortConfiguration(options), + file: fileDescriptor + ) + grpcFile.content = try grpcGenerator.generateCode() + } else { + let grpcGenerator = Generator(fileDescriptor, options: options) + grpcFile.content = grpcGenerator.code + } grpcFile.name = grpcFileName - grpcFile.content = grpcGenerator.code response.file.append(grpcFile) } } diff --git a/Sources/protoc-gen-grpc-swift/options.swift b/Sources/protoc-gen-grpc-swift/options.swift index 155dd7377..e28be14c2 100644 --- a/Sources/protoc-gen-grpc-swift/options.swift +++ b/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) } diff --git a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift index 664501d82..7f7fd53f1 100644 --- a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift +++ b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift @@ -24,7 +24,7 @@ import XCTest final class ProtobufCodeGenParserTests: XCTestCase { func testParser() throws { let parsedCodeGenRequest = try ProtobufCodeGenParser().parse( - input: self.helloWorldFileDescriptor + input: helloWorldFileDescriptor ) XCTAssertEqual(parsedCodeGenRequest.fileName, "helloworld.proto") XCTAssertEqual( @@ -105,89 +105,89 @@ final class ProtobufCodeGenParserTests: XCTestCase { 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" - } - ] - } +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. + 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 + 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 + 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. + 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" + """ + ] + }, + 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" + }, + ] } - let descriptorSet = DescriptorSet(protos: [protoDescriptor]) - return descriptorSet.fileDescriptor(named: "helloworld.proto")! + $0.syntax = "proto3" } + let descriptorSet = DescriptorSet(protos: [protoDescriptor]) + return descriptorSet.fileDescriptor(named: "helloworld.proto")! } diff --git a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift new file mode 100644 index 000000000..699b3e693 --- /dev/null +++ b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift @@ -0,0 +1,421 @@ +/* + * 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 GRPCProtobufCodeGen +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( + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable + } + + extension Helloworld.Greeter.ClientProtocol { + internal func sayHello( + request: ClientRequest.Single, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable { + try await self.sayHello( + request: request, + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + 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( + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) 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) async throws -> ServerResponse.Stream + } + + /// 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(), + serializer: ProtobufSerializer(), + 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) async throws -> ServerResponse.Single + } + + /// Partial conformance to `Helloworld_GreeterStreamingServiceProtocol`. + extension Helloworld.Greeter.ServiceProtocol { + public func sayHello(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + 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) async throws -> ServerResponse.Stream + } + + /// 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(), + serializer: ProtobufSerializer(), + 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) async throws -> ServerResponse.Single + } + + /// Partial conformance to `Helloworld_GreeterStreamingServiceProtocol`. + extension Helloworld.Greeter.ServiceProtocol { + package func sayHello(request: ServerRequest.Stream) async throws -> ServerResponse.Stream { + 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( + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable + } + + extension Helloworld.Greeter.ClientProtocol { + package func sayHello( + request: ClientRequest.Single, + _ body: @Sendable @escaping (ClientResponse.Single) async throws -> R + ) async throws -> R where R: Sendable { + try await self.sayHello( + request: request, + serializer: ProtobufSerializer(), + deserializer: ProtobufDeserializer(), + 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( + request: ClientRequest.Single, + serializer: some MessageSerializer, + deserializer: some MessageDeserializer, + _ body: @Sendable @escaping (ClientResponse.Single) 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, + indentation: indentation, + client: client, + server: server + ) + let generator = ProtobufCodeGenerator(configs: configs, file: helloWorldFileDescriptor) + try XCTAssertEqualWithDiff(try generator.generateCode(), 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 + ) +} From dfccb72752833c2a406287864f66bd652bdad3be Mon Sep 17 00:00:00 2001 From: Stefana Dranca Date: Mon, 29 Jan 2024 16:27:43 +0000 Subject: [PATCH 2/4] fixes --- Package.swift | 1 - Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift | 1 - .../GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift | 4 ++++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index b5522f404..d8975bc11 100644 --- a/Package.swift +++ b/Package.swift @@ -237,7 +237,6 @@ extension Target { .protobuf, .protobufPluginLibrary, .grpcCodeGen, - .grpcProtobuf, .grpcProtobufCodeGen ], exclude: [ diff --git a/Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift b/Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift index 9b7377b5e..25f259bd7 100644 --- a/Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift +++ b/Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift @@ -15,7 +15,6 @@ */ import GRPCCodeGen -import GRPCProtobuf import SwiftProtobufPluginLibrary public struct ProtobufCodeGenerator { diff --git a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift index 699b3e693..228afc91d 100644 --- a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift +++ b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift @@ -14,6 +14,8 @@ * limitations under the License. */ +#if os(macOS) || os(Linux) // swift-format doesn't like canImport(Foundation.Process) + import GRPCCodeGen import GRPCProtobufCodeGen import XCTest @@ -419,3 +421,5 @@ internal func XCTAssertEqualWithDiff( line: line ) } + +#endif // os(macOS) || os(Linux) From 775637b312fbd14d31faef2af5d43881a041c57f Mon Sep 17 00:00:00 2001 From: Stefana Dranca Date: Tue, 30 Jan 2024 16:42:02 +0000 Subject: [PATCH 3/4] implemented feedback, still have to add renderer changes --- Sources/GRPCCodeGen/SourceGenerator.swift | 2 +- .../ProtobufCodeGenerator.swift | 14 +- Sources/protoc-gen-grpc-swift/main.swift | 43 +++-- Sources/protoc-gen-grpc-swift/options.swift | 2 +- .../ProtobufCodeGenParserTests.swift | 162 +++++++++--------- .../ProtobufCodeGeneratorTests.swift | 18 +- 6 files changed, 128 insertions(+), 113 deletions(-) diff --git a/Sources/GRPCCodeGen/SourceGenerator.swift b/Sources/GRPCCodeGen/SourceGenerator.swift index d410b1e32..55b1fc54e 100644 --- a/Sources/GRPCCodeGen/SourceGenerator.swift +++ b/Sources/GRPCCodeGen/SourceGenerator.swift @@ -35,7 +35,7 @@ public struct SourceGenerator: Sendable { /// Whether or not server code should be generated. public var server: Bool - public init(accessLevel: AccessLevel, indentation: Int = 4, client: Bool, server: Bool) { + public init(accessLevel: AccessLevel, client: Bool, server: Bool, indentation: Int = 4) { self.accessLevel = accessLevel self.indentation = indentation self.client = client diff --git a/Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift b/Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift index 25f259bd7..caa198d7a 100644 --- a/Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift +++ b/Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift @@ -18,19 +18,17 @@ import GRPCCodeGen import SwiftProtobufPluginLibrary public struct ProtobufCodeGenerator { - internal var configs: SourceGenerator.Configuration - internal var file: FileDescriptor + internal var configuration: SourceGenerator.Configuration - public init(configs: SourceGenerator.Configuration, file: FileDescriptor) { - self.configs = configs - self.file = file + public init(configuration: SourceGenerator.Configuration) { + self.configuration = configuration } - public func generateCode() throws -> String { + public func generateCode(from fileDescriptor: FileDescriptor) throws -> String { let parser = ProtobufCodeGenParser() - let sourceGenerator = SourceGenerator(configuration: self.configs) + let sourceGenerator = SourceGenerator(configuration: self.configuration) - let codeGenerationRequest = try parser.parse(input: file) + let codeGenerationRequest = try parser.parse(input: fileDescriptor) let sourceFile = try sourceGenerator.generate(codeGenerationRequest) return sourceFile.contents } diff --git a/Sources/protoc-gen-grpc-swift/main.swift b/Sources/protoc-gen-grpc-swift/main.swift index 9263d1cb9..12002c117 100644 --- a/Sources/protoc-gen-grpc-swift/main.swift +++ b/Sources/protoc-gen-grpc-swift/main.swift @@ -116,25 +116,6 @@ func printVersion(args: [String]) { print("\(program) \(Version.versionString)") } -func convertOptionsToSourceGeneratortConfiguration( - _ options: GeneratorOptions -) -> SourceGenerator.Configuration { - let accessLevel: SourceGenerator.Configuration.AccessLevel - switch options.visibility { - case .internal: - accessLevel = .internal - case .package: - accessLevel = .package - case .public: - accessLevel = .public - } - return SourceGenerator.Configuration( - accessLevel: accessLevel, - client: options.generateClient, - server: options.generateServer - ) -} - func main(args: [String]) throws { if args.dropFirst().contains("--version") { printVersion(args: args) @@ -189,10 +170,9 @@ func main(args: [String]) throws { ) if options.v2 { let grpcGenerator = ProtobufCodeGenerator( - configs: convertOptionsToSourceGeneratortConfiguration(options), - file: fileDescriptor + configuration: SourceGenerator.Configuration(options: options) ) - grpcFile.content = try grpcGenerator.generateCode() + grpcFile.content = try grpcGenerator.generateCode(from: fileDescriptor) } else { let grpcGenerator = Generator(fileDescriptor, options: options) grpcFile.content = grpcGenerator.code @@ -213,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 + ) + } +} diff --git a/Sources/protoc-gen-grpc-swift/options.swift b/Sources/protoc-gen-grpc-swift/options.swift index e28be14c2..e2a9b27dd 100644 --- a/Sources/protoc-gen-grpc-swift/options.swift +++ b/Sources/protoc-gen-grpc-swift/options.swift @@ -155,7 +155,7 @@ final class GeneratorOptions { throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value) } - case "V2": + case "_V2": if let value = Bool(pair.value) { self.v2 = value } else { diff --git a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift index 7f7fd53f1..25ba47dd9 100644 --- a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGenParserTests.swift +++ b/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: helloWorldFileDescriptor + input: fileDescriptor ) XCTAssertEqual(parsedCodeGenRequest.fileName, "helloworld.proto") XCTAssertEqual( @@ -107,87 +115,87 @@ final class ProtobufCodeGenParserTests: XCTestCase { } } -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" - } - ] - } +extension Google_Protobuf_FileDescriptorProto { + static var helloWorld: Google_Protobuf_FileDescriptorProto { + 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. + 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 + } + ] + } + return 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 + 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 + 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. + 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" - }, - ] + """ + ] + }, + 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" } - $0.syntax = "proto3" } - let descriptorSet = DescriptorSet(protos: [protoDescriptor]) - return descriptorSet.fileDescriptor(named: "helloworld.proto")! } diff --git a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift index 228afc91d..0e5026fd5 100644 --- a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift +++ b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift @@ -18,6 +18,8 @@ import GRPCCodeGen import GRPCProtobufCodeGen +import SwiftProtobuf +import SwiftProtobufPluginLibrary import XCTest final class ProtobufCodeGeneratorTests: XCTestCase { @@ -375,12 +377,20 @@ final class ProtobufCodeGeneratorTests: XCTestCase { ) throws { let configs = SourceGenerator.Configuration( accessLevel: visibility, - indentation: indentation, client: client, - server: server + server: server, + indentation: indentation ) - let generator = ProtobufCodeGenerator(configs: configs, file: helloWorldFileDescriptor) - try XCTAssertEqualWithDiff(try generator.generateCode(), expectedCode) + 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) } } From 55344b2f5a3dfa76c405ac85bb356f2bb988d132 Mon Sep 17 00:00:00 2001 From: Stefana Dranca Date: Thu, 1 Feb 2024 10:17:52 +0000 Subject: [PATCH 4/4] changed tests to reflect the renderer changes --- .../ProtobufCodeGeneratorTests.swift | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift index 0e5026fd5..88c113106 100644 --- a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift +++ b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift @@ -55,6 +55,7 @@ final class ProtobufCodeGeneratorTests: XCTestCase { import GRPCCore import GRPCProtobuf + internal enum Helloworld { internal enum Greeter { internal enum Methods { @@ -76,10 +77,8 @@ final class ProtobufCodeGeneratorTests: XCTestCase { } /// The greeting service definition. - internal protocol Helloworld_GreeterClientProtocol: Sendable { /// Sends a greeting. - func sayHello( request: ClientRequest.Single, serializer: some MessageSerializer, @@ -103,14 +102,12 @@ final class ProtobufCodeGeneratorTests: XCTestCase { } /// 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( request: ClientRequest.Single, serializer: some MessageSerializer, @@ -161,6 +158,7 @@ final class ProtobufCodeGeneratorTests: XCTestCase { import GRPCCore import GRPCProtobuf + public enum Helloworld { public enum Greeter { public enum Methods { @@ -182,10 +180,8 @@ final class ProtobufCodeGeneratorTests: XCTestCase { } /// The greeting service definition. - public protocol Helloworld_GreeterStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Sends a greeting. - func sayHello(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } @@ -204,10 +200,8 @@ final class ProtobufCodeGeneratorTests: XCTestCase { } /// The greeting service definition. - public protocol Helloworld_GreeterServiceProtocol: Helloworld.Greeter.StreamingServiceProtocol { /// Sends a greeting. - func sayHello(request: ServerRequest.Single) async throws -> ServerResponse.Single } @@ -252,6 +246,7 @@ final class ProtobufCodeGeneratorTests: XCTestCase { import GRPCCore import GRPCProtobuf + package enum Helloworld { package enum Greeter { package enum Methods { @@ -275,10 +270,8 @@ final class ProtobufCodeGeneratorTests: XCTestCase { } /// The greeting service definition. - package protocol Helloworld_GreeterStreamingServiceProtocol: GRPCCore.RegistrableRPCService { /// Sends a greeting. - func sayHello(request: ServerRequest.Stream) async throws -> ServerResponse.Stream } @@ -297,10 +290,8 @@ final class ProtobufCodeGeneratorTests: XCTestCase { } /// The greeting service definition. - package protocol Helloworld_GreeterServiceProtocol: Helloworld.Greeter.StreamingServiceProtocol { /// Sends a greeting. - func sayHello(request: ServerRequest.Single) async throws -> ServerResponse.Single } @@ -313,10 +304,8 @@ final class ProtobufCodeGeneratorTests: XCTestCase { } /// The greeting service definition. - package protocol Helloworld_GreeterClientProtocol: Sendable { /// Sends a greeting. - func sayHello( request: ClientRequest.Single, serializer: some MessageSerializer, @@ -340,14 +329,12 @@ final class ProtobufCodeGeneratorTests: XCTestCase { } /// 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( request: ClientRequest.Single, serializer: some MessageSerializer,