Skip to content

Commit cd104a0

Browse files
Implemented file-containing-extension request for Reflection Service (#1677)
Motivation: The file-containing-extension request enables users to get the file descriptor protos of the proto file containing the extension they are looking for and its transitive dependencies. Modifications: - Created a new struct (ExtensionDescriptor) to represent the type that is extended and the field number of each extension that exists inside the protos passed to the Reflection Service. - Added a <ExtensionDescriptor, FileName> dictionary inside the ReflectionServiceData registry to store the extensions, avoid duplicates and retrieve nicely the file name of the proto that contains the requested extension. The file name is then used to get the serialized file descriptor protos of the proto containing the extension and its transitive dependencies. - Added integration and unit tests. Result: The Reflection Service can now be used to get the serialized file descriptor protos of the proto containing the provided extension and its transitive dependencies.
1 parent 99e9956 commit cd104a0

File tree

4 files changed

+238
-18
lines changed

4 files changed

+238
-18
lines changed

Sources/GRPCReflectionService/Server/ReflectionService.swift

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,21 @@ internal struct ReflectionServiceData: Sendable {
4343
internal var serializedFileDescriptorProto: Data
4444
internal var dependencyFileNames: [String]
4545
}
46+
private struct ExtensionDescriptor: Sendable, Hashable {
47+
internal let extendeeTypeName: String
48+
internal let fieldNumber: Int32
49+
}
4650

4751
internal var fileDescriptorDataByFilename: [String: FileDescriptorProtoData]
4852
internal var serviceNames: [String]
4953
internal var fileNameBySymbol: [String: String]
54+
private var fileNameByExtensionDescriptor: [ExtensionDescriptor: String]
5055

5156
internal init(fileDescriptors: [Google_Protobuf_FileDescriptorProto]) throws {
5257
self.serviceNames = []
5358
self.fileDescriptorDataByFilename = [:]
5459
self.fileNameBySymbol = [:]
60+
self.fileNameByExtensionDescriptor = [:]
5561

5662
for fileDescriptorProto in fileDescriptors {
5763
let serializedFileDescriptorProto: Data
@@ -70,6 +76,8 @@ internal struct ReflectionServiceData: Sendable {
7076
)
7177
self.fileDescriptorDataByFilename[fileDescriptorProto.name] = protoData
7278
self.serviceNames.append(contentsOf: fileDescriptorProto.service.map { $0.name })
79+
80+
// Populating the <symbol, file name> dictionary.
7381
for qualifiedSybolName in fileDescriptorProto.qualifiedSymbolNames {
7482
let oldValue = self.fileNameBySymbol.updateValue(
7583
fileDescriptorProto.name,
@@ -83,6 +91,28 @@ internal struct ReflectionServiceData: Sendable {
8391
)
8492
}
8593
}
94+
95+
// Populating the <extension descriptor, file name> dictionary.
96+
for `extension` in fileDescriptorProto.extension {
97+
let extensionDescriptor = ExtensionDescriptor(
98+
extendeeTypeName: `extension`.extendee,
99+
fieldNumber: `extension`.number
100+
)
101+
let oldFileName = self.fileNameByExtensionDescriptor.updateValue(
102+
fileDescriptorProto.name,
103+
forKey: extensionDescriptor
104+
)
105+
if let oldFileName = oldFileName {
106+
throw GRPCStatus(
107+
code: .alreadyExists,
108+
message:
109+
"""
110+
The extension of the \(extensionDescriptor.extendeeTypeName) type with the field number equal to \
111+
\(extensionDescriptor.fieldNumber) from \(fileDescriptorProto.name) already exists in \(oldFileName).
112+
"""
113+
)
114+
}
115+
}
86116
}
87117
}
88118

@@ -119,6 +149,14 @@ internal struct ReflectionServiceData: Sendable {
119149
internal func nameOfFileContainingSymbol(named symbolName: String) -> String? {
120150
return self.fileNameBySymbol[symbolName]
121151
}
152+
153+
internal func nameOfFileContainingExtension(
154+
named extendeeName: String,
155+
fieldNumber number: Int32
156+
) -> String? {
157+
let key = ExtensionDescriptor(extendeeTypeName: extendeeName, fieldNumber: number)
158+
return self.fileNameByExtensionDescriptor[key]
159+
}
122160
}
123161

124162
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
@@ -172,6 +210,24 @@ internal final class ReflectionServiceProvider: Reflection_ServerReflectionAsync
172210
return try self.findFileByFileName(fileName, request: request)
173211
}
174212

