diff --git a/Makefile b/Makefile index d0fe6930a..d9e10b0cf 100644 --- a/Makefile +++ b/Makefile @@ -117,6 +117,20 @@ ${NORMALIZATION_GRPC}: ${NORMALIZATION_PROTO} ${PROTOC_GEN_GRPC_SWIFT} .PHONY: generate-normalization: ${NORMALIZATION_PB} ${NORMALIZATION_GRPC} +SERIALIZATION_GRPC_REFLECTION=Tests/GRPCTests/Codegen/Serialization/echo.grpc.reflection.txt + +# For serialization we'll set the ReflectionData option to true. +${SERIALIZATION_GRPC_REFLECTION}: ${ECHO_PROTO} ${PROTOC_GEN_GRPC_SWIFT} + protoc $< \ + --proto_path=$(dir $<) \ + --plugin=${PROTOC_GEN_GRPC_SWIFT} \ + --grpc-swift_opt=Client=false,Server=false,ReflectionData=true \ + --grpc-swift_out=$(dir ${SERIALIZATION_GRPC_REFLECTION}) + +# Generates binary file containing the serialized file descriptor proto for the Serialization test +.PHONY: +generate-reflection-data: ${SERIALIZATION_GRPC_REFLECTION} + ### Testing #################################################################### # Normal test suite. diff --git a/Package.swift b/Package.swift index 16aea5d15..120f8df6a 100644 --- a/Package.swift +++ b/Package.swift @@ -205,6 +205,7 @@ extension Target { ), exclude: [ "Codegen/Normalization/normalization.proto", + "Codegen/Serialization/echo.grpc.reflection.txt", ] ) diff --git a/Sources/protoc-gen-grpc-swift/main.swift b/Sources/protoc-gen-grpc-swift/main.swift index 1901f688f..0f32bd53e 100644 --- a/Sources/protoc-gen-grpc-swift/main.swift +++ b/Sources/protoc-gen-grpc-swift/main.swift @@ -64,9 +64,10 @@ enum FileNaming: String { func outputFileName( component: String, fileDescriptor: FileDescriptor, - fileNamingOption: FileNaming + fileNamingOption: FileNaming, + extension: String ) -> String { - let ext = "." + component + ".swift" + let ext = "." + component + "." + `extension` let pathParts = splitPath(pathname: fileDescriptor.name) switch fileNamingOption { case .FullPath: @@ -84,19 +85,22 @@ func uniqueOutputFileName( component: String, fileDescriptor: FileDescriptor, fileNamingOption: FileNaming, - generatedFiles: inout [String: Int] + generatedFiles: inout [String: Int], + extension: String = "swift" ) -> String { let defaultName = outputFileName( component: component, fileDescriptor: fileDescriptor, - fileNamingOption: fileNamingOption + fileNamingOption: fileNamingOption, + extension: `extension` ) if let count = generatedFiles[defaultName] { generatedFiles[defaultName] = count + 1 return outputFileName( component: "\(count)." + component, fileDescriptor: fileDescriptor, - fileNamingOption: fileNamingOption + fileNamingOption: fileNamingOption, + extension: `extension` ) } else { generatedFiles[defaultName] = 1 @@ -136,19 +140,38 @@ func main(args: [String]) throws { // Only generate output for services. for name in request.fileToGenerate { - if let fileDescriptor = descriptorSet.fileDescriptor(named: name), - !fileDescriptor.services.isEmpty { - let grpcFileName = uniqueOutputFileName( - component: "grpc", - fileDescriptor: fileDescriptor, - fileNamingOption: options.fileNaming, - generatedFiles: &generatedFiles - ) - let grpcGenerator = Generator(fileDescriptor, options: options) - var grpcFile = Google_Protobuf_Compiler_CodeGeneratorResponse.File() - grpcFile.name = grpcFileName - grpcFile.content = grpcGenerator.code - response.file.append(grpcFile) + if let fileDescriptor = descriptorSet.fileDescriptor(named: name) { + if (options.generateReflectionData) { + var binaryFile = Google_Protobuf_Compiler_CodeGeneratorResponse.File() + let binaryFileName = uniqueOutputFileName( + component: "grpc.reflection", + fileDescriptor: fileDescriptor, + fileNamingOption: options.fileNaming, + generatedFiles: &generatedFiles, + extension: "txt" + ) + let serializedFileDescriptorProto = try fileDescriptor.proto.serializedData() + .base64EncodedString() + binaryFile.name = binaryFileName + binaryFile.content = serializedFileDescriptorProto + response.file.append(binaryFile) + } + if ( + !fileDescriptor.services + .isEmpty && (options.generateClient || options.generateServer) + ) { + var grpcFile = Google_Protobuf_Compiler_CodeGeneratorResponse.File() + let grpcFileName = uniqueOutputFileName( + component: "grpc", + fileDescriptor: fileDescriptor, + fileNamingOption: options.fileNaming, + generatedFiles: &generatedFiles + ) + let grpcGenerator = Generator(fileDescriptor, options: options) + 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 15d141def..bdef17338 100644 --- a/Sources/protoc-gen-grpc-swift/options.swift +++ b/Sources/protoc-gen-grpc-swift/options.swift @@ -64,6 +64,7 @@ final class GeneratorOptions { private(set) var extraModuleImports: [String] = [] private(set) var gRPCModuleName = "GRPC" private(set) var swiftProtobufModuleName = "SwiftProtobuf" + private(set) var generateReflectionData = false init(parameter: String?) throws { for pair in GeneratorOptions.parseParameter(string: parameter) { @@ -143,6 +144,13 @@ final class GeneratorOptions { throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value) } + case "ReflectionData": + if let value = Bool(pair.value) { + self.generateReflectionData = value + } else { + throw GenerationError.invalidParameterValue(name: pair.key, value: pair.value) + } + default: throw GenerationError.unknownParameter(name: pair.key) } diff --git a/Tests/GRPCTests/Codegen/Serialization/SerializationTests.swift b/Tests/GRPCTests/Codegen/Serialization/SerializationTests.swift new file mode 100644 index 000000000..f8f9174e9 --- /dev/null +++ b/Tests/GRPCTests/Codegen/Serialization/SerializationTests.swift @@ -0,0 +1,85 @@ +/* + * Copyright 2023, 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 GRPC +import SwiftProtobuf +import XCTest + +final class SerializationTests: GRPCTestCase { + var fileDescriptorProto: Google_Protobuf_FileDescriptorProto! + + override func setUp() { + super.setUp() + let binaryFileURL = URL(fileURLWithPath: #filePath) + .deletingLastPathComponent().appendingPathComponent("echo.grpc.reflection.txt") + let base64EncodedData = try! Data(contentsOf: binaryFileURL) + let binaryData = Data(base64Encoded: base64EncodedData)! + self + .fileDescriptorProto = + try! Google_Protobuf_FileDescriptorProto(serializedData: binaryData) + } + + func testFileDescriptorMetadata() throws { + let name = self.fileDescriptorProto.name + XCTAssertEqual(name, "echo.proto") + + let syntax = self.fileDescriptorProto.syntax + XCTAssertEqual(syntax, "proto3") + + let package = self.fileDescriptorProto.package + XCTAssertEqual(package, "echo") + } + + func testFileDescriptorMessages() { + let messages = self.fileDescriptorProto.messageType + XCTAssertEqual(messages.count, 2) + for message in messages { + XCTAssert((message.name == "EchoRequest") || (message.name == "EchoResponse")) + XCTAssertEqual(message.field.count, 1) + XCTAssertEqual(message.field.first!.name, "text") + XCTAssert(message.field.first!.hasNumber) + } + } + + func testFileDescriptorServices() { + let services = self.fileDescriptorProto.service + XCTAssertEqual(services.count, 1) + XCTAssertEqual(self.fileDescriptorProto.service.first!.method.count, 4) + for method in self.fileDescriptorProto.service.first!.method { + switch method.name { + case "Get": + XCTAssertEqual(method.inputType, ".echo.EchoRequest") + XCTAssertEqual(method.outputType, ".echo.EchoResponse") + case "Expand": + XCTAssertEqual(method.inputType, ".echo.EchoRequest") + XCTAssertEqual(method.outputType, ".echo.EchoResponse") + XCTAssert(method.serverStreaming) + case "Collect": + XCTAssertEqual(method.inputType, ".echo.EchoRequest") + XCTAssertEqual(method.outputType, ".echo.EchoResponse") + XCTAssert(method.clientStreaming) + case "Update": + XCTAssertEqual(method.inputType, ".echo.EchoRequest") + XCTAssertEqual(method.outputType, ".echo.EchoResponse") + XCTAssert(method.clientStreaming) + XCTAssert(method.serverStreaming) + default: + XCTFail("The method name is incorrect.") + } + } + } +} diff --git a/Tests/GRPCTests/Codegen/Serialization/echo.grpc.reflection.txt b/Tests/GRPCTests/Codegen/Serialization/echo.grpc.reflection.txt new file mode 100644 index 000000000..af26ef4a7 --- /dev/null +++ b/Tests/GRPCTests/Codegen/Serialization/echo.grpc.reflection.txt @@ -0,0 +1 @@ +CgplY2hvLnByb3RvEgRlY2hvIiEKC0VjaG9SZXF1ZXN0EhIKBHRleHQYASABKAlSBHRleHQiIgoMRWNob1Jlc3BvbnNlEhIKBHRleHQYASABKAlSBHRleHQy2AEKBEVjaG8SLgoDR2V0EhEuZWNoby5FY2hvUmVxdWVzdBoSLmVjaG8uRWNob1Jlc3BvbnNlIgASMwoGRXhwYW5kEhEuZWNoby5FY2hvUmVxdWVzdBoSLmVjaG8uRWNob1Jlc3BvbnNlIgAwARI0CgdDb2xsZWN0EhEuZWNoby5FY2hvUmVxdWVzdBoSLmVjaG8uRWNob1Jlc3BvbnNlIgAoARI1CgZVcGRhdGUSES5lY2hvLkVjaG9SZXF1ZXN0GhIuZWNoby5FY2hvUmVzcG9uc2UiACgBMAFK/QoKBhIEDgAoAQrCBAoBDBIDDgASMrcEIENvcHlyaWdodCAoYykgMjAxNSwgR29vZ2xlIEluYy4KCiBMaWNlbnNlZCB1bmRlciB0aGUgQXBhY2hlIExpY2Vuc2UsIFZlcnNpb24gMi4wICh0aGUgIkxpY2Vuc2UiKTsKIHlvdSBtYXkgbm90IHVzZSB0aGlzIGZpbGUgZXhjZXB0IGluIGNvbXBsaWFuY2Ugd2l0aCB0aGUgTGljZW5zZS4KIFlvdSBtYXkgb2J0YWluIGEgY29weSBvZiB0aGUgTGljZW5zZSBhdAoKICAgICBodHRwOi8vd3d3LmFwYWNoZS5vcmcvbGljZW5zZXMvTElDRU5TRS0yLjAKCiBVbmxlc3MgcmVxdWlyZWQgYnkgYXBwbGljYWJsZSBsYXcgb3IgYWdyZWVkIHRvIGluIHdyaXRpbmcsIHNvZnR3YXJlCiBkaXN0cmlidXRlZCB1bmRlciB0aGUgTGljZW5zZSBpcyBkaXN0cmlidXRlZCBvbiBhbiAiQVMgSVMiIEJBU0lTLAogV0lUSE9VVCBXQVJSQU5USUVTIE9SIENPTkRJVElPTlMgT0YgQU5ZIEtJTkQsIGVpdGhlciBleHByZXNzIG9yIGltcGxpZWQuCiBTZWUgdGhlIExpY2Vuc2UgZm9yIHRoZSBzcGVjaWZpYyBsYW5ndWFnZSBnb3Zlcm5pbmcgcGVybWlzc2lvbnMgYW5kCiBsaW1pdGF0aW9ucyB1bmRlciB0aGUgTGljZW5zZS4KCggKAQISAxAADQoKCgIGABIEEgAeAQoKCgMGAAESAxIIDAo4CgQGAAIAEgMUAjAaKyBJbW1lZGlhdGVseSByZXR1cm5zIGFuIGVjaG8gb2YgYSByZXF1ZXN0LgoKDAoFBgACAAESAxQGCQoMCgUGAAIAAhIDFAoVCgwKBQYAAgADEgMUICwKWQoEBgACARIDFwI6GkwgU3BsaXRzIGEgcmVxdWVzdCBpbnRvIHdvcmRzIGFuZCByZXR1cm5zIGVhY2ggd29yZCBpbiBhIHN0cmVhbSBvZiBtZXNzYWdlcy4KCgwKBQYAAgEBEgMXBgwKDAoFBgACAQISAxcNGAoMCgUGAAIBBhIDFyMpCgwKBQYAAgEDEgMXKjYKYgoEBgACAhIDGgI7GlUgQ29sbGVjdHMgYSBzdHJlYW0gb2YgbWVzc2FnZXMgYW5kIHJldHVybnMgdGhlbSBjb25jYXRlbmF0ZWQgd2hlbiB0aGUgY2FsbGVyIGNsb3Nlcy4KCgwKBQYAAgIBEgMaBg0KDAoFBgACAgUSAxoOFAoMCgUGAAICAhIDGhUgCgwKBQYAAgIDEgMaKzcKTQoEBgACAxIDHQJBGkAgU3RyZWFtcyBiYWNrIG1lc3NhZ2VzIGFzIHRoZXkgYXJlIHJlY2VpdmVkIGluIGFuIGlucHV0IHN0cmVhbS4KCgwKBQYAAgMBEgMdBgwKDAoFBgACAwUSAx0NEwoMCgUGAAIDAhIDHRQfCgwKBQYAAgMGEgMdKjAKDAoFBgACAwMSAx0xPQoKCgIEABIEIAAjAQoKCgMEAAESAyAIEwoyCgQEAAIAEgMiAhIaJSBUaGUgdGV4dCBvZiBhIG1lc3NhZ2UgdG8gYmUgZWNob2VkLgoKDAoFBAACAAUSAyICCAoMCgUEAAIAARIDIgkNCgwKBQQAAgADEgMiEBEKCgoCBAESBCUAKAEKCgoDBAEBEgMlCBQKLAoEBAECABIDJwISGh8gVGhlIHRleHQgb2YgYW4gZWNobyByZXNwb25zZS4KCgwKBQQBAgAFEgMnAggKDAoFBAECAAESAycJDQoMCgUEAQIAAxIDJxARYgZwcm90bzM= \ No newline at end of file