Skip to content

Commit 4df985f

Browse files
Add support for file-containing-symbol reflection request. (#1675)
Motivation: The file-containing-symbol request is part of the Reflection Service and enables users to find the proto file containing a symbol they specify and its transitive dependencies. Modifications: Added a dictionary of the fully qualified names of symbols and their corresponding file names, in the ReflectionServiceData struct. Added the function that creates the server response, after getting the file name corresponding to the symbol name and getting its tranaitive dependencies. Also, split the tests into Integration and Unit tests, and added tests for the new request. Result: Users of the Reflection Service will be able to get from the server the proto file that contains the symbols they are specifying in the request and its transitive dependencies.
1 parent a313fcf commit 4df985f

File tree

4 files changed

+471
-192
lines changed

4 files changed

+471
-192
lines changed

Sources/GRPCReflectionService/Server/ReflectionService.swift

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,13 @@ internal struct ReflectionServiceData: Sendable {
4646

4747
internal var fileDescriptorDataByFilename: [String: FileDescriptorProtoData]
4848
internal var serviceNames: [String]
49+
internal var fileNameBySymbol: [String: String]
4950

5051
internal init(fileDescriptors: [Google_Protobuf_FileDescriptorProto]) throws {
5152
self.serviceNames = []
5253
self.fileDescriptorDataByFilename = [:]
54+
self.fileNameBySymbol = [:]
55+
5356
for fileDescriptorProto in fileDescriptors {
5457
let serializedFileDescriptorProto: Data
5558
do {
@@ -67,6 +70,19 @@ internal struct ReflectionServiceData: Sendable {
6770
)
6871
self.fileDescriptorDataByFilename[fileDescriptorProto.name] = protoData
6972
self.serviceNames.append(contentsOf: fileDescriptorProto.service.map { $0.name })
73+
for qualifiedSybolName in fileDescriptorProto.qualifiedSymbolNames {
74+
let oldValue = self.fileNameBySymbol.updateValue(
75+
fileDescriptorProto.name,
76+
forKey: qualifiedSybolName
77+
)
78+
if let oldValue = oldValue {
79+
throw GRPCStatus(
80+
code: .alreadyExists,
81+
message:
82+
"The \(qualifiedSybolName) symbol from \(fileDescriptorProto.name) already exists in \(oldValue)."
83+
)
84+
}
85+
}
7086
}
7187
}
7288

@@ -99,6 +115,10 @@ internal struct ReflectionServiceData: Sendable {
99115
}
100116
return serializedFileDescriptorProtos
101117
}
118+
119+
internal func nameOfFileContainingSymbol(named symbolName: String) -> String? {
120+
return self.fileNameBySymbol[symbolName]
121+
}
102122
}
103123

104124
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
@@ -139,6 +159,19 @@ internal final class ReflectionServiceProvider: Reflection_ServerReflectionAsync
139159
)
140160
}
141161