213+
internal func findFileByExtension(
214+
extensionRequest: Reflection_ExtensionRequest,
215+
request: Reflection_ServerReflectionRequest
216+
) throws -> Reflection_ServerReflectionResponse {
217+
guard
218+
let fileName = self.protoRegistry.nameOfFileContainingExtension(
219+
named: extensionRequest.containingType,
220+
fieldNumber: extensionRequest.extensionNumber
221+
)
222+
else {
223+
throw GRPCStatus(
224+
code: .notFound,
225+
message: "The provided extension could not be found."
226+
)
227+
}
228+
return try self.findFileByFileName(fileName, request: request)
229+
}
230+
175231
internal func serverReflectionInfo(
176232
requestStream: GRPCAsyncRequestStream<Reflection_ServerReflectionRequest>,
177233
responseStream: GRPCAsyncResponseStreamWriter<Reflection_ServerReflectionResponse>,
@@ -197,6 +253,13 @@ internal final class ReflectionServiceProvider: Reflection_ServerReflectionAsync
197253
)
198254
try await responseStream.send(response)
199255

256+
case let .fileContainingExtension(extensionRequest):
257+
let response = try self.findFileByExtension(
258+
extensionRequest: extensionRequest,
259+
request: request
260+
)
261+
try await responseStream.send(response)
262+
200263
default:
201264
throw GRPCStatus(code: .unimplemented)
202265
}

Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceIntegrationTests.swift

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ final class ReflectionServiceIntegrationTests: GRPCTestCase {
2929
private let protos: [Google_Protobuf_FileDescriptorProto] = makeProtosWithDependencies()
3030
private let independentProto: Google_Protobuf_FileDescriptorProto = generateFileDescriptorProto(
3131
fileName: "independentBar",
32-
suffix: 5
32+
suffix: "5"
3333
)
3434

3535
private func setUpServerAndChannel() throws {
@@ -174,4 +174,66 @@ final class ReflectionServiceIntegrationTests: GRPCTestCase {
174174
}
175175
}
176176
}
177+
178+
func testFileByExtension() async throws {
179+
try self.setUpServerAndChannel()
180+
let client = Reflection_ServerReflectionAsyncClient(channel: self.channel!)
181+
let serviceReflectionInfo = client.makeServerReflectionInfoCall()
182+
183+
try await serviceReflectionInfo.requestStream.send(
184+
.with {
185+
$0.host = "127.0.0.1"
186+
$0.fileContainingExtension = .with {
187+
$0.containingType = "inputMessage1"
188+
$0.extensionNumber = 2
189+
}
190+
}
191+
)
192+
193+
serviceReflectionInfo.requestStream.finish()
194+
var iterator = serviceReflectionInfo.responseStream.makeAsyncIterator()
195+
guard let message = try await iterator.next() else {
196+
return XCTFail("Could not get a response message.")
197+
}
198+
let receivedData: [Google_Protobuf_FileDescriptorProto]
199+
do {
200+
receivedData = try message.fileDescriptorResponse.fileDescriptorProto.map {
201+
try Google_Protobuf_FileDescriptorProto(serializedData: $0)
202+
}
203+
} catch {
204+
return XCTFail("Could not serialize data received as a message.")
205+
}
206+
207+
let fileToFind = self.protos[0]
208+
let dependentProtos = self.protos[1...]
209+
var receivedProtoContainingExtension = 0
210+
var dependenciesCount = 0
211+
for fileDescriptorProto in receivedData {
212+
if fileDescriptorProto == fileToFind {
213+
receivedProtoContainingExtension += 1
214+
XCTAssert(
215+
fileDescriptorProto.extension.map { $0.name }.contains("extensionInputMessage1"),
216+
"""
217+
The response doesn't contain the serialized file descriptor proto \
218+
containing the \"extensionInputMessage1\" extension.
219+
"""
220+
)
221+
} else {
222+
dependenciesCount += 1
223+
XCTAssert(
224+
dependentProtos.contains(fileDescriptorProto),
225+
"""
226+
The \(fileDescriptorProto.name) is not a dependency of the \
227+
proto file containing the \"extensionInputMessage1\" symbol.
228+
"""
229+
)
230+
}
231+
}
232+
XCTAssertEqual(
233+
receivedProtoContainingExtension,
234+
1,
235+
"The file descriptor proto of the proto containing the extension was not received."
236+
)
237+
XCTAssertEqual(dependenciesCount, 3)
238+
}
177239
}

Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceUnitTests.swift

Lines changed: 87 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ final class ReflectionServiceUnitTests: GRPCTestCase {
6262
XCTAssertEqual(registryServices, servicesNames)
6363
}
6464

