Skip to content

Commit 0c41ffe

Browse files
glbrnttgjcairo
andauthored
Add support for the reflection service (#21)
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 --------- Co-authored-by: Gus Cairo <[email protected]>
1 parent b7d8e22 commit 0c41ffe

File tree

6 files changed

+772
-0
lines changed

6 files changed

+772
-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: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
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+
internal import SwiftProtobuf
19+
20+
extension ReflectionService {
21+
struct V1: Grpc_Reflection_V1_ServerReflection.SimpleServiceProtocol {
22+
private typealias Response = Grpc_Reflection_V1_ServerReflectionResponse
23+
private typealias ResponsePayload = Response.OneOf_MessageResponse
24+
private typealias FileDescriptorResponse = Grpc_Reflection_V1_FileDescriptorResponse
25+
private typealias ExtensionNumberResponse = Grpc_Reflection_V1_ExtensionNumberResponse
26+
private let registry: ReflectionServiceRegistry
27+
28+
init(registry: ReflectionServiceRegistry) {
29+
self.registry = registry
30+
}
31+
}
32+
}
33+
34+
extension ReflectionService.V1 {
35+
private func findFileByFileName(_ fileName: String) throws(RPCError) -> FileDescriptorResponse {
36+
let data = try self.registry.serialisedFileDescriptorForDependenciesOfFile(named: fileName)
37+
return .with { $0.fileDescriptorProto = data }
38+
}
39+
40+
func serverReflectionInfo(
41+
request: RPCAsyncSequence<Grpc_Reflection_V1_ServerReflectionRequest, any Swift.Error>,
42+
response: RPCWriter<Grpc_Reflection_V1_ServerReflectionResponse>,
43+
context: ServerContext
44+
) async throws {
45+
for try await message in request {
46+
let payload: ResponsePayload
47+
48+
switch message.messageRequest {
49+
case let .fileByFilename(fileName):
50+
payload = .makeFileDescriptorResponse { () throws(RPCError) -> FileDescriptorResponse in
51+
try self.findFileByFileName(fileName)
52+
}
53+
54+
case .listServices:
55+
payload = .listServicesResponse(
56+
.with {
57+
$0.service = self.registry.serviceNames.map { serviceName in
58+
.with { $0.name = serviceName }
59+
}
60+
}
61+
)
62+
63+
case let .fileContainingSymbol(symbolName):
64+
payload = .makeFileDescriptorResponse { () throws(RPCError) -> FileDescriptorResponse in
65+
let fileName = try self.registry.fileContainingSymbol(symbolName)
66+
return try self.findFileByFileName(fileName)
67+
}
68+
69+
case let .fileContainingExtension(extensionRequest):
70+
payload = .makeFileDescriptorResponse { () throws(RPCError) -> FileDescriptorResponse in
71+
let fileName = try self.registry.fileContainingExtension(
72+
extendeeName: extensionRequest.containingType,
73+
fieldNumber: extensionRequest.extensionNumber
74+
)
75+
return try self.findFileByFileName(fileName)
76+
}
77+
78+
case let .allExtensionNumbersOfType(typeName):
79+
payload = .makeExtensionNumberResponse { () throws(RPCError) -> ExtensionNumberResponse in
80+
let fieldNumbers = try self.registry.extensionFieldNumbersOfType(named: typeName)
81+
return .with {
82+
$0.extensionNumber = fieldNumbers
83+
$0.baseTypeName = typeName
84+
}
85+
}
86+
87+
default:
88+
payload = .errorResponse(
89+
.with {
90+
$0.errorCode = Int32(RPCError.Code.unimplemented.rawValue)
91+
$0.errorMessage = "The request is not implemented."
92+
}
93+
)
94+
}
95+
96+
try await response.write(Response(request: message, response: payload))
97+
}
98+
}
99+
}
100+
101+
extension Grpc_Reflection_V1_ServerReflectionResponse.OneOf_MessageResponse {
102+
fileprivate init(catching body: () throws(RPCError) -> Self) {
103+
do {
104+
self = try body()
105+
} catch {
106+
self = .errorResponse(
107+
.with {
108+
$0.errorCode = Int32(error.code.rawValue)
109+
$0.errorMessage = error.message
110+
}
111+
)
112+
}
113+
}
114+
115+
fileprivate static func makeFileDescriptorResponse(
116+
_ body: () throws(RPCError) -> Grpc_Reflection_V1_FileDescriptorResponse
117+
) -> Self {
118+
Self { () throws(RPCError) -> Self in
119+
return .fileDescriptorResponse(try body())
120+
}
121+
}
122+
123+
fileprivate static func makeExtensionNumberResponse(
124+
_ body: () throws(RPCError) -> Grpc_Reflection_V1_ExtensionNumberResponse
125+
) -> Self {
126+
Self { () throws(RPCError) -> Self in
127+
return .allExtensionNumbersResponse(try body())
128+
}
129+
}
130+
}
131+
132+
extension Grpc_Reflection_V1_ServerReflectionResponse {
133+
fileprivate init(
134+
request: Grpc_Reflection_V1_ServerReflectionRequest,
135+
response: Self.OneOf_MessageResponse
136+
) {
137+
self = .with {
138+
$0.validHost = request.host
139+
$0.originalRequest = request
140+
$0.messageResponse = response
141+
}
142+
}
143+
}
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)