diff --git a/Sources/GRPCReflectionService/Server/ReflectionService.swift b/Sources/GRPCReflectionService/Server/ReflectionService.swift index 81cf8282c..fe388a0ec 100644 --- a/Sources/GRPCReflectionService/Server/ReflectionService.swift +++ b/Sources/GRPCReflectionService/Server/ReflectionService.swift @@ -43,15 +43,21 @@ internal struct ReflectionServiceData: Sendable { internal var serializedFileDescriptorProto: Data internal var dependencyFileNames: [String] } + private struct ExtensionDescriptor: Sendable, Hashable { + internal let extendeeTypeName: String + internal let fieldNumber: Int32 + } internal var fileDescriptorDataByFilename: [String: FileDescriptorProtoData] internal var serviceNames: [String] internal var fileNameBySymbol: [String: String] + private var fileNameByExtensionDescriptor: [ExtensionDescriptor: String] internal init(fileDescriptors: [Google_Protobuf_FileDescriptorProto]) throws { self.serviceNames = [] self.fileDescriptorDataByFilename = [:] self.fileNameBySymbol = [:] + self.fileNameByExtensionDescriptor = [:] for fileDescriptorProto in fileDescriptors { let serializedFileDescriptorProto: Data @@ -70,6 +76,8 @@ internal struct ReflectionServiceData: Sendable { ) self.fileDescriptorDataByFilename[fileDescriptorProto.name] = protoData self.serviceNames.append(contentsOf: fileDescriptorProto.service.map { $0.name }) + + // Populating the dictionary. for qualifiedSybolName in fileDescriptorProto.qualifiedSymbolNames { let oldValue = self.fileNameBySymbol.updateValue( fileDescriptorProto.name, @@ -83,6 +91,28 @@ internal struct ReflectionServiceData: Sendable { ) } } + + // Populating the dictionary. + for `extension` in fileDescriptorProto.extension { + let extensionDescriptor = ExtensionDescriptor( + extendeeTypeName: `extension`.extendee, + fieldNumber: `extension`.number + ) + let oldFileName = self.fileNameByExtensionDescriptor.updateValue( + fileDescriptorProto.name, + forKey: extensionDescriptor + ) + if let oldFileName = oldFileName { + throw GRPCStatus( + code: .alreadyExists, + message: + """ + The extension of the \(extensionDescriptor.extendeeTypeName) type with the field number equal to \ + \(extensionDescriptor.fieldNumber) from \(fileDescriptorProto.name) already exists in \(oldFileName). + """ + ) + } + } } } @@ -119,6 +149,14 @@ internal struct ReflectionServiceData: Sendable { internal func nameOfFileContainingSymbol(named symbolName: String) -> String? { return self.fileNameBySymbol[symbolName] } + + internal func nameOfFileContainingExtension( + named extendeeName: String, + fieldNumber number: Int32 + ) -> String? { + let key = ExtensionDescriptor(extendeeTypeName: extendeeName, fieldNumber: number) + return self.fileNameByExtensionDescriptor[key] + } } @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) @@ -172,6 +210,24 @@ internal final class ReflectionServiceProvider: Reflection_ServerReflectionAsync return try self.findFileByFileName(fileName, request: request) } + internal func findFileByExtension( + extensionRequest: Reflection_ExtensionRequest, + request: Reflection_ServerReflectionRequest + ) throws -> Reflection_ServerReflectionResponse { + guard + let fileName = self.protoRegistry.nameOfFileContainingExtension( + named: extensionRequest.containingType, + fieldNumber: extensionRequest.extensionNumber + ) + else { + throw GRPCStatus( + code: .notFound, + message: "The provided extension could not be found." + ) + } + return try self.findFileByFileName(fileName, request: request) + } + internal func serverReflectionInfo( requestStream: GRPCAsyncRequestStream, responseStream: GRPCAsyncResponseStreamWriter, @@ -197,6 +253,13 @@ internal final class ReflectionServiceProvider: Reflection_ServerReflectionAsync ) try await responseStream.send(response) + case let .fileContainingExtension(extensionRequest): + let response = try self.findFileByExtension( + extensionRequest: extensionRequest, + request: request + ) + try await responseStream.send(response) + default: throw GRPCStatus(code: .unimplemented) } diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceIntegrationTests.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceIntegrationTests.swift index c6bd1a339..651ac0c3c 100644 --- a/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceIntegrationTests.swift +++ b/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceIntegrationTests.swift @@ -29,7 +29,7 @@ final class ReflectionServiceIntegrationTests: GRPCTestCase { private let protos: [Google_Protobuf_FileDescriptorProto] = makeProtosWithDependencies() private let independentProto: Google_Protobuf_FileDescriptorProto = generateFileDescriptorProto( fileName: "independentBar", - suffix: 5 + suffix: "5" ) private func setUpServerAndChannel() throws { @@ -174,4 +174,66 @@ final class ReflectionServiceIntegrationTests: GRPCTestCase { } } } + + func testFileByExtension() async throws { + try self.setUpServerAndChannel() + let client = Reflection_ServerReflectionAsyncClient(channel: self.channel!) + let serviceReflectionInfo = client.makeServerReflectionInfoCall() + + try await serviceReflectionInfo.requestStream.send( + .with { + $0.host = "127.0.0.1" + $0.fileContainingExtension = .with { + $0.containingType = "inputMessage1" + $0.extensionNumber = 2 + } + } + ) + + serviceReflectionInfo.requestStream.finish() + var iterator = serviceReflectionInfo.responseStream.makeAsyncIterator() + guard let message = try await iterator.next() else { + return XCTFail("Could not get a response message.") + } + let receivedData: [Google_Protobuf_FileDescriptorProto] + do { + receivedData = try message.fileDescriptorResponse.fileDescriptorProto.map { + try Google_Protobuf_FileDescriptorProto(serializedData: $0) + } + } catch { + return XCTFail("Could not serialize data received as a message.") + } + + let fileToFind = self.protos[0] + let dependentProtos = self.protos[1...] + var receivedProtoContainingExtension = 0 + var dependenciesCount = 0 + for fileDescriptorProto in receivedData { + if fileDescriptorProto == fileToFind { + receivedProtoContainingExtension += 1 + XCTAssert( + fileDescriptorProto.extension.map { $0.name }.contains("extensionInputMessage1"), + """ + The response doesn't contain the serialized file descriptor proto \ + containing the \"extensionInputMessage1\" extension. + """ + ) + } else { + dependenciesCount += 1 + XCTAssert( + dependentProtos.contains(fileDescriptorProto), + """ + The \(fileDescriptorProto.name) is not a dependency of the \ + proto file containing the \"extensionInputMessage1\" symbol. + """ + ) + } + } + XCTAssertEqual( + receivedProtoContainingExtension, + 1, + "The file descriptor proto of the proto containing the extension was not received." + ) + XCTAssertEqual(dependenciesCount, 3) + } } diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceUnitTests.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceUnitTests.swift index e3913c1d0..7b7f60b14 100644 --- a/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceUnitTests.swift +++ b/Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceUnitTests.swift @@ -62,7 +62,7 @@ final class ReflectionServiceUnitTests: GRPCTestCase { XCTAssertEqual(registryServices, servicesNames) } - /// Testing the fileNameBySymbol array of the ReflectionServiceData object. + /// Testing the fileNameBySymbol dictionary of the ReflectionServiceData object. func testFileNameBySymbol() throws { let protos = makeProtosWithDependencies() let registry = try ReflectionServiceData(fileDescriptors: protos) @@ -85,7 +85,7 @@ final class ReflectionServiceUnitTests: GRPCTestCase { var protos = makeProtosWithDependencies() protos[1].messageType.append( Google_Protobuf_DescriptorProto.with { - $0.name = "inputMessage" + $0.name = "inputMessage2" $0.field = [ Google_Protobuf_FieldDescriptorProto.with { $0.name = "inputField" @@ -104,7 +104,7 @@ final class ReflectionServiceUnitTests: GRPCTestCase { code: .alreadyExists, message: """ - The packagebar2.inputMessage symbol from bar2.proto \ + The packagebar2.inputMessage2 symbol from bar2.proto \ already exists in bar2.proto. """ ) @@ -124,7 +124,7 @@ final class ReflectionServiceUnitTests: GRPCTestCase { func testNameOfFileContainingSymbolMessage() throws { let protos = makeProtosWithDependencies() let registry = try ReflectionServiceData(fileDescriptors: protos) - let fileName = registry.nameOfFileContainingSymbol(named: "packagebar1.inputMessage") + let fileName = registry.nameOfFileContainingSymbol(named: "packagebar1.inputMessage1") XCTAssertEqual(fileName, "bar1.proto") } @@ -148,7 +148,7 @@ final class ReflectionServiceUnitTests: GRPCTestCase { let protos = makeProtosWithDependencies() let registry = try ReflectionServiceData(fileDescriptors: protos) let fileName = registry.nameOfFileContainingSymbol(named: "packagebar2.enumType3") - XCTAssertEqual(fileName, nil) + XCTAssertNil(fileName) } // Testing the serializedFileDescriptorProto method in different cases. @@ -329,4 +329,86 @@ final class ReflectionServiceUnitTests: GRPCTestCase { ) } } + + // Testing the nameOfFileContainingExtension() method. + + func testNameOfFileContainingExtensions() throws { + let protos = makeProtosWithDependencies() + let registry = try ReflectionServiceData(fileDescriptors: protos) + for proto in protos { + for `extension` in proto.extension { + let registryFileName = registry.nameOfFileContainingExtension( + named: `extension`.extendee, + fieldNumber: `extension`.number + ) + XCTAssertEqual(registryFileName, proto.name) + } + } + } + + func testNameOfFileContainingExtensionsSameTypeExtensionsDifferentNumbers() throws { + var protos = makeProtosWithDependencies() + protos[0].extension.append( + .with { + $0.extendee = "inputMessage1" + $0.number = 3 + } + ) + let registry = try ReflectionServiceData(fileDescriptors: protos) + + for proto in protos { + for `extension` in proto.extension { + let registryFileName = registry.nameOfFileContainingExtension( + named: `extension`.extendee, + fieldNumber: `extension`.number + ) + XCTAssertEqual(registryFileName, proto.name) + } + } + } + + func testNameOfFileContainingExtensionsInvalidTypeName() throws { + let protos = makeProtosWithDependencies() + let registry = try ReflectionServiceData(fileDescriptors: protos) + let registryFileName = registry.nameOfFileContainingExtension( + named: "InvalidType", + fieldNumber: 2 + ) + XCTAssertNil(registryFileName) + } + + func testNameOfFileContainingExtensionsInvalidFieldNumber() throws { + let protos = makeProtosWithDependencies() + let registry = try ReflectionServiceData(fileDescriptors: protos) + let registryFileName = registry.nameOfFileContainingExtension( + named: protos[0].extension[0].extendee, + fieldNumber: 4 + ) + XCTAssertNil(registryFileName) + } + + func testNameOfFileContainingExtensionsDuplicatedExtensions() throws { + var protos = makeProtosWithDependencies() + protos[0].extension.append( + .with { + $0.extendee = "inputMessage1" + $0.number = 2 + } + ) + XCTAssertThrowsError( + try ReflectionServiceData(fileDescriptors: protos) + ) { error in + XCTAssertEqual( + error as? GRPCStatus, + GRPCStatus( + code: .alreadyExists, + message: + """ + The extension of the inputMessage1 type with the field number equal to \ + 2 from \(protos[0].name) already exists in \(protos[0].name). + """ + ) + ) + } + } } diff --git a/Tests/GRPCTests/GRPCReflectionServiceTests/Utils.swift b/Tests/GRPCTests/GRPCReflectionServiceTests/Utils.swift index 95f68f131..faea453cb 100644 --- a/Tests/GRPCTests/GRPCReflectionServiceTests/Utils.swift +++ b/Tests/GRPCTests/GRPCReflectionServiceTests/Utils.swift @@ -20,10 +20,10 @@ import SwiftProtobuf internal func generateFileDescriptorProto( fileName name: String, - suffix id: Int + suffix: String ) -> Google_Protobuf_FileDescriptorProto { let inputMessage = Google_Protobuf_DescriptorProto.with { - $0.name = "inputMessage" + $0.name = "inputMessage" + suffix $0.field = [ Google_Protobuf_FieldDescriptorProto.with { $0.name = "inputField" @@ -32,8 +32,14 @@ internal func generateFileDescriptorProto( ] } + let inputMessageExtension = Google_Protobuf_FieldDescriptorProto.with { + $0.name = "extensionInputMessage" + suffix + $0.extendee = "inputMessage" + suffix + $0.number = 2 + } + let outputMessage = Google_Protobuf_DescriptorProto.with { - $0.name = "outputMessage" + $0.name = "outputMessage" + suffix $0.field = [ Google_Protobuf_FieldDescriptorProto.with { $0.name = "outputField" @@ -43,7 +49,7 @@ internal func generateFileDescriptorProto( } let enumType = Google_Protobuf_EnumDescriptorProto.with { - $0.name = "enumType" + String(id) + $0.name = "enumType" + suffix $0.value = [ Google_Protobuf_EnumValueDescriptorProto.with { $0.name = "value1" @@ -55,22 +61,23 @@ internal func generateFileDescriptorProto( } let method = Google_Protobuf_MethodDescriptorProto.with { - $0.name = "testMethod" + String(id) + $0.name = "testMethod" + suffix $0.inputType = inputMessage.name $0.outputType = outputMessage.name } let serviceDescriptor = Google_Protobuf_ServiceDescriptorProto.with { $0.method = [method] - $0.name = "service" + String(id) + $0.name = "service" + suffix } let fileDescriptorProto = Google_Protobuf_FileDescriptorProto.with { $0.service = [serviceDescriptor] - $0.name = name + String(id) + ".proto" - $0.package = "package" + name + String(id) + $0.name = name + suffix + ".proto" + $0.package = "package" + name + suffix $0.messageType = [inputMessage, outputMessage] $0.enumType = [enumType] + $0.extension = [inputMessageExtension] } return fileDescriptorProto @@ -80,7 +87,7 @@ internal func generateFileDescriptorProto( internal func makeProtosWithDependencies() -> [Google_Protobuf_FileDescriptorProto] { var fileDependencies: [Google_Protobuf_FileDescriptorProto] = [] for id in 1 ... 4 { - let fileDescriptorProto = generateFileDescriptorProto(fileName: "bar", suffix: id) + let fileDescriptorProto = generateFileDescriptorProto(fileName: "bar", suffix: String(id)) if id != 1 { // Dependency of the first dependency. fileDependencies[0].dependency.append(fileDescriptorProto.name) @@ -92,10 +99,16 @@ internal func makeProtosWithDependencies() -> [Google_Protobuf_FileDescriptorPro internal func makeProtosWithComplexDependencies() -> [Google_Protobuf_FileDescriptorProto] { var protos: [Google_Protobuf_FileDescriptorProto] = [] - protos.append(generateFileDescriptorProto(fileName: "foo", suffix: 0)) + protos.append(generateFileDescriptorProto(fileName: "foo", suffix: "0")) for id in 1 ... 10 { - let fileDescriptorProtoA = generateFileDescriptorProto(fileName: "fooA", suffix: id) - let fileDescriptorProtoB = generateFileDescriptorProto(fileName: "fooB", suffix: id) + let fileDescriptorProtoA = generateFileDescriptorProto( + fileName: "fooA", + suffix: String(id) + "A" + ) + let fileDescriptorProtoB = generateFileDescriptorProto( + fileName: "fooB", + suffix: String(id) + "B" + ) let parent = protos.count > 1 ? protos.count - Int.random(in: 1 ..< 3) : protos.count - 1 protos[parent].dependency.append(fileDescriptorProtoA.name) protos[parent].dependency.append(fileDescriptorProtoB.name)