From 66dd0ce5a309780417721dcb17daac86a321206c Mon Sep 17 00:00:00 2001 From: George Barnett Date: Tue, 3 Dec 2024 08:31:17 +0000 Subject: [PATCH] Simplify and update code gen tests (#14) Motivation: The protobuf code generator doesn't actually do a lot: it parses a protobuf file descriptor and turns that into a request for the code generator provided by the GRPCCodeGen module. This means that the tests should focus on the conversion rather than the generated code (although this is still useful to test at a high level). As it stands all tests are done on the generated code rather than the conversion to a code generator request. The existing tests also hand roll a protobuf file descriptor which is tedious and error prone. The upstream code generator also has changes which aren't yet reflected here. Modifications: - Define some test proto files for which we generate serialized protobuf file descriptor sets. - Add a suite of code gen parser tests which check the request created from the file descriptor proto. - Add a single code generator test. - Downgrade an access level from public to package as public wasn't necessary. Result: Better tests --- .licenseignore | 2 + Package.swift | 3 + .../ProtobufCodeGenParser.swift | 56 +- .../ProtobufCodeGenerator.swift | 25 +- .../protoc-gen-grpc-swift/GenerateGRPC.swift | 4 +- .../Generated/bar-service.pb | Bin 0 -> 86 bytes .../Generated/foo-service.pb | Bin 0 -> 502 bytes .../Generated/test-service.pb | Bin 0 -> 822 bytes .../ProtobufCodeGenParserTests.swift | 577 ++----- .../ProtobufCodeGeneratorTests.swift | 1501 +++++++++++------ .../GRPCProtobufCodeGenTests/Utilities.swift | 82 + dev/protos/generate.sh | 67 + dev/protos/local/bar-service.proto | 3 + dev/protos/local/foo-messages.proto | 6 + dev/protos/local/foo-service.proto | 13 + dev/protos/local/test-service.proto | 19 + 16 files changed, 1395 insertions(+), 963 deletions(-) create mode 100644 Tests/GRPCProtobufCodeGenTests/Generated/bar-service.pb create mode 100644 Tests/GRPCProtobufCodeGenTests/Generated/foo-service.pb create mode 100644 Tests/GRPCProtobufCodeGenTests/Generated/test-service.pb create mode 100644 Tests/GRPCProtobufCodeGenTests/Utilities.swift create mode 100755 dev/protos/generate.sh create mode 100644 dev/protos/local/bar-service.proto create mode 100644 dev/protos/local/foo-messages.proto create mode 100644 dev/protos/local/foo-service.proto create mode 100644 dev/protos/local/test-service.proto diff --git a/.licenseignore b/.licenseignore index 6357010..ca688d8 100644 --- a/.licenseignore +++ b/.licenseignore @@ -34,6 +34,8 @@ Dockerfile Snippets/* dev/git.commit.template dev/version-bump.commit.template +dev/protos/local/* .unacceptablelanguageignore LICENSE **/*.swift +*.pb diff --git a/Package.swift b/Package.swift index a167b2c..903773f 100644 --- a/Package.swift +++ b/Package.swift @@ -94,6 +94,9 @@ let targets: [Target] = [ .product(name: "SwiftProtobuf", package: "swift-protobuf"), .product(name: "SwiftProtobufPluginLibrary", package: "swift-protobuf"), ], + resources: [ + .copy("Generated") + ], swiftSettings: defaultSwiftSettings ), ] diff --git a/Sources/GRPCProtobufCodeGen/ProtobufCodeGenParser.swift b/Sources/GRPCProtobufCodeGen/ProtobufCodeGenParser.swift index e7e0a10..d51c02d 100644 --- a/Sources/GRPCProtobufCodeGen/ProtobufCodeGenParser.swift +++ b/Sources/GRPCProtobufCodeGen/ProtobufCodeGenParser.swift @@ -15,45 +15,50 @@ */ internal import Foundation -internal import GRPCCodeGen -internal import SwiftProtobufPluginLibrary +internal import SwiftProtobuf +package import SwiftProtobufPluginLibrary + +package import struct GRPCCodeGen.CodeGenerationRequest +package import struct GRPCCodeGen.Dependency +package import struct GRPCCodeGen.MethodDescriptor +package import struct GRPCCodeGen.Name +package import struct GRPCCodeGen.ServiceDescriptor +package import struct GRPCCodeGen.SourceGenerator /// Parses a ``FileDescriptor`` object into a ``CodeGenerationRequest`` object. -internal struct ProtobufCodeGenParser { - let input: FileDescriptor - let namer: SwiftProtobufNamer +package struct ProtobufCodeGenParser { let extraModuleImports: [String] let protoToModuleMappings: ProtoFileToModuleMappings let accessLevel: SourceGenerator.Config.AccessLevel - internal init( - input: FileDescriptor, + package init( protoFileModuleMappings: ProtoFileToModuleMappings, extraModuleImports: [String], accessLevel: SourceGenerator.Config.AccessLevel ) { - self.input = input self.extraModuleImports = extraModuleImports self.protoToModuleMappings = protoFileModuleMappings - self.namer = SwiftProtobufNamer( - currentFile: input, - protoFileToModuleMappings: protoFileModuleMappings - ) self.accessLevel = accessLevel } - internal func parse() throws -> CodeGenerationRequest { - var header = self.input.header + package func parse(descriptor: FileDescriptor) throws -> CodeGenerationRequest { + let namer = SwiftProtobufNamer( + currentFile: descriptor, + protoFileToModuleMappings: self.protoToModuleMappings + ) + + var header = descriptor.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: \(self.input.name) + // Source: \(descriptor.name) // // For information on using the generated types, please see the documentation: // https://github.com/grpc/grpc-swift @@ -65,19 +70,20 @@ internal struct ProtobufCodeGenParser { let lookupDeserializer: (String) -> String = { messageType in "GRPCProtobuf.ProtobufDeserializer<\(messageType)>()" } - let services = self.input.services.map { - ServiceDescriptor( + + let services = descriptor.services.map { + GRPCCodeGen.ServiceDescriptor( descriptor: $0, - package: input.package, - protobufNamer: self.namer, - file: self.input + package: descriptor.package, + protobufNamer: namer, + file: descriptor ) } return CodeGenerationRequest( - fileName: self.input.name, + fileName: descriptor.name, leadingTrivia: header + leadingTrivia, - dependencies: self.codeDependencies, + dependencies: self.codeDependencies(file: descriptor), services: services, lookupSerializer: lookupSerializer, lookupDeserializer: lookupDeserializer @@ -86,14 +92,16 @@ internal struct ProtobufCodeGenParser { } extension ProtobufCodeGenParser { - fileprivate var codeDependencies: [Dependency] { + fileprivate func codeDependencies( + file: FileDescriptor + ) -> [Dependency] { var codeDependencies: [Dependency] = [ .init(module: "GRPCProtobuf", accessLevel: .internal) ] // Adding as dependencies the modules containing generated code or types for // '.proto' files imported in the '.proto' file we are parsing. codeDependencies.append( - contentsOf: (self.protoToModuleMappings.neededModules(forFile: self.input) ?? []).map { + contentsOf: (self.protoToModuleMappings.neededModules(forFile: file) ?? []).map { Dependency(module: $0, accessLevel: self.accessLevel) } ) diff --git a/Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift b/Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift index ad88831..57855c3 100644 --- a/Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift +++ b/Sources/GRPCProtobufCodeGen/ProtobufCodeGenerator.swift @@ -14,32 +14,31 @@ * limitations under the License. */ -public import GRPCCodeGen -public import SwiftProtobufPluginLibrary +package import GRPCCodeGen +package import SwiftProtobufPluginLibrary -public struct ProtobufCodeGenerator { - internal var configuration: SourceGenerator.Config +package struct ProtobufCodeGenerator { + internal var config: SourceGenerator.Config - public init( - configuration: SourceGenerator.Config + package init( + config: SourceGenerator.Config ) { - self.configuration = configuration + self.config = config } - public func generateCode( - from fileDescriptor: FileDescriptor, + package func generateCode( + fileDescriptor: FileDescriptor, protoFileModuleMappings: ProtoFileToModuleMappings, extraModuleImports: [String] ) throws -> String { let parser = ProtobufCodeGenParser( - input: fileDescriptor, protoFileModuleMappings: protoFileModuleMappings, extraModuleImports: extraModuleImports, - accessLevel: self.configuration.accessLevel + accessLevel: self.config.accessLevel ) - let sourceGenerator = SourceGenerator(config: self.configuration) + let sourceGenerator = SourceGenerator(config: self.config) - let codeGenerationRequest = try parser.parse() + let codeGenerationRequest = try parser.parse(descriptor: fileDescriptor) let sourceFile = try sourceGenerator.generate(codeGenerationRequest) return sourceFile.contents } diff --git a/Sources/protoc-gen-grpc-swift/GenerateGRPC.swift b/Sources/protoc-gen-grpc-swift/GenerateGRPC.swift index a4ebe9c..f878d7e 100644 --- a/Sources/protoc-gen-grpc-swift/GenerateGRPC.swift +++ b/Sources/protoc-gen-grpc-swift/GenerateGRPC.swift @@ -96,9 +96,9 @@ final class GenerateGRPC: CodeGenerator { ) let config = SourceGenerator.Config(options: options) - let fileGenerator = ProtobufCodeGenerator(configuration: config) + let fileGenerator = ProtobufCodeGenerator(config: config) let contents = try fileGenerator.generateCode( - from: descriptor, + fileDescriptor: descriptor, protoFileModuleMappings: options.protoToModuleMappings, extraModuleImports: options.extraModuleImports ) diff --git a/Tests/GRPCProtobufCodeGenTests/Generated/bar-service.pb b/Tests/GRPCProtobufCodeGenTests/Generated/bar-service.pb new file mode 100644 index 0000000000000000000000000000000000000000..dda0bf73d5d9546ea4663292dbf056ad226d87fb GIT binary patch literal 86 zcmd-I;Sx+rEYdAbEh@`QPSqo+qd3uRs!iw+upu!c75rPQQz%)n!1Mdkw`p8 z3YB{!;)BxOXvvX5N8(K+1pqFJIz1W2l(Ec*u?jk!J=)>HD|~CrLXfW6e!M<42RRmh z1YBRkG0*+}@TEV4q4h@<5f#-CdT~72eeP0zUt9n8TK%Q=jdOL-m^}@9YB?RtM=TF$ zD?YN4s!-F0_BJixz?tUpM4ux^91OP-n@x_tm$+hbhHkkUaLPvzz8rAj*D&H#z_}4e U!!0-B=!kP8j*d9DTuq<-1FvCG+yDRo literal 0 HcmV?d00001 diff --git a/Tests/GRPCProtobufCodeGenTests/Generated/test-service.pb b/Tests/GRPCProtobufCodeGenTests/Generated/test-service.pb new file mode 100644 index 0000000000000000000000000000000000000000..62fe3c71fbc514f82486fa1edd80ed40f1416a9e GIT binary patch literal 822 zcmb7@-AcnS6vy+?g?iR?E;=VwXB>)xTEA`-1gBm(L@%ZfP`4t5*+~}#pUXG%0mPH8 znRqd9SM4u3|KDj&D0~W#Iv(l$QJlRey*PZ$hI%L+v(_rZ+yg#u($_cL@&F9nziFi7 zFA8OY-EmCrLFqA#vJY7eO*EY3g8AQtakfR|DO9fqNu27g&f@4LNuU3f+o2|DTE^LI z$wMkHVd*OACt2Ln$uNxuvk7+SA?76NMr%W6DhHL^qgE2O4Q)C&bIAvM-4p&pvsmQWAJ=Y<}(yj(xN zh-rF;dT*jzt>w%3DoH(GHlvcRd=mNJaq()" - ) - XCTAssertEqual( - parsedCodeGenRequest.lookupDeserializer("Helloworld_HelloRequest"), - "GRPCProtobuf.ProtobufDeserializer()" - ) - } - func testParserNestedPackage() throws { - let descriptorSet = DescriptorSet( - protos: [ - Google_Protobuf_FileDescriptorProto( - name: "same-module.proto", - package: "same-package" - ), - Google_Protobuf_FileDescriptorProto( - name: "different-module.proto", - package: "different-package" - ), - Google_Protobuf_FileDescriptorProto.helloWorldNestedPackage, - ] - ) + @Test("Leading trivia") + func leadingTrivia() { + let expected = """ + /// Leading trivia. + + // DO NOT EDIT. + // swift-format-ignore-file + // + // Generated by the gRPC Swift generator plugin for the protocol buffer compiler. + // Source: test-service.proto + // + // For information on using the generated types, please see the documentation: + // https://github.com/grpc/grpc-swift - guard let fileDescriptor = descriptorSet.fileDescriptor(named: "helloworld.proto") else { - return XCTFail( - """ - Could not find the file descriptor of "helloworld.proto". """ - ) + + #expect(self.codeGen.leadingTrivia == expected) } - let moduleMappings = SwiftProtobuf_GenSwift_ModuleMappings.with { - $0.mapping = [ - SwiftProtobuf_GenSwift_ModuleMappings.Entry.with { - $0.protoFilePath = ["different-module.proto"] - $0.moduleName = "DifferentModule" - } + + @Test("Dependencies") + func dependencies() { + let expected: [GRPCCodeGen.Dependency] = [ + .init(module: "GRPCProtobuf", accessLevel: .internal) // Always an internal import ] + #expect(self.codeGen.dependencies == expected) } - let parsedCodeGenRequest = try ProtobufCodeGenParser( - input: fileDescriptor, - protoFileModuleMappings: ProtoFileToModuleMappings(moduleMappingsProto: moduleMappings), - extraModuleImports: ["ExtraModule"], - accessLevel: .internal - ).parse() - - self.testCommonHelloworldParsedRequestFields(for: parsedCodeGenRequest) - - let expectedMethod = MethodDescriptor( - documentation: "/// Sends a greeting.\n", - name: Name( - base: "SayHello", - generatedUpperCase: "SayHello", - generatedLowerCase: "sayHello" - ), - isInputStreaming: false, - isOutputStreaming: false, - inputType: "Hello_World_HelloRequest", - outputType: "Hello_World_HelloReply" - ) - guard let method = parsedCodeGenRequest.services.first?.methods.first else { return XCTFail() } - XCTAssertEqual(method, expectedMethod) - - let expectedService = ServiceDescriptor( - documentation: "/// The greeting service definition.\n", - name: Name( - base: "Greeter", - generatedUpperCase: "Greeter", - generatedLowerCase: "greeter" - ), - namespace: Name( - base: "hello.world", - generatedUpperCase: "Hello_World", - generatedLowerCase: "hello_world" - ), - methods: [expectedMethod] - ) - guard let service = parsedCodeGenRequest.services.first else { return XCTFail() } - XCTAssertEqual(service, expectedService) - XCTAssertEqual(service.methods.count, 1) - - XCTAssertEqual( - parsedCodeGenRequest.lookupSerializer("Hello_World_HelloRequest"), - "GRPCProtobuf.ProtobufSerializer()" - ) - XCTAssertEqual( - parsedCodeGenRequest.lookupDeserializer("Hello_World_HelloRequest"), - "GRPCProtobuf.ProtobufDeserializer()" - ) - } - func testParserEmptyPackage() throws { - let descriptorSet = DescriptorSet( - protos: [ - Google_Protobuf_FileDescriptorProto( - name: "same-module.proto", - package: "same-package" - ), - Google_Protobuf_FileDescriptorProto( - name: "different-module.proto", - package: "different-package" - ), - Google_Protobuf_FileDescriptorProto.helloWorldEmptyPackage, - ] - ) + @Suite("Service") + struct Service { + let service: GRPCCodeGen.ServiceDescriptor - guard let fileDescriptor = descriptorSet.fileDescriptor(named: "helloworld.proto") else { - return XCTFail( - """ - Could not find the file descriptor of "helloworld.proto". - """ - ) - } - let moduleMappings = SwiftProtobuf_GenSwift_ModuleMappings.with { - $0.mapping = [ - SwiftProtobuf_GenSwift_ModuleMappings.Entry.with { - $0.protoFilePath = ["different-module.proto"] - $0.moduleName = "DifferentModule" + init() throws { + let request = try parseDescriptor(try #require(try TestService.fileDescriptor)) + try #require(request.services.count == 1) + self.service = try #require(request.services.first) + } + + @Test("Name") + func name() { + #expect(self.service.name.base == "TestService") + } + + @Test("Namespace") + func namespace() { + #expect(self.service.namespace.base == "test") + } + + @Suite("Methods") + struct Methods { + let unary: GRPCCodeGen.MethodDescriptor + let clientStreaming: GRPCCodeGen.MethodDescriptor + let serverStreaming: GRPCCodeGen.MethodDescriptor + let bidiStreaming: GRPCCodeGen.MethodDescriptor + + init() throws { + let request = try parseDescriptor(try #require(try TestService.fileDescriptor)) + #expect(request.services.count == 1) + let service = try #require(request.services.first) + + self.unary = service.methods[0] + self.clientStreaming = service.methods[1] + self.serverStreaming = service.methods[2] + self.bidiStreaming = service.methods[3] } - ] - } - let parsedCodeGenRequest = try ProtobufCodeGenParser( - input: fileDescriptor, - protoFileModuleMappings: ProtoFileToModuleMappings(moduleMappingsProto: moduleMappings), - extraModuleImports: ["ExtraModule"], - accessLevel: .internal - ).parse() - - self.testCommonHelloworldParsedRequestFields(for: parsedCodeGenRequest) - - let expectedMethod = MethodDescriptor( - documentation: "/// Sends a greeting.\n", - name: 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 = ServiceDescriptor( - documentation: "/// The greeting service definition.\n", - name: Name( - base: "Greeter", - generatedUpperCase: "Greeter", - generatedLowerCase: "greeter" - ), - namespace: Name( - base: "", - generatedUpperCase: "", - generatedLowerCase: "" - ), - methods: [expectedMethod] - ) - guard let service = parsedCodeGenRequest.services.first else { return XCTFail() } - XCTAssertEqual(service, expectedService) - XCTAssertEqual(service.methods.count, 1) - - XCTAssertEqual( - parsedCodeGenRequest.lookupSerializer("HelloRequest"), - "GRPCProtobuf.ProtobufSerializer()" - ) - XCTAssertEqual( - parsedCodeGenRequest.lookupDeserializer("HelloRequest"), - "GRPCProtobuf.ProtobufDeserializer()" - ) - } -} -extension ProtobufCodeGenParserTests { - func testCommonHelloworldParsedRequestFields(for request: CodeGenerationRequest) { - XCTAssertEqual(request.fileName, "helloworld.proto") - XCTAssertEqual( - request.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(request.dependencies.count, 3) - let expectedDependencyNames = ["GRPCProtobuf", "DifferentModule", "ExtraModule"] - let parsedDependencyNames = request.dependencies.map { $0.module } - XCTAssertEqual(parsedDependencyNames, expectedDependencyNames) - XCTAssertEqual(request.services.count, 1) - } -} + @Test("Documentation") + func documentation() { + #expect(self.unary.documentation == "/// Unary docs.\n") + #expect(self.clientStreaming.documentation == "/// Client streaming docs.\n") + #expect(self.serverStreaming.documentation == "/// Server streaming docs.\n") + #expect(self.bidiStreaming.documentation == "/// Bidirectional streaming docs.\n") + } -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" + @Test("Name") + func name() { + #expect(self.unary.name.base == "Unary") + #expect(self.clientStreaming.name.base == "ClientStreaming") + #expect(self.serverStreaming.name.base == "ServerStreaming") + #expect(self.bidiStreaming.name.base == "BidirectionalStreaming") } - ] - } - 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" + + @Test("Input") + func input() { + #expect(self.unary.inputType == "Test_TestInput") + #expect(!self.unary.isInputStreaming) + + #expect(self.clientStreaming.inputType == "Test_TestInput") + #expect(self.clientStreaming.isInputStreaming) + + #expect(self.serverStreaming.inputType == "Test_TestInput") + #expect(!self.serverStreaming.isInputStreaming) + + #expect(self.bidiStreaming.inputType == "Test_TestInput") + #expect(self.bidiStreaming.isInputStreaming) } - ] - } - 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 + @Test("Output") + func output() { + #expect(self.unary.outputType == "Test_TestOutput") + #expect(!self.unary.isOutputStreaming) + + #expect(self.clientStreaming.outputType == "Test_TestOutput") + #expect(!self.clientStreaming.isOutputStreaming) + + #expect(self.serverStreaming.outputType == "Test_TestOutput") + #expect(self.serverStreaming.isOutputStreaming) + + #expect(self.bidiStreaming.outputType == "Test_TestOutput") + #expect(self.bidiStreaming.isOutputStreaming) } - ] - } - return Google_Protobuf_FileDescriptorProto.with { - $0.name = "helloworld.proto" - $0.package = "helloworld" - $0.dependency = ["same-module.proto", "different-module.proto"] - $0.publicDependency = [0, 1] - $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" } } - static var helloWorldNestedPackage: Google_Protobuf_FileDescriptorProto { - let service = Google_Protobuf_ServiceDescriptorProto.with { - $0.name = "Greeter" - $0.method = [ - Google_Protobuf_MethodDescriptorProto.with { - $0.name = "SayHello" - $0.inputType = ".hello.world.HelloRequest" - $0.outputType = ".hello.world.HelloReply" - $0.clientStreaming = false - $0.serverStreaming = false - } + @Suite("Multi-service file (foo-service.proto)") + struct FooService: UsesDescriptorSet { + static let descriptorSetName = "foo-service" + static let fileDescriptorName = "foo-service" + + let codeGen: CodeGenerationRequest + + init() throws { + let descriptor = try #require(try Self.fileDescriptor) + self.codeGen = try parseDescriptor(descriptor) + } + + @Test("Name") + func name() { + #expect(self.codeGen.fileName == "foo-service.proto") + } + + @Test("Dependencies") + func dependencies() { + let expected: [GRPCCodeGen.Dependency] = [ + .init(module: "GRPCProtobuf", accessLevel: .internal) // Always an internal import ] + #expect(self.codeGen.dependencies == expected) } - var helloWorldCopy = self.helloWorld - helloWorldCopy.package = "hello.world" - helloWorldCopy.service = [service] + @Test("Service1") + func service1() throws { + let service = self.codeGen.services[0] + #expect(service.name.base == "FooService1") + #expect(service.namespace.base == "foo") + #expect(service.methods.count == 1) + } - return helloWorldCopy - } + @Test("Service1.Method") + func service1Method() throws { + let method = self.codeGen.services[0].methods[0] + #expect(method.name.base == "Foo") + #expect(method.inputType == "Foo_FooInput") + #expect(method.outputType == "Foo_FooOutput") + } - static var helloWorldEmptyPackage: Google_Protobuf_FileDescriptorProto { - let service = Google_Protobuf_ServiceDescriptorProto.with { - $0.name = "Greeter" - $0.method = [ - Google_Protobuf_MethodDescriptorProto.with { - $0.name = "SayHello" - $0.inputType = ".HelloRequest" - $0.outputType = ".HelloReply" - $0.clientStreaming = false - $0.serverStreaming = false - } - ] + @Test("Service2") + func service2() throws { + let service = self.codeGen.services[1] + #expect(service.name.base == "FooService2") + #expect(service.namespace.base == "foo") + #expect(service.methods.count == 1) } - var helloWorldCopy = self.helloWorld - helloWorldCopy.package = "" - helloWorldCopy.service = [service] - return helloWorldCopy + @Test("Service2.Method") + func service2Method() throws { + let method = self.codeGen.services[1].methods[0] + #expect(method.name.base == "Foo") + #expect(method.inputType == "Foo_FooInput") + #expect(method.outputType == "Foo_FooOutput") + } } - internal init(name: String, package: String) { - self.init() - self.name = name - self.package = package + @Suite("Service with no package (bar-service.proto)") + struct BarService: UsesDescriptorSet { + static var descriptorSetName: String { "bar-service" } + static var fileDescriptorName: String { "bar-service" } + + let codeGen: CodeGenerationRequest + let service: GRPCCodeGen.ServiceDescriptor + + init() throws { + let descriptor = try #require(try Self.fileDescriptor) + self.codeGen = try parseDescriptor(descriptor) + self.service = try #require(self.codeGen.services.first) + } + + @Test("Service name") + func serviceName() { + #expect(self.service.name.base == "BarService") + } + + @Test("Service namespace") + func serviceNamespace() { + #expect(self.service.namespace.base == "") + } } } diff --git a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift index 17c0df5..6422b96 100644 --- a/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift +++ b/Tests/GRPCProtobufCodeGenTests/ProtobufCodeGeneratorTests.swift @@ -14,615 +14,1050 @@ * 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( - proto: Google_Protobuf_FileDescriptorProto.helloWorldNestedPackage, - 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 - - internal import GRPCCore - internal import GRPCProtobuf - internal import DifferentModule - internal import ExtraModule - - internal enum Hello_World_Greeter { - internal static let descriptor = GRPCCore.ServiceDescriptor.hello_world_Greeter - internal enum Method { - internal enum SayHello { - internal typealias Input = Hello_World_HelloRequest - internal typealias Output = Hello_World_HelloReply - internal static let descriptor = GRPCCore.MethodDescriptor( - service: Hello_World_Greeter.descriptor.fullyQualifiedService, - method: "SayHello" - ) - } - internal static let descriptors: [GRPCCore.MethodDescriptor] = [ - SayHello.descriptor - ] - } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias ClientProtocol = Hello_World_Greeter_ClientProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal typealias Client = Hello_World_Greeter_Client - } +import Testing + +struct ProtobufCodeGeneratorTests: UsesDescriptorSet { + static let descriptorSetName = "test-service" + static let fileDescriptorName = "test-service" + + @Test("Generate", arguments: [SourceGenerator.Config.AccessLevel.internal, .public, .package]) + func generate(accessLevel: SourceGenerator.Config.AccessLevel) throws { + let generator = ProtobufCodeGenerator( + config: SourceGenerator.Config( + accessLevel: accessLevel, + accessLevelOnImports: false, + client: true, + server: true, + indentation: 2 + ) + ) + + let access: String + switch accessLevel { + case .public: + access = "public" + case .internal: + access = "internal" + case .package: + access = "package" + default: + fatalError() + } + + let generated = try generator.generateCode( + fileDescriptor: Self.fileDescriptor, + protoFileModuleMappings: ProtoFileToModuleMappings(), + extraModuleImports: [] + ) + + let expected = """ + /// Leading trivia. + + // DO NOT EDIT. + // swift-format-ignore-file + // + // Generated by the gRPC Swift generator plugin for the protocol buffer compiler. + // Source: test-service.proto + // + // For information on using the generated types, please see the documentation: + // https://github.com/grpc/grpc-swift + + import GRPCCore + import GRPCProtobuf + + // MARK: - test.TestService - extension GRPCCore.ServiceDescriptor { - internal static let hello_world_Greeter = Self( - package: "hello.world", - service: "Greeter" + /// Namespace containing generated types for the "test.TestService" service. + \(access) enum Test_TestService { + /// Service descriptor for the "test.TestService" service. + \(access) static let descriptor = GRPCCore.ServiceDescriptor(fullyQualifiedService: "test.TestService") + /// Namespace for method metadata. + \(access) enum Method { + /// Namespace for "Unary" metadata. + \(access) enum Unary { + /// Request type for "Unary". + \(access) typealias Input = Test_TestInput + /// Response type for "Unary". + \(access) typealias Output = Test_TestOutput + /// Descriptor for "Unary". + \(access) static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "test.TestService"), + method: "Unary" + ) + } + /// Namespace for "ClientStreaming" metadata. + \(access) enum ClientStreaming { + /// Request type for "ClientStreaming". + \(access) typealias Input = Test_TestInput + /// Response type for "ClientStreaming". + \(access) typealias Output = Test_TestOutput + /// Descriptor for "ClientStreaming". + \(access) static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "test.TestService"), + method: "ClientStreaming" + ) + } + /// Namespace for "ServerStreaming" metadata. + \(access) enum ServerStreaming { + /// Request type for "ServerStreaming". + \(access) typealias Input = Test_TestInput + /// Response type for "ServerStreaming". + \(access) typealias Output = Test_TestOutput + /// Descriptor for "ServerStreaming". + \(access) static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "test.TestService"), + method: "ServerStreaming" + ) + } + /// Namespace for "BidirectionalStreaming" metadata. + \(access) enum BidirectionalStreaming { + /// Request type for "BidirectionalStreaming". + \(access) typealias Input = Test_TestInput + /// Response type for "BidirectionalStreaming". + \(access) typealias Output = Test_TestOutput + /// Descriptor for "BidirectionalStreaming". + \(access) static let descriptor = GRPCCore.MethodDescriptor( + service: GRPCCore.ServiceDescriptor(fullyQualifiedService: "test.TestService"), + method: "BidirectionalStreaming" ) + } + /// Descriptors for all methods in the "test.TestService" service. + \(access) static let descriptors: [GRPCCore.MethodDescriptor] = [ + Unary.descriptor, + ClientStreaming.descriptor, + ServerStreaming.descriptor, + BidirectionalStreaming.descriptor + ] } + } - /// The greeting service definition. - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal protocol Hello_World_Greeter_ClientProtocol: Sendable { - /// Sends a greeting. - func sayHello( - request: GRPCCore.ClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R - ) async throws -> R where R: Sendable - } + extension GRPCCore.ServiceDescriptor { + /// Service descriptor for the "test.TestService" service. + \(access) static let test_TestService = GRPCCore.ServiceDescriptor(fullyQualifiedService: "test.TestService") + } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - extension Hello_World_Greeter.ClientProtocol { - internal func sayHello( - request: GRPCCore.ClientRequest, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R = { - try $0.message - } - ) async throws -> R where R: Sendable { - try await self.sayHello( - request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), - options: options, - body - ) - } + // MARK: test.TestService (server) + + extension Test_TestService { + /// Streaming variant of the service protocol for the "test.TestService" service. + /// + /// This protocol is the lowest-level of the service protocols generated for this service + /// giving you the most flexibility over the implementation of your service. This comes at + /// the cost of more verbose and less strict APIs. Each RPC requires you to implement it in + /// terms of a request stream and response stream. Where only a single request or response + /// message is expected, you are responsible for enforcing this invariant is maintained. + /// + /// Where possible, prefer using the stricter, less-verbose ``ServiceProtocol`` + /// or ``SimpleServiceProtocol`` instead. + /// + /// > Source IDL Documentation: + /// > + /// > Service docs. + \(access) protocol StreamingServiceProtocol: GRPCCore.RegistrableRPCService { + /// Handle the "Unary" method. + /// + /// > Source IDL Documentation: + /// > + /// > Unary docs. + /// + /// - Parameters: + /// - request: A streaming request of `Test_TestInput` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Test_TestOutput` messages. + func unary( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "ClientStreaming" method. + /// + /// > Source IDL Documentation: + /// > + /// > Client streaming docs. + /// + /// - Parameters: + /// - request: A streaming request of `Test_TestInput` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Test_TestOutput` messages. + func clientStreaming( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "ServerStreaming" method. + /// + /// > Source IDL Documentation: + /// > + /// > Server streaming docs. + /// + /// - Parameters: + /// - request: A streaming request of `Test_TestInput` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Test_TestOutput` messages. + func serverStreaming( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "BidirectionalStreaming" method. + /// + /// > Source IDL Documentation: + /// > + /// > Bidirectional streaming docs. + /// + /// - Parameters: + /// - request: A streaming request of `Test_TestInput` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Test_TestOutput` messages. + func bidirectionalStreaming( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - extension Hello_World_Greeter.ClientProtocol { - /// Sends a greeting. - internal func sayHello( - _ message: Hello_World_HelloRequest, - metadata: GRPCCore.Metadata = [:], - options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { - try $0.message - } - ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest( - message: message, - metadata: metadata - ) - return try await self.sayHello( - request: request, - options: options, - handleResponse - ) - } + /// Service protocol for the "test.TestService" service. + /// + /// This protocol is higher level than ``StreamingServiceProtocol`` but lower level than + /// the ``SimpleServiceProtocol``, it provides access to request and response metadata and + /// trailing response metadata. If you don't need these then consider using + /// the ``SimpleServiceProtocol``. If you need fine grained control over your RPCs then + /// use ``StreamingServiceProtocol``. + /// + /// > Source IDL Documentation: + /// > + /// > Service docs. + \(access) protocol ServiceProtocol: Test_TestService.StreamingServiceProtocol { + /// Handle the "Unary" method. + /// + /// > Source IDL Documentation: + /// > + /// > Unary docs. + /// + /// - Parameters: + /// - request: A request containing a single `Test_TestInput` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `Test_TestOutput` message. + func unary( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "ClientStreaming" method. + /// + /// > Source IDL Documentation: + /// > + /// > Client streaming docs. + /// + /// - Parameters: + /// - request: A streaming request of `Test_TestInput` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A response containing a single `Test_TestOutput` message. + func clientStreaming( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse + + /// Handle the "ServerStreaming" method. + /// + /// > Source IDL Documentation: + /// > + /// > Server streaming docs. + /// + /// - Parameters: + /// - request: A request containing a single `Test_TestInput` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Test_TestOutput` messages. + func serverStreaming( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse + + /// Handle the "BidirectionalStreaming" method. + /// + /// > Source IDL Documentation: + /// > + /// > Bidirectional streaming docs. + /// + /// - Parameters: + /// - request: A streaming request of `Test_TestInput` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A streaming response of `Test_TestOutput` messages. + func bidirectionalStreaming( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse } - /// The greeting service definition. - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - internal struct Hello_World_Greeter_Client: Hello_World_Greeter.ClientProtocol { - private let client: GRPCCore.GRPCClient + /// Simple service protocol for the "test.TestService" service. + /// + /// This is the highest level protocol for the service. The API is the easiest to use but + /// doesn't provide access to request or response metadata. If you need access to these + /// then use ``ServiceProtocol`` instead. + /// + /// > Source IDL Documentation: + /// > + /// > Service docs. + \(access) protocol SimpleServiceProtocol: Test_TestService.ServiceProtocol { + /// Handle the "Unary" method. + /// + /// > Source IDL Documentation: + /// > + /// > Unary docs. + /// + /// - Parameters: + /// - request: A `Test_TestInput` message. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `Test_TestOutput` to respond with. + func unary( + request: Test_TestInput, + context: GRPCCore.ServerContext + ) async throws -> Test_TestOutput - internal init(wrapping client: GRPCCore.GRPCClient) { - self.client = client - } + /// Handle the "ClientStreaming" method. + /// + /// > Source IDL Documentation: + /// > + /// > Client streaming docs. + /// + /// - Parameters: + /// - request: A stream of `Test_TestInput` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + /// - Returns: A `Test_TestOutput` to respond with. + func clientStreaming( + request: GRPCCore.RPCAsyncSequence, + context: GRPCCore.ServerContext + ) async throws -> Test_TestOutput - /// Sends a greeting. - internal func sayHello( - request: GRPCCore.ClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, - options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R = { - try $0.message - } - ) async throws -> R where R: Sendable { - try await self.client.unary( - request: request, - descriptor: Hello_World_Greeter.Method.SayHello.descriptor, - serializer: serializer, - deserializer: deserializer, - options: options, - handler: body - ) - } + /// Handle the "ServerStreaming" method. + /// + /// > Source IDL Documentation: + /// > + /// > Server streaming docs. + /// + /// - Parameters: + /// - request: A `Test_TestInput` message. + /// - response: A response stream of `Test_TestOutput` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + func serverStreaming( + request: Test_TestInput, + response: GRPCCore.RPCWriter, + context: GRPCCore.ServerContext + ) async throws + + /// Handle the "BidirectionalStreaming" method. + /// + /// > Source IDL Documentation: + /// > + /// > Bidirectional streaming docs. + /// + /// - Parameters: + /// - request: A stream of `Test_TestInput` messages. + /// - response: A response stream of `Test_TestOutput` messages. + /// - context: Context providing information about the RPC. + /// - Throws: Any error which occurred during the processing of the request. Thrown errors + /// of type `RPCError` are mapped to appropriate statuses. All other errors are converted + /// to an internal error. + func bidirectionalStreaming( + request: GRPCCore.RPCAsyncSequence, + response: GRPCCore.RPCWriter, + context: GRPCCore.ServerContext + ) async throws } - """ - ) + } - try testCodeGeneration( - proto: Google_Protobuf_FileDescriptorProto.helloWorld, - 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 - - public import GRPCCore - internal import GRPCProtobuf - public import DifferentModule - public import ExtraModule - - public enum Helloworld_Greeter { - public static let descriptor = GRPCCore.ServiceDescriptor.helloworld_Greeter - public enum Method { - public enum SayHello { - public typealias Input = Helloworld_HelloRequest - public typealias Output = Helloworld_HelloReply - public static let descriptor = GRPCCore.MethodDescriptor( - service: Helloworld_Greeter.descriptor.fullyQualifiedService, - method: "SayHello" + // Default implementation of 'registerMethods(with:)'. + extension Test_TestService.StreamingServiceProtocol { + \(access) func registerMethods(with router: inout GRPCCore.RPCRouter) { + router.registerHandler( + forMethod: Test_TestService.Method.Unary.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.unary( + request: request, + context: context ) } - public static let descriptors: [GRPCCore.MethodDescriptor] = [ - SayHello.descriptor - ] - } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias StreamingServiceProtocol = Helloworld_Greeter_StreamingServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public typealias ServiceProtocol = Helloworld_Greeter_ServiceProtocol + ) + router.registerHandler( + forMethod: Test_TestService.Method.ClientStreaming.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.clientStreaming( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Test_TestService.Method.ServerStreaming.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.serverStreaming( + request: request, + context: context + ) + } + ) + router.registerHandler( + forMethod: Test_TestService.Method.BidirectionalStreaming.descriptor, + deserializer: GRPCProtobuf.ProtobufDeserializer(), + serializer: GRPCProtobuf.ProtobufSerializer(), + handler: { request, context in + try await self.bidirectionalStreaming( + request: request, + context: context + ) + } + ) } + } - extension GRPCCore.ServiceDescriptor { - public static let helloworld_Greeter = Self( - package: "helloworld", - service: "Greeter" + // Default implementation of streaming methods from 'StreamingServiceProtocol'. + extension Test_TestService.ServiceProtocol { + \(access) func unary( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.unary( + request: GRPCCore.ServerRequest(stream: request), + context: context ) + return GRPCCore.StreamingServerResponse(single: response) } - /// The greeting service definition. - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public protocol Helloworld_Greeter_StreamingServiceProtocol: GRPCCore.RegistrableRPCService { - /// Sends a greeting. - func sayHello( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse + \(access) func clientStreaming( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.clientStreaming( + request: request, + context: context + ) + return GRPCCore.StreamingServerResponse(single: response) } - /// Conformance to `GRPCCore.RegistrableRPCService`. - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - extension Helloworld_Greeter.StreamingServiceProtocol { - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public func registerMethods(with router: inout GRPCCore.RPCRouter) { - router.registerHandler( - forMethod: Helloworld_Greeter.Method.SayHello.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.sayHello( - request: request, - context: context - ) - } - ) - } + \(access) func serverStreaming( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + let response = try await self.serverStreaming( + request: GRPCCore.ServerRequest(stream: request), + context: context + ) + return response } + } - /// The greeting service definition. - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - public protocol Helloworld_Greeter_ServiceProtocol: Helloworld_Greeter.StreamingServiceProtocol { - /// Sends a greeting. - func sayHello( - request: GRPCCore.ServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse + // Default implementation of methods from 'ServiceProtocol'. + extension Test_TestService.SimpleServiceProtocol { + \(access) func unary( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.unary( + request: request.message, + context: context + ), + metadata: [:] + ) } - /// Partial conformance to `Helloworld_Greeter_StreamingServiceProtocol`. - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - extension Helloworld_Greeter.ServiceProtocol { - public func sayHello( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse { - let response = try await self.sayHello( - request: GRPCCore.ServerRequest(stream: request), + \(access) func clientStreaming( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.ServerResponse { + return GRPCCore.ServerResponse( + message: try await self.clientStreaming( + request: request.messages, context: context - ) - return GRPCCore.StreamingServerResponse(single: response) - } + ), + metadata: [:] + ) } - """ - ) - try testCodeGeneration( - proto: Google_Protobuf_FileDescriptorProto.helloWorldEmptyPackage, - 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 - - package import GRPCCore - internal import GRPCProtobuf - package import DifferentModule - package import ExtraModule - - package enum Greeter { - package static let descriptor = GRPCCore.ServiceDescriptor.Greeter - package enum Method { - package enum SayHello { - package typealias Input = HelloRequest - package typealias Output = HelloReply - package static let descriptor = GRPCCore.MethodDescriptor( - service: Greeter.descriptor.fullyQualifiedService, - method: "SayHello" + \(access) func serverStreaming( + request: GRPCCore.ServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + return GRPCCore.StreamingServerResponse( + metadata: [:], + producer: { writer in + try await self.serverStreaming( + request: request.message, + response: writer, + context: context ) + return [:] } - package static let descriptors: [GRPCCore.MethodDescriptor] = [ - SayHello.descriptor - ] - } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package typealias StreamingServiceProtocol = Greeter_StreamingServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package typealias ServiceProtocol = Greeter_ServiceProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package typealias ClientProtocol = Greeter_ClientProtocol - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package typealias Client = Greeter_Client + ) } - extension GRPCCore.ServiceDescriptor { - package static let Greeter = Self( - package: "", - service: "Greeter" + \(access) func bidirectionalStreaming( + request: GRPCCore.StreamingServerRequest, + context: GRPCCore.ServerContext + ) async throws -> GRPCCore.StreamingServerResponse { + return GRPCCore.StreamingServerResponse( + metadata: [:], + producer: { writer in + try await self.bidirectionalStreaming( + request: request.messages, + response: writer, + context: context + ) + return [:] + } ) } + } - /// The greeting service definition. - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package protocol Greeter_StreamingServiceProtocol: GRPCCore.RegistrableRPCService { - /// Sends a greeting. - func sayHello( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse - } + // MARK: test.TestService (client) - /// Conformance to `GRPCCore.RegistrableRPCService`. - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - extension Greeter.StreamingServiceProtocol { - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package func registerMethods(with router: inout GRPCCore.RPCRouter) { - router.registerHandler( - forMethod: Greeter.Method.SayHello.descriptor, - deserializer: GRPCProtobuf.ProtobufDeserializer(), - serializer: GRPCProtobuf.ProtobufSerializer(), - handler: { request, context in - try await self.sayHello( - request: request, - context: context - ) - } - ) - } - } + extension Test_TestService { + /// Generated client protocol for the "test.TestService" service. + /// + /// You don't need to implement this protocol directly, use the generated + /// implementation, ``Client``. + /// + /// > Source IDL Documentation: + /// > + /// > Service docs. + \(access) protocol ClientProtocol: Sendable { + /// Call the "Unary" method. + /// + /// > Source IDL Documentation: + /// > + /// > Unary docs. + /// + /// - Parameters: + /// - request: A request containing a single `Test_TestInput` message. + /// - serializer: A serializer for `Test_TestInput` messages. + /// - deserializer: A deserializer for `Test_TestOutput` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func unary( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable - /// The greeting service definition. - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package protocol Greeter_ServiceProtocol: Greeter.StreamingServiceProtocol { - /// Sends a greeting. - func sayHello( - request: GRPCCore.ServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.ServerResponse - } + /// Call the "ClientStreaming" method. + /// + /// > Source IDL Documentation: + /// > + /// > Client streaming docs. + /// + /// - Parameters: + /// - request: A streaming request producing `Test_TestInput` messages. + /// - serializer: A serializer for `Test_TestInput` messages. + /// - deserializer: A deserializer for `Test_TestOutput` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func clientStreaming( + request: GRPCCore.StreamingClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable - /// Partial conformance to `Greeter_StreamingServiceProtocol`. - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - extension Greeter.ServiceProtocol { - package func sayHello( - request: GRPCCore.StreamingServerRequest, - context: GRPCCore.ServerContext - ) async throws -> GRPCCore.StreamingServerResponse { - let response = try await self.sayHello( - request: GRPCCore.ServerRequest(stream: request), - context: context - ) - return GRPCCore.StreamingServerResponse(single: response) - } - } + /// Call the "ServerStreaming" method. + /// + /// > Source IDL Documentation: + /// > + /// > Server streaming docs. + /// + /// - Parameters: + /// - request: A request containing a single `Test_TestInput` message. + /// - serializer: A serializer for `Test_TestInput` messages. + /// - deserializer: A deserializer for `Test_TestOutput` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func serverStreaming( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable - /// The greeting service definition. - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package protocol Greeter_ClientProtocol: Sendable { - /// Sends a greeting. - func sayHello( - request: GRPCCore.ClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, + /// Call the "BidirectionalStreaming" method. + /// + /// > Source IDL Documentation: + /// > + /// > Bidirectional streaming docs. + /// + /// - Parameters: + /// - request: A streaming request producing `Test_TestInput` messages. + /// - serializer: A serializer for `Test_TestInput` messages. + /// - deserializer: A deserializer for `Test_TestOutput` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + func bidirectionalStreaming( + request: GRPCCore.StreamingClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, options: GRPCCore.CallOptions, - _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R - ) async throws -> R where R: Sendable + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - extension Greeter.ClientProtocol { - package func sayHello( - request: GRPCCore.ClientRequest, + /// Generated client for the "test.TestService" service. + /// + /// The ``Client`` provides an implementation of ``ClientProtocol`` which wraps + /// a `GRPCCore.GRPCCClient`. The underlying `GRPCClient` provides the long-lived + /// means of communication with the remote peer. + /// + /// > Source IDL Documentation: + /// > + /// > Service docs. + \(access) struct Client: ClientProtocol { + private let client: GRPCCore.GRPCClient + + /// Creates a new client wrapping the provided `GRPCCore.GRPCClient`. + /// + /// - Parameters: + /// - client: A `GRPCCore.GRPCClient` providing a communication channel to the service. + \(access) init(wrapping client: GRPCCore.GRPCClient) { + self.client = client + } + + /// Call the "Unary" method. + /// + /// > Source IDL Documentation: + /// > + /// > Unary docs. + /// + /// - Parameters: + /// - request: A request containing a single `Test_TestInput` message. + /// - serializer: A serializer for `Test_TestInput` messages. + /// - deserializer: A deserializer for `Test_TestOutput` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + \(access) func unary( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R = { - try $0.message + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message } - ) async throws -> R where R: Sendable { - try await self.sayHello( + ) async throws -> Result where Result: Sendable { + try await self.client.unary( request: request, - serializer: GRPCProtobuf.ProtobufSerializer(), - deserializer: GRPCProtobuf.ProtobufDeserializer(), + descriptor: Test_TestService.Method.Unary.descriptor, + serializer: serializer, + deserializer: deserializer, options: options, - body + onResponse: handleResponse ) } - } - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - extension Greeter.ClientProtocol { - /// Sends a greeting. - package func sayHello( - _ message: HelloRequest, - metadata: GRPCCore.Metadata = [:], + /// Call the "ClientStreaming" method. + /// + /// > Source IDL Documentation: + /// > + /// > Client streaming docs. + /// + /// - Parameters: + /// - request: A streaming request producing `Test_TestInput` messages. + /// - serializer: A serializer for `Test_TestInput` messages. + /// - deserializer: A deserializer for `Test_TestOutput` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + \(access) func clientStreaming( + request: GRPCCore.StreamingClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, options: GRPCCore.CallOptions = .defaults, - onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { - try $0.message + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message } ) async throws -> Result where Result: Sendable { - let request = GRPCCore.ClientRequest( - message: message, - metadata: metadata - ) - return try await self.sayHello( + try await self.client.clientStreaming( request: request, + descriptor: Test_TestService.Method.ClientStreaming.descriptor, + serializer: serializer, + deserializer: deserializer, options: options, - handleResponse + onResponse: handleResponse ) } - } - - /// The greeting service definition. - @available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) - package struct Greeter_Client: Greeter.ClientProtocol { - private let client: GRPCCore.GRPCClient - package init(wrapping client: GRPCCore.GRPCClient) { - self.client = client + /// Call the "ServerStreaming" method. + /// + /// > Source IDL Documentation: + /// > + /// > Server streaming docs. + /// + /// - Parameters: + /// - request: A request containing a single `Test_TestInput` message. + /// - serializer: A serializer for `Test_TestInput` messages. + /// - deserializer: A deserializer for `Test_TestOutput` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + \(access) func serverStreaming( + request: GRPCCore.ClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { + try await self.client.serverStreaming( + request: request, + descriptor: Test_TestService.Method.ServerStreaming.descriptor, + serializer: serializer, + deserializer: deserializer, + options: options, + onResponse: handleResponse + ) } - /// Sends a greeting. - package func sayHello( - request: GRPCCore.ClientRequest, - serializer: some GRPCCore.MessageSerializer, - deserializer: some GRPCCore.MessageDeserializer, + /// Call the "BidirectionalStreaming" method. + /// + /// > Source IDL Documentation: + /// > + /// > Bidirectional streaming docs. + /// + /// - Parameters: + /// - request: A streaming request producing `Test_TestInput` messages. + /// - serializer: A serializer for `Test_TestInput` messages. + /// - deserializer: A deserializer for `Test_TestOutput` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + \(access) func bidirectionalStreaming( + request: GRPCCore.StreamingClientRequest, + serializer: some GRPCCore.MessageSerializer, + deserializer: some GRPCCore.MessageDeserializer, options: GRPCCore.CallOptions = .defaults, - _ body: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> R = { - try $0.message - } - ) async throws -> R where R: Sendable { - try await self.client.unary( + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { + try await self.client.bidirectionalStreaming( request: request, - descriptor: Greeter.Method.SayHello.descriptor, + descriptor: Test_TestService.Method.BidirectionalStreaming.descriptor, serializer: serializer, deserializer: deserializer, options: options, - handler: body + onResponse: handleResponse ) } } - """ - ) - } + } - func testNoAccessLevelOnImports() throws { - let proto = Google_Protobuf_FileDescriptorProto(name: "helloworld.proto", package: "") - try testCodeGeneration( - proto: proto, - indentation: 2, - visibility: .package, - client: true, - server: true, - accessLevelOnImports: false, - expectedCode: """ - // 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 - import ExtraModule - - """ - ) - } + // Helpers providing default arguments to 'ClientProtocol' methods. + extension Test_TestService.ClientProtocol { + /// Call the "Unary" method. + /// + /// > Source IDL Documentation: + /// > + /// > Unary docs. + /// + /// - Parameters: + /// - request: A request containing a single `Test_TestInput` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + \(access) func unary( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.unary( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } - func testCodeGeneration( - proto: Google_Protobuf_FileDescriptorProto, - indentation: Int, - visibility: SourceGenerator.Config.AccessLevel, - client: Bool, - server: Bool, - accessLevelOnImports: Bool = true, - expectedCode: String, - file: StaticString = #filePath, - line: UInt = #line - ) throws { - let config = SourceGenerator.Config( - accessLevel: visibility, - accessLevelOnImports: accessLevelOnImports, - client: client, - server: server, - indentation: indentation - ) + /// Call the "ClientStreaming" method. + /// + /// > Source IDL Documentation: + /// > + /// > Client streaming docs. + /// + /// - Parameters: + /// - request: A streaming request producing `Test_TestInput` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + \(access) func clientStreaming( + request: GRPCCore.StreamingClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + try await self.clientStreaming( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } - let descriptorSet = DescriptorSet( - protos: [ - Google_Protobuf_FileDescriptorProto(name: "same-module.proto", package: "same-package"), - Google_Protobuf_FileDescriptorProto( - name: "different-module.proto", - package: "different-package" - ), - proto, - ]) - guard let fileDescriptor = descriptorSet.fileDescriptor(named: "helloworld.proto") else { - return XCTFail( - """ - Could not find the file descriptor of "helloworld.proto". - """ - ) - } + /// Call the "ServerStreaming" method. + /// + /// > Source IDL Documentation: + /// > + /// > Server streaming docs. + /// + /// - Parameters: + /// - request: A request containing a single `Test_TestInput` message. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + \(access) func serverStreaming( + request: GRPCCore.ClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { + try await self.serverStreaming( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) + } - let moduleMappings = SwiftProtobuf_GenSwift_ModuleMappings.with { - $0.mapping = [ - SwiftProtobuf_GenSwift_ModuleMappings.Entry.with { - $0.protoFilePath = ["different-module.proto"] - $0.moduleName = "DifferentModule" + /// Call the "BidirectionalStreaming" method. + /// + /// > Source IDL Documentation: + /// > + /// > Bidirectional streaming docs. + /// + /// - Parameters: + /// - request: A streaming request producing `Test_TestInput` messages. + /// - options: Options to apply to this RPC. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + \(access) func bidirectionalStreaming( + request: GRPCCore.StreamingClientRequest, + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { + try await self.bidirectionalStreaming( + request: request, + serializer: GRPCProtobuf.ProtobufSerializer(), + deserializer: GRPCProtobuf.ProtobufDeserializer(), + options: options, + onResponse: handleResponse + ) } - ] - } - let generator = ProtobufCodeGenerator(configuration: config) - try XCTAssertEqualWithDiff( - try generator.generateCode( - from: fileDescriptor, - protoFileModuleMappings: ProtoFileToModuleMappings(moduleMappingsProto: moduleMappings), - extraModuleImports: ["ExtraModule"] - ), - expectedCode, - file: file, - line: line - ) - } -} + } -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) -} + // Helpers providing sugared APIs for 'ClientProtocol' methods. + extension Test_TestService.ClientProtocol { + /// Call the "Unary" method. + /// + /// > Source IDL Documentation: + /// > + /// > Unary docs. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + \(access) func unary( + _ message: Test_TestInput, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.unary( + request: request, + options: options, + onResponse: handleResponse + ) + } -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 - ) -} + /// Call the "ClientStreaming" method. + /// + /// > Source IDL Documentation: + /// > + /// > Client streaming docs. + /// + /// - Parameters: + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - producer: A closure producing request messages to send to the server. The request + /// stream is closed when the closure returns. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + \(access) func clientStreaming( + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + requestProducer producer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, + onResponse handleResponse: @Sendable @escaping (GRPCCore.ClientResponse) async throws -> Result = { response in + try response.message + } + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.StreamingClientRequest( + metadata: metadata, + producer: producer + ) + return try await self.clientStreaming( + request: request, + options: options, + onResponse: handleResponse + ) + } -#endif // os(macOS) || os(Linux) + /// Call the "ServerStreaming" method. + /// + /// > Source IDL Documentation: + /// > + /// > Server streaming docs. + /// + /// - Parameters: + /// - message: request message to send. + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + \(access) func serverStreaming( + _ message: Test_TestInput, + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.ClientRequest( + message: message, + metadata: metadata + ) + return try await self.serverStreaming( + request: request, + options: options, + onResponse: handleResponse + ) + } + + /// Call the "BidirectionalStreaming" method. + /// + /// > Source IDL Documentation: + /// > + /// > Bidirectional streaming docs. + /// + /// - Parameters: + /// - metadata: Additional metadata to send, defaults to empty. + /// - options: Options to apply to this RPC, defaults to `.defaults`. + /// - producer: A closure producing request messages to send to the server. The request + /// stream is closed when the closure returns. + /// - handleResponse: A closure which handles the response, the result of which is + /// returned to the caller. Returning from the closure will cancel the RPC if it + /// hasn't already finished. + /// - Returns: The result of `handleResponse`. + \(access) func bidirectionalStreaming( + metadata: GRPCCore.Metadata = [:], + options: GRPCCore.CallOptions = .defaults, + requestProducer producer: @Sendable @escaping (GRPCCore.RPCWriter) async throws -> Void, + onResponse handleResponse: @Sendable @escaping (GRPCCore.StreamingClientResponse) async throws -> Result + ) async throws -> Result where Result: Sendable { + let request = GRPCCore.StreamingClientRequest( + metadata: metadata, + producer: producer + ) + return try await self.bidirectionalStreaming( + request: request, + options: options, + onResponse: handleResponse + ) + } + } + """ + + #expect(generated == expected) + } +} diff --git a/Tests/GRPCProtobufCodeGenTests/Utilities.swift b/Tests/GRPCProtobufCodeGenTests/Utilities.swift new file mode 100644 index 0000000..203e922 --- /dev/null +++ b/Tests/GRPCProtobufCodeGenTests/Utilities.swift @@ -0,0 +1,82 @@ +/* + * 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 GRPCProtobufCodeGen +import SwiftProtobuf +import SwiftProtobufPluginLibrary +import Testing + +import struct GRPCCodeGen.CodeGenerationRequest +import struct GRPCCodeGen.SourceGenerator + +protocol UsesDescriptorSet { + static var descriptorSetName: String { get } + static var fileDescriptorName: String { get } + + static var descriptorSet: DescriptorSet { get throws } + static var fileDescriptor: FileDescriptor { get throws } +} + +extension UsesDescriptorSet { + static var descriptorSet: DescriptorSet { + get throws { + try loadDescriptorSet(named: Self.descriptorSetName) + } + } + + static var fileDescriptor: FileDescriptor { + get throws { + let descriptorSet = try Self.descriptorSet + if let fileDescriptor = descriptorSet.fileDescriptor(named: fileDescriptorName + ".proto") { + return fileDescriptor + } else { + throw MissingFileDescriptor() + } + } + } +} + +struct MissingFileDescriptor: Error {} + +private func loadDescriptorSet( + named name: String, + withExtension extension: String = "pb" +) throws -> DescriptorSet { + let maybeURL = Bundle.module.url( + forResource: name, + withExtension: `extension`, + subdirectory: "Generated" + ) + + let url = try #require(maybeURL) + let data = try #require(try Data(contentsOf: url)) + let descriptorSet = try Google_Protobuf_FileDescriptorSet(serializedBytes: data) + return DescriptorSet(proto: descriptorSet) +} + +func parseDescriptor( + _ descriptor: FileDescriptor, + extraModuleImports: [String] = [], + accessLevel: SourceGenerator.Config.AccessLevel = .internal +) throws -> CodeGenerationRequest { + let parser = ProtobufCodeGenParser( + protoFileModuleMappings: .init(), + extraModuleImports: extraModuleImports, + accessLevel: accessLevel + ) + return try parser.parse(descriptor: descriptor) +} diff --git a/dev/protos/generate.sh b/dev/protos/generate.sh new file mode 100755 index 0000000..8f87099 --- /dev/null +++ b/dev/protos/generate.sh @@ -0,0 +1,67 @@ +#!/bin/bash +## 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. + +set -eu + +here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +root="$here/../.." +protoc=$(which protoc) + +function invoke_protoc { + # Setting -x when running the script produces a lot of output, instead boil + # just echo out the protoc invocations. + echo "$protoc" "$@" + "$protoc" "$@" +} + +#- DESCRIPTOR SETS ------------------------------------------------------------ + +function generate_test_service_descriptor_set { + local proto proto_path output + proto="$here/local/test-service.proto" + proto_path="$(dirname "$proto")" + output="$root/Tests/GRPCProtobufCodeGenTests/Generated/test-service.pb" + + invoke_protoc --descriptor_set_out="$output" "$proto" -I "$proto_path" --include_source_info +} + +function generate_foo_service_descriptor_set { + local proto proto_path output + proto="$here/local/foo-service.proto" + proto_path="$(dirname "$proto")" + output="$root/Tests/GRPCProtobufCodeGenTests/Generated/foo-service.pb" + + invoke_protoc --descriptor_set_out="$output" "$proto" -I "$proto_path" \ + --include_source_info \ + --include_imports +} + +function generate_bar_service_descriptor_set { + local proto proto_path output + proto="$here/local/bar-service.proto" + proto_path="$(dirname "$proto")" + output="$root/Tests/GRPCProtobufCodeGenTests/Generated/bar-service.pb" + + invoke_protoc --descriptor_set_out="$output" "$proto" -I "$proto_path" \ + --include_source_info \ + --include_imports +} + +#------------------------------------------------------------------------------ + +# Descriptor sets +generate_test_service_descriptor_set +generate_foo_service_descriptor_set +generate_bar_service_descriptor_set diff --git a/dev/protos/local/bar-service.proto b/dev/protos/local/bar-service.proto new file mode 100644 index 0000000..816815c --- /dev/null +++ b/dev/protos/local/bar-service.proto @@ -0,0 +1,3 @@ +syntax = "proto3"; + +service BarService {} diff --git a/dev/protos/local/foo-messages.proto b/dev/protos/local/foo-messages.proto new file mode 100644 index 0000000..7b9beca --- /dev/null +++ b/dev/protos/local/foo-messages.proto @@ -0,0 +1,6 @@ +syntax = "proto3"; + +package foo; + +message FooInput {} +message FooOutput {} diff --git a/dev/protos/local/foo-service.proto b/dev/protos/local/foo-service.proto new file mode 100644 index 0000000..441a061 --- /dev/null +++ b/dev/protos/local/foo-service.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; + +import "foo-messages.proto"; + +package foo; + +service FooService1 { + rpc Foo (FooInput) returns (FooOutput) {} +} + +service FooService2 { + rpc Foo (FooInput) returns (FooOutput) {} +} diff --git a/dev/protos/local/test-service.proto b/dev/protos/local/test-service.proto new file mode 100644 index 0000000..6bc50cd --- /dev/null +++ b/dev/protos/local/test-service.proto @@ -0,0 +1,19 @@ +// Leading trivia. +syntax = "proto3"; + +package test; + +// Service docs. +service TestService { + // Unary docs. + rpc Unary (TestInput) returns (TestOutput) {} + // Client streaming docs. + rpc ClientStreaming (stream TestInput) returns (TestOutput) {} + // Server streaming docs. + rpc ServerStreaming (TestInput) returns (stream TestOutput) {} + // Bidirectional streaming docs. + rpc BidirectionalStreaming (stream TestInput) returns (stream TestOutput) {} +} + +message TestInput {} +message TestOutput {}