-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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 <me@gustavocairo.com>
- Loading branch information
Showing
6 changed files
with
772 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
143 changes: 143 additions & 0 deletions
143
Sources/GRPCReflectionService/Service/ReflectionService+V1.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
/* | ||
* Copyright 2024, gRPC Authors All rights reserved. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
internal import GRPCCore | ||
internal import SwiftProtobuf | ||
|
||
extension ReflectionService { | ||
struct V1: Grpc_Reflection_V1_ServerReflection.SimpleServiceProtocol { | ||
private typealias Response = Grpc_Reflection_V1_ServerReflectionResponse | ||
private typealias ResponsePayload = Response.OneOf_MessageResponse | ||
private typealias FileDescriptorResponse = Grpc_Reflection_V1_FileDescriptorResponse | ||
private typealias ExtensionNumberResponse = Grpc_Reflection_V1_ExtensionNumberResponse | ||
private let registry: ReflectionServiceRegistry | ||
|
||
init(registry: ReflectionServiceRegistry) { | ||
self.registry = registry | ||
} | ||
} | ||
} | ||
|
||
extension ReflectionService.V1 { | ||
private func findFileByFileName(_ fileName: String) throws(RPCError) -> FileDescriptorResponse { | ||
let data = try self.registry.serialisedFileDescriptorForDependenciesOfFile(named: fileName) | ||
return .with { $0.fileDescriptorProto = data } | ||
} | ||
|
||
func serverReflectionInfo( | ||
request: RPCAsyncSequence<Grpc_Reflection_V1_ServerReflectionRequest, any Swift.Error>, | ||
response: RPCWriter<Grpc_Reflection_V1_ServerReflectionResponse>, | ||
context: ServerContext | ||
) async throws { | ||
for try await message in request { | ||
let payload: ResponsePayload | ||
|
||
switch message.messageRequest { | ||
case let .fileByFilename(fileName): | ||
payload = .makeFileDescriptorResponse { () throws(RPCError) -> FileDescriptorResponse in | ||
try self.findFileByFileName(fileName) | ||
} | ||
|
||
case .listServices: | ||
payload = .listServicesResponse( | ||
.with { | ||
$0.service = self.registry.serviceNames.map { serviceName in | ||
.with { $0.name = serviceName } | ||
} | ||
} | ||
) | ||
|
||
case let .fileContainingSymbol(symbolName): | ||
payload = .makeFileDescriptorResponse { () throws(RPCError) -> FileDescriptorResponse in | ||
let fileName = try self.registry.fileContainingSymbol(symbolName) | ||
return try self.findFileByFileName(fileName) | ||
} | ||
|
||
case let .fileContainingExtension(extensionRequest): | ||
payload = .makeFileDescriptorResponse { () throws(RPCError) -> FileDescriptorResponse in | ||
let fileName = try self.registry.fileContainingExtension( | ||
extendeeName: extensionRequest.containingType, | ||
fieldNumber: extensionRequest.extensionNumber | ||
) | ||
return try self.findFileByFileName(fileName) | ||
} | ||
|
||
case let .allExtensionNumbersOfType(typeName): | ||
payload = .makeExtensionNumberResponse { () throws(RPCError) -> ExtensionNumberResponse in | ||
let fieldNumbers = try self.registry.extensionFieldNumbersOfType(named: typeName) | ||
return .with { | ||
$0.extensionNumber = fieldNumbers | ||
$0.baseTypeName = typeName | ||
} | ||
} | ||
|
||
default: | ||
payload = .errorResponse( | ||
.with { | ||
$0.errorCode = Int32(RPCError.Code.unimplemented.rawValue) | ||
$0.errorMessage = "The request is not implemented." | ||
} | ||
) | ||
} | ||
|
||
try await response.write(Response(request: message, response: payload)) | ||
} | ||
} | ||
} | ||
|
||
extension Grpc_Reflection_V1_ServerReflectionResponse.OneOf_MessageResponse { | ||
fileprivate init(catching body: () throws(RPCError) -> Self) { | ||
do { | ||
self = try body() | ||
} catch { | ||
self = .errorResponse( | ||
.with { | ||
$0.errorCode = Int32(error.code.rawValue) | ||
$0.errorMessage = error.message | ||
} | ||
) | ||
} | ||
} | ||
|
||
fileprivate static func makeFileDescriptorResponse( | ||
_ body: () throws(RPCError) -> Grpc_Reflection_V1_FileDescriptorResponse | ||
) -> Self { | ||
Self { () throws(RPCError) -> Self in | ||
return .fileDescriptorResponse(try body()) | ||
} | ||
} | ||
|
||
fileprivate static func makeExtensionNumberResponse( | ||
_ body: () throws(RPCError) -> Grpc_Reflection_V1_ExtensionNumberResponse | ||
) -> Self { | ||
Self { () throws(RPCError) -> Self in | ||
return .allExtensionNumbersResponse(try body()) | ||
} | ||
} | ||
} | ||
|
||
extension Grpc_Reflection_V1_ServerReflectionResponse { | ||
fileprivate init( | ||
request: Grpc_Reflection_V1_ServerReflectionRequest, | ||
response: Self.OneOf_MessageResponse | ||
) { | ||
self = .with { | ||
$0.validHost = request.host | ||
$0.originalRequest = request | ||
$0.messageResponse = response | ||
} | ||
} | ||
} |
154 changes: 154 additions & 0 deletions
154
Sources/GRPCReflectionService/Service/ReflectionService.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
/* | ||
* Copyright 2024, gRPC Authors All rights reserved. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
private import DequeModule | ||
public import GRPCCore | ||
public import SwiftProtobuf | ||
|
||
#if canImport(FoundationEssentials) | ||
public import struct FoundationEssentials.URL | ||
public import struct FoundationEssentials.Data | ||
#else | ||
public import struct Foundation.URL | ||
public import struct Foundation.Data | ||
#endif | ||
|
||
/// Implements the gRPC Reflection service (v1). | ||
/// | ||
/// The reflection service is a regular gRPC service providing information about other | ||
/// services. | ||
/// | ||
/// The service will offer information to clients about any registered services. You can register | ||
/// a service by providing its descriptor set to the service. | ||
public final class ReflectionService: Sendable { | ||
private let service: ReflectionService.V1 | ||
|
||
/// Create a new instance of the reflection service from a list of descriptor set file URLs. | ||
/// | ||
/// - Parameter fileURLs: A list of file URLs containing serialized descriptor sets. | ||
public convenience init( | ||
descriptorSetFileURLs fileURLs: [URL] | ||
) throws { | ||
let fileDescriptorProtos = try Self.readDescriptorSets(atURLs: fileURLs) | ||
try self.init(fileDescriptors: fileDescriptorProtos) | ||
} | ||
|
||
/// Create a new instance of the reflection service from a list of descriptor set file paths. | ||
/// | ||
/// - Parameter filePaths: A list of file paths containing serialized descriptor sets. | ||
public convenience init( | ||
descriptorSetFilePaths filePaths: [String] | ||
) throws { | ||
let fileDescriptorProtos = try Self.readDescriptorSets(atPaths: filePaths) | ||
try self.init(fileDescriptors: fileDescriptorProtos) | ||
} | ||
|
||
/// Create a new instance of the reflection service from a list of file descriptor messages. | ||
/// | ||
/// - Parameter fileDescriptors: A list of file descriptors of the services to register. | ||
public init( | ||
fileDescriptors: [Google_Protobuf_FileDescriptorProto] | ||
) throws { | ||
let registry = try ReflectionServiceRegistry(fileDescriptors: fileDescriptors) | ||
self.service = ReflectionService.V1(registry: registry) | ||
} | ||
} | ||
|
||
extension ReflectionService: RegistrableRPCService { | ||
public func registerMethods(with router: inout RPCRouter) { | ||
self.service.registerMethods(with: &router) | ||
} | ||
} | ||
|
||
extension ReflectionService { | ||
static func readSerializedFileDescriptorProto( | ||
atPath path: String | ||
) throws -> Google_Protobuf_FileDescriptorProto { | ||
let fileURL: URL | ||
#if canImport(Darwin) | ||
fileURL = URL(filePath: path, directoryHint: .notDirectory) | ||
#else | ||
fileURL = URL(fileURLWithPath: path) | ||
#endif | ||
|
||
let binaryData = try Data(contentsOf: fileURL) | ||
guard let serializedData = Data(base64Encoded: binaryData) else { | ||
throw RPCError( | ||
code: .invalidArgument, | ||
message: | ||
""" | ||
The \(path) file contents could not be transformed \ | ||
into serialized data representing a file descriptor proto. | ||
""" | ||
) | ||
} | ||
|
||
return try Google_Protobuf_FileDescriptorProto(serializedBytes: serializedData) | ||
} | ||
|
||
static func readSerializedFileDescriptorProtos( | ||
atPaths paths: [String] | ||
) throws -> [Google_Protobuf_FileDescriptorProto] { | ||
var fileDescriptorProtos = [Google_Protobuf_FileDescriptorProto]() | ||
fileDescriptorProtos.reserveCapacity(paths.count) | ||
for path in paths { | ||
try fileDescriptorProtos.append(Self.readSerializedFileDescriptorProto(atPath: path)) | ||
} | ||
return fileDescriptorProtos | ||
} | ||
|
||
static func readDescriptorSet( | ||
atURL fileURL: URL | ||
) throws -> [Google_Protobuf_FileDescriptorProto] { | ||
let binaryData = try Data(contentsOf: fileURL) | ||
let descriptorSet = try Google_Protobuf_FileDescriptorSet(serializedBytes: binaryData) | ||
return descriptorSet.file | ||
} | ||
|
||
static func readDescriptorSet( | ||
atPath path: String | ||
) throws -> [Google_Protobuf_FileDescriptorProto] { | ||
let fileURL: URL | ||
#if canImport(Darwin) | ||
fileURL = URL(filePath: path, directoryHint: .notDirectory) | ||
#else | ||
fileURL = URL(fileURLWithPath: path) | ||
#endif | ||
return try Self.readDescriptorSet(atURL: fileURL) | ||
} | ||
|
||
static func readDescriptorSets( | ||
atURLs fileURLs: [URL] | ||
) throws -> [Google_Protobuf_FileDescriptorProto] { | ||
var fileDescriptorProtos = [Google_Protobuf_FileDescriptorProto]() | ||
fileDescriptorProtos.reserveCapacity(fileURLs.count) | ||
for url in fileURLs { | ||
try fileDescriptorProtos.append(contentsOf: Self.readDescriptorSet(atURL: url)) | ||
} | ||
return fileDescriptorProtos | ||
} | ||
|
||
static func readDescriptorSets( | ||
atPaths paths: [String] | ||
) throws -> [Google_Protobuf_FileDescriptorProto] { | ||
var fileDescriptorProtos = [Google_Protobuf_FileDescriptorProto]() | ||
fileDescriptorProtos.reserveCapacity(paths.count) | ||
for path in paths { | ||
try fileDescriptorProtos.append(contentsOf: Self.readDescriptorSet(atPath: path)) | ||
} | ||
return fileDescriptorProtos | ||
} | ||
} |
Oops, something went wrong.