Skip to content

Commit 7b613ad

Browse files
committed
Add support for the reflection service
Motivation: The reflection service is widely used and we should offer an implementation out-of-the-box. Modifications: - Add back an updated version of the reflection service from grpc-swift v1. Result: Can use reflection service
1 parent b7d8e22 commit 7b613ad

File tree

6 files changed

+771
-0
lines changed

6 files changed

+771
-0
lines changed

Package.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ let products: [Product] = [
2222
name: "GRPCHealthService",
2323
targets: ["GRPCHealthService"]
2424
),
25+
.library(
26+
name: "GRPCReflectionService",
27+
targets: ["GRPCReflectionService"]
28+
),
2529
.library(
2630
name: "GRPCInterceptors",
2731
targets: ["GRPCInterceptors"]
@@ -79,6 +83,30 @@ let targets: [Target] = [
7983
swiftSettings: defaultSwiftSettings
8084
),
8185

86+
// An implementation of the gRPC Reflection service.
87+
.target(
88+
name: "GRPCReflectionService",
89+
dependencies: [
90+
.product(name: "GRPCCore", package: "grpc-swift"),
91+
.product(name: "GRPCProtobuf", package: "grpc-swift-protobuf"),
92+
.product(name: "SwiftProtobuf", package: "swift-protobuf"),
93+
],
94+
swiftSettings: defaultSwiftSettings
95+
),
96+
.testTarget(
97+
name: "GRPCReflectionServiceTests",
98+
dependencies: [
99+
.target(name: "GRPCReflectionService"),
100+
.product(name: "GRPCCore", package: "grpc-swift"),
101+
.product(name: "GRPCInProcessTransport", package: "grpc-swift"),
102+
.product(name: "SwiftProtobuf", package: "swift-protobuf"),
103+
],
104+
resources: [
105+
.copy("Generated/DescriptorSets")
106+
],
107+
swiftSettings: defaultSwiftSettings
108+
),
109+
82110
// Common interceptors for gRPC.
83111
.target(
84112
name: "GRPCInterceptors",
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/*
2+
* Copyright 2024, 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+
internal import GRPCCore
18+
19+
extension ReflectionService {
20+
struct V1: Grpc_Reflection_V1_ServerReflection.SimpleServiceProtocol {
21+
private typealias Response = Grpc_Reflection_V1_ServerReflectionResponse
22+
private typealias ResponsePayload = Response.OneOf_MessageResponse
23+
private typealias FileDescriptorResponse = Grpc_Reflection_V1_FileDescriptorResponse
24+
private typealias ExtensionNumberResponse = Grpc_Reflection_V1_ExtensionNumberResponse
25+
private let registry: ReflectionServiceRegistry
26+
27+
init(registry: ReflectionServiceRegistry) {
28+
self.registry = registry
29+
}
30+
}
31+
}
32+
33+
extension ReflectionService.V1 {
34+
private func findFileByFileName(_ fileName: String) throws(RPCError) -> FileDescriptorResponse {
35+
let data = try self.registry.serialisedFileDescriptorForDependenciesOfFile(named: fileName)
36+
return .with { $0.fileDescriptorProto = data }
37+
}
38+
39+
func serverReflectionInfo(
40+
request: RPCAsyncSequence<Grpc_Reflection_V1_ServerReflectionRequest, any Swift.Error>,
41+
response: RPCWriter<Grpc_Reflection_V1_ServerReflectionResponse>,
42+
context: ServerContext
43+
) async throws {
44+
for try await message in request {
45+
let payload: ResponsePayload
46+
47+
switch message.messageRequest {
48+
case let .fileByFilename(fileName):
49+
payload = .makeFileDescriptorResponse { () throws(RPCError) -> FileDescriptorResponse in
50+
try self.findFileByFileName(fileName)
51+
}
52+
53+
case .listServices:
54+
payload = .listServicesResponse(
55+
.with {
56+
$0.service = self.registry.serviceNames.map { serviceName in
57+
.with { $0.name = serviceName }
58+
}
59+
}
60+
)
61+
62+
case let .fileContainingSymbol(symbolName):
63+
payload = .makeFileDescriptorResponse { () throws(RPCError) -> FileDescriptorResponse in
64+
let fileName = try self.registry.fileContainingSymbol(symbolName)
65+
return try self.findFileByFileName(fileName)
66+
}
67+
68+
case let .fileContainingExtension(extensionRequest):
69+
payload = .makeFileDescriptorResponse { () throws(RPCError) -> FileDescriptorResponse in
70+
let fileName = try self.registry.fileContainingExtension(
71+
extendeeName: extensionRequest.containingType,
72+
fieldNumber: extensionRequest.extensionNumber
73+
)
74+
return try self.findFileByFileName(fileName)
75+
}
76+
77+
case let .allExtensionNumbersOfType(typeName):
78+
payload = .makeExtensionNumberResponse { () throws(RPCError) -> ExtensionNumberResponse in
79+
let fieldNumbers = try self.registry.extensionFieldNumbersOfType(named: typeName)
80+
return .with {
81+
$0.extensionNumber = fieldNumbers
82+
$0.baseTypeName = typeName
83+
}
84+
}
85+
86+
default:
87+
payload = .errorResponse(
88+
.with {
89+
$0.errorCode = Int32(RPCError.Code.unimplemented.rawValue)
90+
$0.errorMessage = "The request is not implemented."
91+
}
92+
)
93+
}
94+
95+
try await response.write(Response(request: message, response: payload))
96+
}
97+
}
98+
}
99+
100+
extension Grpc_Reflection_V1_ServerReflectionResponse.OneOf_MessageResponse {
101+
fileprivate init(catching body: () throws(RPCError) -> Self) {
102+
do {
103+
self = try body()
104+
} catch {
105+
self = .errorResponse(
106+
.with {
107+
$0.errorCode = Int32(error.code.rawValue)
108+
$0.errorMessage = error.message
109+
}
110+
)
111+
}
112+
}
113+
114+
fileprivate static func makeFileDescriptorResponse(
115+
_ body: () throws(RPCError) -> Grpc_Reflection_V1_FileDescriptorResponse
116+
) -> Self {
117+
Self { () throws(RPCError) -> Self in
118+
return .fileDescriptorResponse(try body())
119+
}
120+
}
121+
122+
fileprivate static func makeExtensionNumberResponse(
123+
_ body: () throws(RPCError) -> Grpc_Reflection_V1_ExtensionNumberResponse
124+
) -> Self {
125+
Self { () throws(RPCError) -> Self in
126+
return .allExtensionNumbersResponse(try body())
127+
}
128+
}
129+
}
130+
131+
extension Grpc_Reflection_V1_ServerReflectionResponse {
132+
fileprivate init(
133+
request: Grpc_Reflection_V1_ServerReflectionRequest,
134+
response: Self.OneOf_MessageResponse
135+
) {
136+
self = .with {
137+
$0.validHost = request.host
138+
$0.originalRequest = request
139+
$0.messageResponse = response
140+
}
141+
}
142+
}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/*
2+
* Copyright 2024, 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+
private import DequeModule
18+
public import GRPCCore
19+
public import SwiftProtobuf
20+
21+
#if canImport(FoundationEssentials)
22+
public import struct FoundationEssentials.URL
23+
public import struct FoundationEssentials.Data
24+
#else
25+
public import struct Foundation.URL
26+
public import struct Foundation.Data
27+
#endif
28+
29+
/// Implements the gRPC Reflection service (v1).
30+
///
31+
/// The reflection service is a regular gRPC service providing information about other
32+
/// services.
33+
///
34+
/// The service will offer information to clients about any registered services. You can register
35+
/// a service by providing its descriptor set to the service.
36+
public final class ReflectionService: Sendable {
37+
private let service: ReflectionService.V1
38+
39+
/// Create a new instance of the reflection service from a list of descriptor set file URLs.
40+
///
41+
/// - Parameter fileURLs: A list of file URLs containing serialized descriptor sets.
42+
public convenience init(
43+
descriptorSetFileURLs fileURLs: [URL]
44+
) throws {
45+
let fileDescriptorProtos = try Self.readDescriptorSets(atURLs: fileURLs)
46+
try self.init(fileDescriptors: fileDescriptorProtos)
47+
}
48+
49+
/// Create a new instance of the reflection service from a list of descriptor set file paths.
50+
///
51+
/// - Parameter filePaths: A list of file paths containing serialized descriptor sets.
52+
public convenience init(
53+
descriptorSetFilePaths filePaths: [String]
54+
) throws {
55+
let fileDescriptorProtos = try Self.readDescriptorSets(atPaths: filePaths)
56+
try self.init(fileDescriptors: fileDescriptorProtos)
57+
}
58+
59+
/// Create a new instance of the reflection service from a list of file descriptor messages.
60+
///
61+
/// - Parameter fileDescriptors: A list of file descriptors of the services to register.
62+
public init(
63+
fileDescriptors: [Google_Protobuf_FileDescriptorProto]
64+
) throws {
65+
let registry = try ReflectionServiceRegistry(fileDescriptors: fileDescriptors)
66+
self.service = ReflectionService.V1(registry: registry)
67+
}
68+
}
69+
70+
extension ReflectionService: RegistrableRPCService {
71+
public func registerMethods(with router: inout RPCRouter) {
72+
self.service.registerMethods(with: &router)
73+
}
74+
}
75+
76+
extension ReflectionService {
77+
static func readSerializedFileDescriptorProto(
78+
atPath path: String
79+
) throws -> Google_Protobuf_FileDescriptorProto {
80+
let fileURL: URL
81+
#if canImport(Darwin)
82+
fileURL = URL(filePath: path, directoryHint: .notDirectory)
83+
#else
84+
fileURL = URL(fileURLWithPath: path)
85+
#endif
86+
87+
let binaryData = try Data(contentsOf: fileURL)
88+
guard let serializedData = Data(base64Encoded: binaryData) else {
89+
throw RPCError(
90+
code: .invalidArgument,
91+
message:
92+
"""
93+
The \(path) file contents could not be transformed \
94+
into serialized data representing a file descriptor proto.
95+
"""
96+
)
97+
}
98+
99+
return try Google_Protobuf_FileDescriptorProto(serializedBytes: serializedData)
100+
}
101+
102+
static func readSerializedFileDescriptorProtos(
103+
atPaths paths: [String]
104+
) throws -> [Google_Protobuf_FileDescriptorProto] {
105+
var fileDescriptorProtos = [Google_Protobuf_FileDescriptorProto]()
106+
fileDescriptorProtos.reserveCapacity(paths.count)
107+
for path in paths {
108+
try fileDescriptorProtos.append(Self.readSerializedFileDescriptorProto(atPath: path))
109+
}
110+
return fileDescriptorProtos
111+
}
112+
113+
static func readDescriptorSet(
114+
atURL fileURL: URL
115+
) throws -> [Google_Protobuf_FileDescriptorProto] {
116+
let binaryData = try Data(contentsOf: fileURL)
117+
let descriptorSet = try Google_Protobuf_FileDescriptorSet(serializedBytes: binaryData)
118+
return descriptorSet.file
119+
}
120+
121+
static func readDescriptorSet(
122+
atPath path: String
123+
) throws -> [Google_Protobuf_FileDescriptorProto] {
124+
let fileURL: URL
125+
#if canImport(Darwin)
126+
fileURL = URL(filePath: path, directoryHint: .notDirectory)
127+
#else
128+
fileURL = URL(fileURLWithPath: path)
129+
#endif
130+
return try Self.readDescriptorSet(atURL: fileURL)
131+
}
132+
133+
static func readDescriptorSets(
134+
atURLs fileURLs: [URL]
135+
) throws -> [Google_Protobuf_FileDescriptorProto] {
136+
var fileDescriptorProtos = [Google_Protobuf_FileDescriptorProto]()
137+
fileDescriptorProtos.reserveCapacity(fileURLs.count)
138+
for url in fileURLs {
139+
try fileDescriptorProtos.append(contentsOf: Self.readDescriptorSet(atURL: url))
140+
}
141+
return fileDescriptorProtos
142+
}
143+
144+
static func readDescriptorSets(
145+
atPaths paths: [String]
146+
) throws -> [Google_Protobuf_FileDescriptorProto] {
147+
var fileDescriptorProtos = [Google_Protobuf_FileDescriptorProto]()
148+
fileDescriptorProtos.reserveCapacity(paths.count)
149+
for path in paths {
150+
try fileDescriptorProtos.append(contentsOf: Self.readDescriptorSet(atPath: path))
151+
}
152+
return fileDescriptorProtos
153+
}
154+
}

0 commit comments

Comments
 (0)