Skip to content

Commit

Permalink
Add support for the reflection service (#21)
Browse files Browse the repository at this point in the history
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
glbrntt and gjcairo authored Dec 20, 2024
1 parent b7d8e22 commit 0c41ffe
Show file tree
Hide file tree
Showing 6 changed files with 772 additions and 0 deletions.
28 changes: 28 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ let products: [Product] = [
name: "GRPCHealthService",
targets: ["GRPCHealthService"]
),
.library(
name: "GRPCReflectionService",
targets: ["GRPCReflectionService"]
),
.library(
name: "GRPCInterceptors",
targets: ["GRPCInterceptors"]
Expand Down Expand Up @@ -79,6 +83,30 @@ let targets: [Target] = [
swiftSettings: defaultSwiftSettings
),

// An implementation of the gRPC Reflection service.
.target(
name: "GRPCReflectionService",
dependencies: [
.product(name: "GRPCCore", package: "grpc-swift"),
.product(name: "GRPCProtobuf", package: "grpc-swift-protobuf"),
.product(name: "SwiftProtobuf", package: "swift-protobuf"),
],
swiftSettings: defaultSwiftSettings
),
.testTarget(
name: "GRPCReflectionServiceTests",
dependencies: [
.target(name: "GRPCReflectionService"),
.product(name: "GRPCCore", package: "grpc-swift"),
.product(name: "GRPCInProcessTransport", package: "grpc-swift"),
.product(name: "SwiftProtobuf", package: "swift-protobuf"),
],
resources: [
.copy("Generated/DescriptorSets")
],
swiftSettings: defaultSwiftSettings
),

// Common interceptors for gRPC.
.target(
name: "GRPCInterceptors",
Expand Down
143 changes: 143 additions & 0 deletions Sources/GRPCReflectionService/Service/ReflectionService+V1.swift
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 Sources/GRPCReflectionService/Service/ReflectionService.swift
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
}
}
Loading

0 comments on commit 0c41ffe

Please sign in to comment.