162+
internal func findFileBySymbol(
163+
_ symbolName: String,
164+
request: Reflection_ServerReflectionRequest
165+
) throws -> Reflection_ServerReflectionResponse {
166+
guard let fileName = self.protoRegistry.nameOfFileContainingSymbol(named: symbolName) else {
167+
throw GRPCStatus(
168+
code: .notFound,
169+
message: "The provided symbol could not be found."
170+
)
171+
}
172+
return try self.findFileByFileName(fileName, request: request)
173+
}
174+
142175
internal func serverReflectionInfo(
143176
requestStream: GRPCAsyncRequestStream<Reflection_ServerReflectionRequest>,
144177
responseStream: GRPCAsyncResponseStreamWriter<Reflection_ServerReflectionResponse>,
@@ -157,6 +190,13 @@ internal final class ReflectionServiceProvider: Reflection_ServerReflectionAsync
157190
let response = try self.getServicesNames(request: request)
158191
try await responseStream.send(response)
159192

193+
case let .fileContainingSymbol(symbolName):
194+
let response = try self.findFileBySymbol(
195+
symbolName,
196+
request: request
197+
)
198+
try await responseStream.send(response)
199+
160200
default:
161201
throw GRPCStatus(code: .unimplemented)
162202
}
@@ -187,3 +227,37 @@ extension Reflection_ServerReflectionResponse {
187227
}
188228
}
189229
}
230+
231+
extension Google_Protobuf_FileDescriptorProto {
232+
var qualifiedServiceAndMethodNames: [String] {
233+
var names: [String] = []
234+
235+
for service in self.service {
236+
names.append(self.package + "." + service.name)
237+
names.append(
238+
contentsOf: service.method
239+
.map { self.package + "." + service.name + "." + $0.name }
240+
)
241+
}
242+
return names
243+
}
244+
245+
var qualifiedMessageTypes: [String] {
246+
return self.messageType.map {
247+
self.package + "." + $0.name
248+
}
249+
}
250+
251+
var qualifiedEnumTypes: [String] {
252+
return self.enumType.map {
253+
self.package + "." + $0.name
254+
}
255+
}
256+
257+
var qualifiedSymbolNames: [String] {
258+
var names = self.qualifiedServiceAndMethodNames
259+
names.append(contentsOf: self.qualifiedMessageTypes)
260+
names.append(contentsOf: self.qualifiedEnumTypes)
261+
return names
262+
}
263+
}
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
/*
2+
* Copyright 2023, gRPC Authors All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import Foundation
18+
import GRPC
19+
import GRPCReflectionService
20+
import NIOPosix
21+
import SwiftProtobuf
22+
import XCTest
23+
24+
@testable import GRPCReflectionService
25+
26+
final class ReflectionServiceIntegrationTests: GRPCTestCase {
27+
private var server: Server?
28+
private var channel: GRPCChannel?
29+
private let protos: [Google_Protobuf_FileDescriptorProto] = makeProtosWithDependencies()
30+
private let independentProto: Google_Protobuf_FileDescriptorProto = generateFileDescriptorProto(
31+
fileName: "independentBar",
32+
suffix: 5
33+
)
34+
35+
private func setUpServerAndChannel() throws {
36+
let reflectionServiceProvider = try ReflectionService(
37+
fileDescriptors: self.protos + [self.independentProto]
38+
)
39+
40+
let server = try Server.insecure(group: MultiThreadedEventLoopGroup.singleton)
41+
.withServiceProviders([reflectionServiceProvider])
42+
.withLogger(self.serverLogger)
43+
.bind(host: "127.0.0.1", port: 0)
44+
.wait()
45+
self.server = server
46+
47+
let channel = try GRPCChannelPool.with(
48+
target: .hostAndPort("127.0.0.1", server.channel.localAddress!.port!),
49+
transportSecurity: .plaintext,
50+
eventLoopGroup: MultiThreadedEventLoopGroup.singleton
51+
) {
52+
$0.backgroundActivityLogger = self.clientLogger
53+
}
54+
55+
self.channel = channel
56+
}
57+
58+
override func tearDown() {
59+
if let channel = self.channel {
60+
XCTAssertNoThrow(try channel.close().wait())
61+
}
62+
if let server = self.server {
63+
XCTAssertNoThrow(try server.close().wait())
64+
}
65+
66+
super.tearDown()
67+
}
68+
69+
func testFileByFileName() async throws {
70+
try self.setUpServerAndChannel()
71+
let client = Reflection_ServerReflectionAsyncClient(channel: self.channel!)
72+
let serviceReflectionInfo = client.makeServerReflectionInfoCall()
73+
try await serviceReflectionInfo.requestStream.send(
74+
.with {
75+
$0.host = "127.0.0.1"
76+
$0.fileByFilename = "bar1.proto"
77+
}
78+
)
79+
serviceReflectionInfo.requestStream.finish()
80+
81+
var iterator = serviceReflectionInfo.responseStream.makeAsyncIterator()
82+
guard let message = try await iterator.next() else {
83+
return XCTFail("Could not get a response message.")
84+
}
85+
86+
let receivedFileDescriptorProto =
87+
try Google_Protobuf_FileDescriptorProto(
88+
serializedData: (message.fileDescriptorResponse
89+
.fileDescriptorProto[0])
90+
)
91+
92+
XCTAssertEqual(receivedFileDescriptorProto.name, "bar1.proto")
93+
XCTAssertEqual(receivedFileDescriptorProto.service.count, 1)
94+
95+
guard let service = receivedFileDescriptorProto.service.first else {
96+
return XCTFail("The received file descriptor proto doesn't have any services.")
97+
}
98+
guard let method = service.method.first else {
99+
return XCTFail("The service of the received file descriptor proto doesn't have any methods.")
100+
}
101+
XCTAssertEqual(method.name, "testMethod1")
102+
XCTAssertEqual(message.fileDescriptorResponse.fileDescriptorProto.count, 4)
103+
}
104+
105+
func testListServices() async throws {
106+
try self.setUpServerAndChannel()
107+
let client = Reflection_ServerReflectionAsyncClient(channel: self.channel!)
108+
let serviceReflectionInfo = client.makeServerReflectionInfoCall()
109+
110+
try await serviceReflectionInfo.requestStream.send(
111+
.with {
112+
$0.host = "127.0.0.1"
113+
$0.listServices = "services"
114+
}
115+
)
116+
117+
serviceReflectionInfo.requestStream.finish()
118+
var iterator = serviceReflectionInfo.responseStream.makeAsyncIterator()
119+
guard let message = try await iterator.next() else {
120+
return XCTFail("Could not get a response message.")
121+
}
122+
123+
let receivedServices = message.listServicesResponse.service.map { $0.name }.sorted()
124+
let servicesNames = (self.protos + [self.independentProto]).serviceNames.sorted()
125+
126+
XCTAssertEqual(receivedServices, servicesNames)
127+
}
128+
129+
func testFileBySymbol() async throws {
130+
try self.setUpServerAndChannel()
131+
let client = Reflection_ServerReflectionAsyncClient(channel: self.channel!)
132+
let serviceReflectionInfo = client.makeServerReflectionInfoCall()
133+
134+
try await serviceReflectionInfo.requestStream.send(
135+
.with {
136+
$0.host = "127.0.0.1"
137+
$0.fileContainingSymbol = "packagebar1.enumType1"
138+
}
139+
)
140+
141+
serviceReflectionInfo.requestStream.finish()
142+
var iterator = serviceReflectionInfo.responseStream.makeAsyncIterator()
143+
guard let message = try await iterator.next() else {
144+
return XCTFail("Could not get a response message.")
145+
}
146+
let receivedData: [Google_Protobuf_FileDescriptorProto]
147+
do {
148+
receivedData = try message.fileDescriptorResponse.fileDescriptorProto.map {
149+
try Google_Protobuf_FileDescriptorProto(serializedData: $0)
150+
}
151+
} catch {
152+
return XCTFail("Could not serialize data received as a message.")
153+
}
154+
155+
let fileToFind = self.protos[0]
156+
let dependentProtos = self.protos[1...]
157+
for fileDescriptorProto in receivedData {
158+
if fileDescriptorProto == fileToFind {
159+
XCTAssert(
160+
fileDescriptorProto.enumType.names.contains("enumType1"),
161+
"""
162+
The response doesn't contain the serialized file descriptor proto \
163+
containing the \"packagebar1.enumType1\" symbol.
164+
"""
165+
)
166+
} else {
167+
XCTAssert(
168+
dependentProtos.contains(fileDescriptorProto),
169+
"""
170+
The \(fileDescriptorProto.name) is not a dependency of the \
171+
proto file containing the \"packagebar1.enumType1\" symbol.
172+
"""
173+
)
174+
}
175+
}
176+
}
177+
}

0 commit comments

Comments
 (0)