65-
/// Testing the fileNameBySymbol array of the ReflectionServiceData object.
65+
/// Testing the fileNameBySymbol dictionary of the ReflectionServiceData object.
6666
func testFileNameBySymbol() throws {
6767
let protos = makeProtosWithDependencies()
6868
let registry = try ReflectionServiceData(fileDescriptors: protos)
@@ -85,7 +85,7 @@ final class ReflectionServiceUnitTests: GRPCTestCase {
8585
var protos = makeProtosWithDependencies()
8686
protos[1].messageType.append(
8787
Google_Protobuf_DescriptorProto.with {
88-
$0.name = "inputMessage"
88+
$0.name = "inputMessage2"
8989
$0.field = [
9090
Google_Protobuf_FieldDescriptorProto.with {
9191
$0.name = "inputField"
@@ -104,7 +104,7 @@ final class ReflectionServiceUnitTests: GRPCTestCase {
104104
code: .alreadyExists,
105105
message:
106106
"""
107-
The packagebar2.inputMessage symbol from bar2.proto \
107+
The packagebar2.inputMessage2 symbol from bar2.proto \
108108
already exists in bar2.proto.
109109
"""
110110
)
@@ -124,7 +124,7 @@ final class ReflectionServiceUnitTests: GRPCTestCase {
124124
func testNameOfFileContainingSymbolMessage() throws {
125125
let protos = makeProtosWithDependencies()
126126
let registry = try ReflectionServiceData(fileDescriptors: protos)
127-
let fileName = registry.nameOfFileContainingSymbol(named: "packagebar1.inputMessage")
127+
let fileName = registry.nameOfFileContainingSymbol(named: "packagebar1.inputMessage1")
128128
XCTAssertEqual(fileName, "bar1.proto")
129129
}
130130

@@ -148,7 +148,7 @@ final class ReflectionServiceUnitTests: GRPCTestCase {
148148
let protos = makeProtosWithDependencies()
149149
let registry = try ReflectionServiceData(fileDescriptors: protos)
150150
let fileName = registry.nameOfFileContainingSymbol(named: "packagebar2.enumType3")
151-
XCTAssertEqual(fileName, nil)
151+
XCTAssertNil(fileName)
152152
}
153153

154154
// Testing the serializedFileDescriptorProto method in different cases.
@@ -329,4 +329,86 @@ final class ReflectionServiceUnitTests: GRPCTestCase {
329329
)
330330
}
331331
}
332+
333+
// Testing the nameOfFileContainingExtension() method.
334+
335+
func testNameOfFileContainingExtensions() throws {
336+
let protos = makeProtosWithDependencies()
337+
let registry = try ReflectionServiceData(fileDescriptors: protos)
338+
for proto in protos {
339+
for `extension` in proto.extension {
340+
let registryFileName = registry.nameOfFileContainingExtension(
341+
named: `extension`.extendee,
342+
fieldNumber: `extension`.number
343+
)
344+
XCTAssertEqual(registryFileName, proto.name)
345+
}
346+
}
347+
}
348+
349+
func testNameOfFileContainingExtensionsSameTypeExtensionsDifferentNumbers() throws {
350+
var protos = makeProtosWithDependencies()
351+
protos[0].extension.append(
352+
.with {
353+
$0.extendee = "inputMessage1"
354+
$0.number = 3
355+
}
356+
)
357+
let registry = try ReflectionServiceData(fileDescriptors: protos)
358+
359+
for proto in protos {
360+
for `extension` in proto.extension {
361+
let registryFileName = registry.nameOfFileContainingExtension(
362+
named: `extension`.extendee,
363+
fieldNumber: `extension`.number
364+
)
365+
XCTAssertEqual(registryFileName, proto.name)
366+
}
367+
}
368+
}
369+
370+
func testNameOfFileContainingExtensionsInvalidTypeName() throws {
371+
let protos = makeProtosWithDependencies()
372+
let registry = try ReflectionServiceData(fileDescriptors: protos)
373+
let registryFileName = registry.nameOfFileContainingExtension(
374+
named: "InvalidType",
375+
fieldNumber: 2
376+
)
377+
XCTAssertNil(registryFileName)
378+
}
379+
380+
func testNameOfFileContainingExtensionsInvalidFieldNumber() throws {
381+
let protos = makeProtosWithDependencies()
382+
let registry = try ReflectionServiceData(fileDescriptors: protos)
383+
let registryFileName = registry.nameOfFileContainingExtension(
384+
named: protos[0].extension[0].extendee,
385+
fieldNumber: 4
386+
)
387+
XCTAssertNil(registryFileName)
388+
}
389+
390+
func testNameOfFileContainingExtensionsDuplicatedExtensions() throws {
391+
var protos = makeProtosWithDependencies()
392+
protos[0].extension.append(
393+
.with {
394+
$0.extendee = "inputMessage1"
395+
$0.number = 2
396+
}
397+
)
398+
XCTAssertThrowsError(
399+
try ReflectionServiceData(fileDescriptors: protos)
400+
) { error in
401+
XCTAssertEqual(
402+
error as? GRPCStatus,
403+
GRPCStatus(
404+
code: .alreadyExists,
405+
message:
406+
"""
407+
The extension of the inputMessage1 type with the field number equal to \
408+
2 from \(protos[0].name) already exists in \(protos[0].name).
409+
"""
410+
)
411+
)
412+
}
413+
}
332414
}

0 commit comments

Comments
 (0)