Skip to content

Commit ef5a0fe

Browse files
Support all_extension_numbers_of_type reflection requests (#1680)
Motivation: The reflection service should provide the possibility for users to request the list with all the field numbers of the extensions of a type. Modifications: - Implemented the dictionary that stores the arrays of integers representing the field numbers of the extensions for each type that has extensions. - Implemented the methods of the Reflection Service that create the specific response with the array of integers representing the field numbers or an empty array for the case that the type doesn't have any extensions (but is a valid type). - Implemented integration and Unit tests. Result: The Reflection Service will enable users to find all the extension field numbers for a specific type they requested.
1 parent e97206c commit ef5a0fe

File tree

4 files changed

+186
-41
lines changed

4 files changed

+186
-41
lines changed

Sources/GRPCReflectionService/Server/ReflectionService.swift

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,18 @@ internal struct ReflectionServiceData: Sendable {
5151
internal var fileDescriptorDataByFilename: [String: FileDescriptorProtoData]
5252
internal var serviceNames: [String]
5353
internal var fileNameBySymbol: [String: String]
54+
55+
// Stores the file names for each extension identified by an ExtensionDescriptor object.
5456
private var fileNameByExtensionDescriptor: [ExtensionDescriptor: String]
57+
// Stores the field numbers for each type that has extensions.
58+
private var fieldNumbersByType: [String: [Int32]]
5559

5660
internal init(fileDescriptors: [Google_Protobuf_FileDescriptorProto]) throws {
5761
self.serviceNames = []
5862
self.fileDescriptorDataByFilename = [:]
5963
self.fileNameBySymbol = [:]
6064
self.fileNameByExtensionDescriptor = [:]
65+
self.fieldNumbersByType = [:]
6166

6267
for fileDescriptorProto in fileDescriptors {
6368
let serializedFileDescriptorProto: Data
@@ -92,10 +97,15 @@ internal struct ReflectionServiceData: Sendable {
9297
}
9398
}
9499

95-
// Populating the <extension descriptor, file name> dictionary.
100+
for typeName in fileDescriptorProto.qualifiedMessageTypes {
101+
self.fieldNumbersByType[typeName] = []
102+
}
103+
104+
// Populating the <extension descriptor, file name> dictionary and the <typeName, [FieldNumber]> one.
96105
for `extension` in fileDescriptorProto.extension {
106+
let typeName = String(`extension`.extendee.drop(while: { $0 == "." }))
97107
let extensionDescriptor = ExtensionDescriptor(
98-
extendeeTypeName: `extension`.extendee,
108+
extendeeTypeName: typeName,
99109
fieldNumber: `extension`.number
100110
)
101111
let oldFileName = self.fileNameByExtensionDescriptor.updateValue(
@@ -112,6 +122,7 @@ internal struct ReflectionServiceData: Sendable {
112122
"""
113123
)
114124
}
125+
self.fieldNumbersByType[typeName, default: []].append(`extension`.number)
115126
}
116127
}
117128
}
@@ -151,12 +162,23 @@ internal struct ReflectionServiceData: Sendable {
151162
}
152163

153164
internal func nameOfFileContainingExtension(
154-
named extendeeName: String,
165+
extendeeName: String,
155166
fieldNumber number: Int32
156167
) -> String? {
157168
let key = ExtensionDescriptor(extendeeTypeName: extendeeName, fieldNumber: number)
158169
return self.fileNameByExtensionDescriptor[key]
159170
}
171+
172+
// Returns an empty array if the type has no extensions.
173+
internal func extensionsFieldNumbersOfType(named typeName: String) throws -> [Int32] {
174+
guard let fieldNumbers = self.fieldNumbersByType[typeName] else {
175+
throw GRPCStatus(
176+
code: .invalidArgument,
177+
message: "The provided type is invalid."
178+
)
179+
}
180+
return fieldNumbers
181+
}
160182
}
161183

162184
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
@@ -216,7 +238,7 @@ internal final class ReflectionServiceProvider: Reflection_ServerReflectionAsync
216238
) throws -> Reflection_ServerReflectionResponse {
217239
guard
218240
let fileName = self.protoRegistry.nameOfFileContainingExtension(
219-
named: extensionRequest.containingType,
241+
extendeeName: extensionRequest.containingType,
220242
fieldNumber: extensionRequest.extensionNumber
221243
)
222244
else {
@@ -228,6 +250,20 @@ internal final class ReflectionServiceProvider: Reflection_ServerReflectionAsync
228250
return try self.findFileByFileName(fileName, request: request)
229251
}
230252

253+
internal func findExtensionsFieldNumbersOfType(
254+
named typeName: String,
255+
request: Reflection_ServerReflectionRequest
256+
) throws -> Reflection_ServerReflectionResponse {
257+
let fieldNumbers = try self.protoRegistry.extensionsFieldNumbersOfType(named: typeName)
258+
return Reflection_ServerReflectionResponse(
259+
request: request,
260+
extensionNumberResponse: .with {
261+
$0.baseTypeName = typeName
262+
$0.extensionNumber = fieldNumbers
263+
}
264+
)
265+
}
266+
231267
internal func serverReflectionInfo(
232268
requestStream: GRPCAsyncRequestStream<Reflection_ServerReflectionRequest>,
233269
responseStream: GRPCAsyncResponseStreamWriter<Reflection_ServerReflectionResponse>,
@@ -260,6 +296,13 @@ internal final class ReflectionServiceProvider: Reflection_ServerReflectionAsync
260296
)
261297
try await responseStream.send(response)
262298

299+
case let .allExtensionNumbersOfType(typeName):
300+
let response = try self.findExtensionsFieldNumbersOfType(
301+
named: typeName,
302+
request: request
303+
)
304+
try await responseStream.send(response)
305+
263306
default:
264307
throw GRPCStatus(code: .unimplemented)
265308
}
@@ -289,6 +332,17 @@ extension Reflection_ServerReflectionResponse {
289332
$0.listServicesResponse = listServicesResponse
290333
}
291334
}
335+
336+
init(
337+
request: Reflection_ServerReflectionRequest,
338+
extensionNumberResponse: Reflection_ExtensionNumberResponse
339+
) {
340+
self = .with {
341+
$0.validHost = request.host
342+
$0.originalRequest = request
343+
$0.allExtensionNumbersResponse = extensionNumberResponse
344+
}
345+
}
292346
}
293347

294348
extension Google_Protobuf_FileDescriptorProto {

Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceIntegrationTests.swift

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ final class ReflectionServiceIntegrationTests: GRPCTestCase {
184184
.with {
185185
$0.host = "127.0.0.1"
186186
$0.fileContainingExtension = .with {
187-
$0.containingType = "inputMessage1"
187+
$0.containingType = "packagebar1.inputMessage1"
188188
$0.extensionNumber = 2
189189
}
190190
}
@@ -212,10 +212,12 @@ final class ReflectionServiceIntegrationTests: GRPCTestCase {
212212
if fileDescriptorProto == fileToFind {
213213
receivedProtoContainingExtension += 1
214214
XCTAssert(
215-
fileDescriptorProto.extension.map { $0.name }.contains("extensionInputMessage1"),
215+
fileDescriptorProto.extension.map { $0.name }.contains(
216+
"extension.packagebar1.inputMessage1-2"
217+
),
216218
"""
217219
The response doesn't contain the serialized file descriptor proto \
218-
containing the \"extensionInputMessage1\" extension.
220+
containing the \"extensioninputMessage1-2\" extension.
219221
"""
220222
)
221223
} else {
@@ -224,7 +226,7 @@ final class ReflectionServiceIntegrationTests: GRPCTestCase {
224226
dependentProtos.contains(fileDescriptorProto),
225227
"""
226228
The \(fileDescriptorProto.name) is not a dependency of the \
227-
proto file containing the \"extensionInputMessage1\" symbol.
229+
proto file containing the \"extensioninputMessage1-2\" extension.
228230
"""
229231
)
230232
}
@@ -236,4 +238,25 @@ final class ReflectionServiceIntegrationTests: GRPCTestCase {
236238
)
237239
XCTAssertEqual(dependenciesCount, 3)
238240
}
241+
242+
func testAllExtensionNumbersOfType() async throws {
243+
try self.setUpServerAndChannel()
244+
let client = Reflection_ServerReflectionAsyncClient(channel: self.channel!)
245+
let serviceReflectionInfo = client.makeServerReflectionInfoCall()
246+
247+
try await serviceReflectionInfo.requestStream.send(
248+
.with {
249+
$0.host = "127.0.0.1"
250+
$0.allExtensionNumbersOfType = "packagebar2.inputMessage2"
251+
}
252+
)
253+
254+
serviceReflectionInfo.requestStream.finish()
255+
var iterator = serviceReflectionInfo.responseStream.makeAsyncIterator()
256+
guard let message = try await iterator.next() else {
257+
return XCTFail("Could not get a response message.")
258+
}
259+
XCTAssertEqual(message.allExtensionNumbersResponse.baseTypeName, "packagebar2.inputMessage2")
260+
XCTAssertEqual(message.allExtensionNumbersResponse.extensionNumber, [1, 2, 3, 4, 5])
261+
}
239262
}

Tests/GRPCTests/GRPCReflectionServiceTests/ReflectionServiceUnitTests.swift

Lines changed: 77 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -337,29 +337,9 @@ final class ReflectionServiceUnitTests: GRPCTestCase {
337337
let registry = try ReflectionServiceData(fileDescriptors: protos)
338338
for proto in protos {
339339
for `extension` in proto.extension {
340+
let typeName = String(`extension`.extendee.drop(while: { $0 == "." }))
340341
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,
342+
extendeeName: typeName,
363343
fieldNumber: `extension`.number
364344
)
365345
XCTAssertEqual(registryFileName, proto.name)
@@ -371,7 +351,7 @@ final class ReflectionServiceUnitTests: GRPCTestCase {
371351
let protos = makeProtosWithDependencies()
372352
let registry = try ReflectionServiceData(fileDescriptors: protos)
373353
let registryFileName = registry.nameOfFileContainingExtension(
374-
named: "InvalidType",
354+
extendeeName: "InvalidType",
375355
fieldNumber: 2
376356
)
377357
XCTAssertNil(registryFileName)
@@ -381,8 +361,8 @@ final class ReflectionServiceUnitTests: GRPCTestCase {
381361
let protos = makeProtosWithDependencies()
382362
let registry = try ReflectionServiceData(fileDescriptors: protos)
383363
let registryFileName = registry.nameOfFileContainingExtension(
384-
named: protos[0].extension[0].extendee,
385-
fieldNumber: 4
364+
extendeeName: protos[0].extension[0].extendee,
365+
fieldNumber: 9
386366
)
387367
XCTAssertNil(registryFileName)
388368
}
@@ -391,7 +371,7 @@ final class ReflectionServiceUnitTests: GRPCTestCase {
391371
var protos = makeProtosWithDependencies()
392372
protos[0].extension.append(
393373
.with {
394-
$0.extendee = "inputMessage1"
374+
$0.extendee = ".packagebar1.inputMessage1"
395375
$0.number = 2
396376
}
397377
)
@@ -404,11 +384,81 @@ final class ReflectionServiceUnitTests: GRPCTestCase {
404384
code: .alreadyExists,
405385
message:
406386
"""
407-
The extension of the inputMessage1 type with the field number equal to \
387+
The extension of the packagebar1.inputMessage1 type with the field number equal to \
408388
2 from \(protos[0].name) already exists in \(protos[0].name).
409389
"""
410390
)
411391
)
412392
}
413393
}
394+
395+
// Testing the extensionsFieldNumbersOfType() method.
396+
397+
func testExtensionsFieldNumbersOfType() throws {
398+
var protos = makeProtosWithDependencies()
399+
protos[0].extension.append(
400+
.with {
401+
$0.extendee = ".packagebar1.inputMessage1"
402+
$0.number = 120
403+
}
404+
)
405+
let registry = try ReflectionServiceData(fileDescriptors: protos)
406+
let extensionNumbers = try registry.extensionsFieldNumbersOfType(
407+
named: "packagebar1.inputMessage1"
408+
)
409+
XCTAssertEqual(extensionNumbers, [1, 2, 3, 4, 5, 120])
410+
}
411+
412+
func testExtensionsFieldNumbersOfTypeNoExtensionsType() throws {
413+
var protos = makeProtosWithDependencies()
414+
protos[0].messageType.append(
415+
Google_Protobuf_DescriptorProto.with {
416+
$0.name = "noExtensionMessage"
417+
$0.field = [
418+
Google_Protobuf_FieldDescriptorProto.with {
419+
$0.name = "noExtensionField"
420+
$0.type = .bool
421+
}
422+
]
423+
}
424+
)
425+
let registry = try ReflectionServiceData(fileDescriptors: protos)
426+
let extensionNumbers = try registry.extensionsFieldNumbersOfType(
427+
named: "packagebar1.noExtensionMessage"
428+
)
429+
XCTAssertEqual(extensionNumbers, [])
430+
}
431+
432+
func testExtensionsFieldNumbersOfTypeInvalidTypeName() throws {
433+
let protos = makeProtosWithDependencies()
434+
let registry = try ReflectionServiceData(fileDescriptors: protos)
435+
XCTAssertThrowsError(
436+
try registry.extensionsFieldNumbersOfType(
437+
named: "packagebar1.invalidTypeMessage"
438+
)
439+
) { error in
440+
XCTAssertEqual(
441+
error as? GRPCStatus,
442+
GRPCStatus(
443+
code: .invalidArgument,
444+
message: "The provided type is invalid."
445+
)
446+
)
447+
}
448+
}
449+
450+
func testExtensionsFieldNumbersOfTypeExtensionsInDifferentProtoFiles() throws {
451+
var protos = makeProtosWithDependencies()
452+
protos[2].extension.append(
453+
.with {
454+
$0.extendee = ".packagebar1.inputMessage1"
455+
$0.number = 130
456+
}
457+
)
458+
let registry = try ReflectionServiceData(fileDescriptors: protos)
459+
let extensionNumbers = try registry.extensionsFieldNumbersOfType(
460+
named: "packagebar1.inputMessage1"
461+
)
462+
XCTAssertEqual(extensionNumbers, [1, 2, 3, 4, 5, 130])
463+
}
414464
}

Tests/GRPCTests/GRPCReflectionServiceTests/Utils.swift

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,23 @@ import Foundation
1818
import GRPC
1919
import SwiftProtobuf
2020

21+
internal func makeExtensions(
22+
forType typeName: String,
23+
number: Int
24+
) -> [Google_Protobuf_FieldDescriptorProto] {
25+
var extensions: [Google_Protobuf_FieldDescriptorProto] = []
26+
for id in 1 ... number {
27+
extensions.append(
28+
Google_Protobuf_FieldDescriptorProto.with {
29+
$0.name = "extension" + typeName + "-" + String(id)
30+
$0.extendee = typeName
31+
$0.number = Int32(id)
32+
}
33+
)
34+
}
35+
return extensions
36+
}
37+
2138
internal func generateFileDescriptorProto(
2239
fileName name: String,
2340
suffix: String
@@ -32,11 +49,11 @@ internal func generateFileDescriptorProto(
3249
]
3350
}
3451

35-
let inputMessageExtension = Google_Protobuf_FieldDescriptorProto.with {
36-
$0.name = "extensionInputMessage" + suffix
37-
$0.extendee = "inputMessage" + suffix
38-
$0.number = 2
39-
}
52+
let packageName = "package" + name + suffix
53+
let inputMessageExtensions = makeExtensions(
54+
forType: "." + packageName + "." + "inputMessage" + suffix,
55+
number: 5
56+
)
4057

4158
let outputMessage = Google_Protobuf_DescriptorProto.with {
4259
$0.name = "outputMessage" + suffix
@@ -77,7 +94,7 @@ internal func generateFileDescriptorProto(
7794
$0.package = "package" + name + suffix
7895
$0.messageType = [inputMessage, outputMessage]
7996
$0.enumType = [enumType]
80-
$0.extension = [inputMessageExtension]
97+
$0.extension = inputMessageExtensions
8198
}
8299

83100
return fileDescriptorProto
@@ -109,6 +126,7 @@ internal func makeProtosWithComplexDependencies() -> [Google_Protobuf_FileDescri
109126
fileName: "fooB",
110127
suffix: String(id) + "B"
111128
)
129+
112130
let parent = protos.count > 1 ? protos.count - Int.random(in: 1 ..< 3) : protos.count - 1
113131
protos[parent].dependency.append(fileDescriptorProtoA.name)
114132
protos[parent].dependency.append(fileDescriptorProtoB.name)

0 commit comments

Comments
 (0)