From ba8122e64ed1cfa0b60e2dcfc398c75e3cde97b4 Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 21 Nov 2024 11:11:04 +0000 Subject: [PATCH 1/2] Use ServiceDescriptor within MethodDescriptor Motivation: MethodDescriptor uses a string to represent the fully qualified name of the service the method belongs to. ServiceDescriptor also represents a fully qualified name of a service. It'd make more sense for MethodDescriptor to use a ServiceDescriptor instead of a string to describe the service. Modifications: - ServiceDescriptor now stores the fully qualified service name (as this is generally more useful) and computes the package and unqualified service name. - ServiceDescriptor can now be created from the fully qualified name or from a separate package and service name. - MethodDescriptor now holds a ServiceDescriptor instead of a String for the service Result: More coherent API --- .../ClientInterceptorPipelineOperation.swift | 2 +- .../ServerInterceptorPipelineOperation.swift | 2 +- Sources/GRPCCore/Internal/MethodConfigs.swift | 10 ++++-- Sources/GRPCCore/MethodDescriptor.swift | 31 +++++++++++++---- Sources/GRPCCore/ServiceDescriptor.swift | 34 ++++++++++++++----- ...entInterceptorPipelineOperationTests.swift | 8 ++--- .../ClientRPCExecutorTestHarness.swift | 2 +- .../ServerRPCExecutorTestHarness.swift | 2 +- .../Call/Server/RPCRouterTests.swift | 12 ++++--- .../ServerInterceptorPipelineOperation.swift | 8 ++--- Tests/GRPCCoreTests/GRPCClientTests.swift | 12 +++---- Tests/GRPCCoreTests/GRPCServerTests.swift | 4 +-- .../Internal/MethodConfigsTests.swift | 10 +++--- .../GRPCCoreTests/MethodDescriptorTests.swift | 14 ++++---- .../ServiceDescriptorTests.swift | 26 ++++++++++---- .../Test Utilities/Services/BinaryEcho.swift | 8 ++--- .../Test Utilities/Services/HelloWorld.swift | 2 +- .../InProcessClientTransportTests.swift | 33 ++++++------------ .../InProcessServerTransportTests.swift | 6 ++-- .../InProcessTransportTests.swift | 5 ++- 20 files changed, 141 insertions(+), 90 deletions(-) diff --git a/Sources/GRPCCore/Call/Client/ClientInterceptorPipelineOperation.swift b/Sources/GRPCCore/Call/Client/ClientInterceptorPipelineOperation.swift index 4ae2df8d5..d67047bc7 100644 --- a/Sources/GRPCCore/Call/Client/ClientInterceptorPipelineOperation.swift +++ b/Sources/GRPCCore/Call/Client/ClientInterceptorPipelineOperation.swift @@ -61,7 +61,7 @@ public struct ClientInterceptorPipelineOperation: Sendable { return true case .services(let services): - return services.map({ $0.fullyQualifiedService }).contains(descriptor.service) + return services.contains(descriptor.service) case .methods(let methods): return methods.contains(descriptor) diff --git a/Sources/GRPCCore/Call/Server/ServerInterceptorPipelineOperation.swift b/Sources/GRPCCore/Call/Server/ServerInterceptorPipelineOperation.swift index e511ea3ec..e657fdf1c 100644 --- a/Sources/GRPCCore/Call/Server/ServerInterceptorPipelineOperation.swift +++ b/Sources/GRPCCore/Call/Server/ServerInterceptorPipelineOperation.swift @@ -61,7 +61,7 @@ public struct ServerInterceptorPipelineOperation: Sendable { return true case .services(let services): - return services.map({ $0.fullyQualifiedService }).contains(descriptor.service) + return services.contains(descriptor.service) case .methods(let methods): return methods.contains(descriptor) diff --git a/Sources/GRPCCore/Internal/MethodConfigs.swift b/Sources/GRPCCore/Internal/MethodConfigs.swift index 163122dfe..6aab8858d 100644 --- a/Sources/GRPCCore/Internal/MethodConfigs.swift +++ b/Sources/GRPCCore/Internal/MethodConfigs.swift @@ -51,7 +51,10 @@ package struct MethodConfigs: Sendable, Hashable { /// - descriptor: The ``MethodDescriptor`` for which to get or set a ``MethodConfig``. package subscript(_ descriptor: MethodDescriptor) -> MethodConfig? { get { - var name = MethodConfig.Name(service: descriptor.service, method: descriptor.method) + var name = MethodConfig.Name( + service: descriptor.service.fullyQualifiedService, + method: descriptor.method + ) if let configuration = self.elements[name] { return configuration @@ -70,7 +73,10 @@ package struct MethodConfigs: Sendable, Hashable { } set { - let name = MethodConfig.Name(service: descriptor.service, method: descriptor.method) + let name = MethodConfig.Name( + service: descriptor.service.fullyQualifiedService, + method: descriptor.method + ) self.elements[name] = newValue } } diff --git a/Sources/GRPCCore/MethodDescriptor.swift b/Sources/GRPCCore/MethodDescriptor.swift index 8d2795ac1..dc4270e70 100644 --- a/Sources/GRPCCore/MethodDescriptor.swift +++ b/Sources/GRPCCore/MethodDescriptor.swift @@ -16,11 +16,8 @@ /// A description of a method on a service. public struct MethodDescriptor: Sendable, Hashable { - /// The name of the service, including the package name. - /// - /// For example, the name of the "Greeter" service in "helloworld" package - /// is "helloworld.Greeter". - public var service: String + /// A description of the service, including its package name. + public var service: ServiceDescriptor /// The name of the method in the service, excluding the service name. public var method: String @@ -39,8 +36,30 @@ public struct MethodDescriptor: Sendable, Hashable { /// - service: The name of the service, including the package name. For example, /// "helloworld.Greeter". /// - method: The name of the method. For example, "SayHello". - public init(service: String, method: String) { + public init(service: ServiceDescriptor, method: String) { self.service = service self.method = method } + + /// Creates a new method descriptor. + /// + /// - Parameters: + /// - fullyQualifiedService: The fully qualified name of the service, including the package + /// name. For example, "helloworld.Greeter". + /// - method: The name of the method. For example, "SayHello". + public init(fullyQualifiedService: String, method: String) { + self.service = ServiceDescriptor(fullyQualifiedService: fullyQualifiedService) + self.method = method + } + + @available(*, deprecated, renamed: "init(fullyQualifiedService:method:)") + /// Creates a new method descriptor. + /// + /// - Parameters: + /// - service: The fully qualified name of the service, including the package + /// name. For example, "helloworld.Greeter". + /// - method: The name of the method. For example, "SayHello". + public init(service: String, method: String) { + self.init(fullyQualifiedService: service, method: method) + } } diff --git a/Sources/GRPCCore/ServiceDescriptor.swift b/Sources/GRPCCore/ServiceDescriptor.swift index b09730c3b..861eec14d 100644 --- a/Sources/GRPCCore/ServiceDescriptor.swift +++ b/Sources/GRPCCore/ServiceDescriptor.swift @@ -18,20 +18,33 @@ public struct ServiceDescriptor: Sendable, Hashable { /// The name of the package the service belongs to. For example, "helloworld". /// An empty string means that the service does not belong to any package. - public var package: String + public var package: String { + if let index = self.fullyQualifiedService.utf8.lastIndex(of: UInt8(ascii: ".")) { + return String(self.fullyQualifiedService[.. Void ) async throws { let inProcess = InProcessTransport() - let client = GRPCClient(transport: inProcess.client, interceptorPipeline: interceptorPipeline) - let server = GRPCServer(transport: inProcess.server, services: services) + _ = GRPCClient(transport: inProcess.client, interceptorPipeline: interceptorPipeline) + _ = GRPCServer(transport: inProcess.server, services: services) try await withGRPCServer( transport: inProcess.server, @@ -137,7 +137,7 @@ final class GRPCClientTests: XCTestCase { try await self.withInProcessConnectedClient(services: [BinaryEcho()]) { client, _ in try await client.unary( request: .init(message: [3, 1, 4, 1, 5]), - descriptor: MethodDescriptor(service: "not", method: "implemented"), + descriptor: MethodDescriptor(fullyQualifiedService: "not", method: "implemented"), serializer: IdentitySerializer(), deserializer: IdentityDeserializer(), options: .defaults @@ -157,7 +157,7 @@ final class GRPCClientTests: XCTestCase { try await writer.write([byte]) } }), - descriptor: MethodDescriptor(service: "not", method: "implemented"), + descriptor: MethodDescriptor(fullyQualifiedService: "not", method: "implemented"), serializer: IdentitySerializer(), deserializer: IdentityDeserializer(), options: .defaults @@ -173,7 +173,7 @@ final class GRPCClientTests: XCTestCase { try await self.withInProcessConnectedClient(services: [BinaryEcho()]) { client, _ in try await client.serverStreaming( request: .init(message: [3, 1, 4, 1, 5]), - descriptor: MethodDescriptor(service: "not", method: "implemented"), + descriptor: MethodDescriptor(fullyQualifiedService: "not", method: "implemented"), serializer: IdentitySerializer(), deserializer: IdentityDeserializer(), options: .defaults @@ -193,7 +193,7 @@ final class GRPCClientTests: XCTestCase { try await writer.write([byte]) } }), - descriptor: MethodDescriptor(service: "not", method: "implemented"), + descriptor: MethodDescriptor(fullyQualifiedService: "not", method: "implemented"), serializer: IdentitySerializer(), deserializer: IdentityDeserializer(), options: .defaults diff --git a/Tests/GRPCCoreTests/GRPCServerTests.swift b/Tests/GRPCCoreTests/GRPCServerTests.swift index b61fb2022..c7856d7c0 100644 --- a/Tests/GRPCCoreTests/GRPCServerTests.swift +++ b/Tests/GRPCCoreTests/GRPCServerTests.swift @@ -164,7 +164,7 @@ final class GRPCServerTests: XCTestCase { func testUnimplementedMethod() async throws { try await self.withInProcessClientConnectedToServer(services: [BinaryEcho()]) { client, _ in try await client.withStream( - descriptor: MethodDescriptor(service: "not", method: "implemented"), + descriptor: MethodDescriptor(fullyQualifiedService: "not", method: "implemented"), options: .defaults ) { stream in try await stream.outbound.write(.metadata([:])) @@ -248,7 +248,7 @@ final class GRPCServerTests: XCTestCase { interceptorPipeline: [.apply(.requestCounter(counter), to: .all)] ) { client, _ in try await client.withStream( - descriptor: MethodDescriptor(service: "not", method: "implemented"), + descriptor: MethodDescriptor(fullyQualifiedService: "not", method: "implemented"), options: .defaults ) { stream in try await stream.outbound.write(.metadata([:])) diff --git a/Tests/GRPCCoreTests/Internal/MethodConfigsTests.swift b/Tests/GRPCCoreTests/Internal/MethodConfigsTests.swift index 095a5eded..bac916763 100644 --- a/Tests/GRPCCoreTests/Internal/MethodConfigsTests.swift +++ b/Tests/GRPCCoreTests/Internal/MethodConfigsTests.swift @@ -26,7 +26,7 @@ final class MethodConfigsTests: XCTestCase { let defaultConfiguration = MethodConfig(names: [], executionPolicy: .hedge(policy)) var configurations = MethodConfigs() configurations.setDefaultConfig(defaultConfiguration) - let descriptor = MethodDescriptor(service: "test", method: "first") + let descriptor = MethodDescriptor(fullyQualifiedService: "test", method: "first") let retryPolicy = RetryPolicy( maxAttempts: 10, initialBackoff: .seconds(1), @@ -49,7 +49,7 @@ final class MethodConfigsTests: XCTestCase { let defaultConfiguration = MethodConfig(names: [], executionPolicy: .hedge(policy)) var configurations = MethodConfigs() configurations.setDefaultConfig(defaultConfiguration) - let firstDescriptor = MethodDescriptor(service: "test", method: "") + let firstDescriptor = MethodDescriptor(fullyQualifiedService: "test", method: "") let retryPolicy = RetryPolicy( maxAttempts: 10, initialBackoff: .seconds(1), @@ -60,7 +60,7 @@ final class MethodConfigsTests: XCTestCase { let overrideConfiguration = MethodConfig(names: [], executionPolicy: .retry(retryPolicy)) configurations[firstDescriptor] = overrideConfiguration - let secondDescriptor = MethodDescriptor(service: "test", method: "second") + let secondDescriptor = MethodDescriptor(fullyQualifiedService: "test", method: "second") XCTAssertEqual(configurations[secondDescriptor], overrideConfiguration) } @@ -73,7 +73,7 @@ final class MethodConfigsTests: XCTestCase { let defaultConfiguration = MethodConfig(names: [], executionPolicy: .hedge(policy)) var configurations = MethodConfigs() configurations.setDefaultConfig(defaultConfiguration) - let firstDescriptor = MethodDescriptor(service: "test1", method: "first") + let firstDescriptor = MethodDescriptor(fullyQualifiedService: "test1", method: "first") let retryPolicy = RetryPolicy( maxAttempts: 10, initialBackoff: .seconds(1), @@ -84,7 +84,7 @@ final class MethodConfigsTests: XCTestCase { let overrideConfiguration = MethodConfig(names: [], executionPolicy: .retry(retryPolicy)) configurations[firstDescriptor] = overrideConfiguration - let secondDescriptor = MethodDescriptor(service: "test2", method: "second") + let secondDescriptor = MethodDescriptor(fullyQualifiedService: "test2", method: "second") XCTAssertEqual(configurations[secondDescriptor], defaultConfiguration) } } diff --git a/Tests/GRPCCoreTests/MethodDescriptorTests.swift b/Tests/GRPCCoreTests/MethodDescriptorTests.swift index cf4568898..98740aa28 100644 --- a/Tests/GRPCCoreTests/MethodDescriptorTests.swift +++ b/Tests/GRPCCoreTests/MethodDescriptorTests.swift @@ -14,13 +14,15 @@ * limitations under the License. */ import GRPCCore -import XCTest +import Testing -final class MethodDescriptorTests: XCTestCase { +@Suite +struct MethodDescriptorTests { + @Test("Fully qualified name") func testFullyQualifiedName() { - let descriptor = MethodDescriptor(service: "foo.bar", method: "Baz") - XCTAssertEqual(descriptor.service, "foo.bar") - XCTAssertEqual(descriptor.method, "Baz") - XCTAssertEqual(descriptor.fullyQualifiedMethod, "foo.bar/Baz") + let descriptor = MethodDescriptor(fullyQualifiedService: "foo.bar", method: "Baz") + #expect(descriptor.service == ServiceDescriptor(fullyQualifiedService: "foo.bar")) + #expect(descriptor.method == "Baz") + #expect(descriptor.fullyQualifiedMethod == "foo.bar/Baz") } } diff --git a/Tests/GRPCCoreTests/ServiceDescriptorTests.swift b/Tests/GRPCCoreTests/ServiceDescriptorTests.swift index 0adfe524a..7124ae058 100644 --- a/Tests/GRPCCoreTests/ServiceDescriptorTests.swift +++ b/Tests/GRPCCoreTests/ServiceDescriptorTests.swift @@ -14,13 +14,25 @@ * limitations under the License. */ import GRPCCore -import XCTest +import Testing -final class ServiceDescriptorTests: XCTestCase { - func testFullyQualifiedName() { - let descriptor = ServiceDescriptor(package: "foo.bar", service: "Baz") - XCTAssertEqual(descriptor.package, "foo.bar") - XCTAssertEqual(descriptor.service, "Baz") - XCTAssertEqual(descriptor.fullyQualifiedService, "foo.bar.Baz") +@Suite +struct ServiceDescriptorTests { + @Test( + "Decompose fully qualified service name", + arguments: [ + ("foo.bar.baz", "foo.bar", "baz"), + ("foo.bar", "foo", "bar"), + ("foo", "", "foo"), + ("..", ".", ""), + (".", "", ""), + ("", "", ""), + ] + ) + func packageAndService(fullyQualified: String, package: String, service: String) { + let descriptor = ServiceDescriptor(fullyQualifiedService: fullyQualified) + #expect(descriptor.fullyQualifiedService == fullyQualified) + #expect(descriptor.package == package) + #expect(descriptor.service == service) } } diff --git a/Tests/GRPCCoreTests/Test Utilities/Services/BinaryEcho.swift b/Tests/GRPCCoreTests/Test Utilities/Services/BinaryEcho.swift index 8d0ece3c7..a0043eda3 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Services/BinaryEcho.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Services/BinaryEcho.swift @@ -98,19 +98,19 @@ struct BinaryEcho: RegistrableRPCService { enum Methods { static let get = MethodDescriptor( - service: BinaryEcho.serviceDescriptor.fullyQualifiedService, + fullyQualifiedService: BinaryEcho.serviceDescriptor.fullyQualifiedService, method: "Get" ) static let collect = MethodDescriptor( - service: BinaryEcho.serviceDescriptor.fullyQualifiedService, + fullyQualifiedService: BinaryEcho.serviceDescriptor.fullyQualifiedService, method: "Collect" ) static let expand = MethodDescriptor( - service: BinaryEcho.serviceDescriptor.fullyQualifiedService, + fullyQualifiedService: BinaryEcho.serviceDescriptor.fullyQualifiedService, method: "Expand" ) static let update = MethodDescriptor( - service: BinaryEcho.serviceDescriptor.fullyQualifiedService, + fullyQualifiedService: BinaryEcho.serviceDescriptor.fullyQualifiedService, method: "Update" ) } diff --git a/Tests/GRPCCoreTests/Test Utilities/Services/HelloWorld.swift b/Tests/GRPCCoreTests/Test Utilities/Services/HelloWorld.swift index 01501e0bb..a464cec02 100644 --- a/Tests/GRPCCoreTests/Test Utilities/Services/HelloWorld.swift +++ b/Tests/GRPCCoreTests/Test Utilities/Services/HelloWorld.swift @@ -42,7 +42,7 @@ struct HelloWorld: RegistrableRPCService { enum Methods { static let sayHello = MethodDescriptor( - service: HelloWorld.serviceDescriptor.fullyQualifiedService, + fullyQualifiedService: HelloWorld.serviceDescriptor.fullyQualifiedService, method: "SayHello" ) } diff --git a/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift b/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift index 16b915f41..36ef7b84a 100644 --- a/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift +++ b/Tests/GRPCInProcessTransportTests/InProcessClientTransportTests.swift @@ -110,10 +110,7 @@ final class InProcessClientTransportTests: XCTestCase { try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { - try await client.withStream( - descriptor: .init(service: "test", method: "test"), - options: .defaults - ) { _ in + try await client.withStream(descriptor: .testTest, options: .defaults) { _ in // Once the pending stream is opened, close the client to new connections, // so that, once this closure is executed and this stream is closed, // the client will return from `connect()`. @@ -138,10 +135,7 @@ final class InProcessClientTransportTests: XCTestCase { client.beginGracefulShutdown() await XCTAssertThrowsErrorAsync(ofType: RPCError.self) { - try await client.withStream( - descriptor: .init(service: "test", method: "test"), - options: .defaults - ) { _ in } + try await client.withStream(descriptor: .testTest, options: .defaults) { _ in } } errorHandler: { error in XCTAssertEqual(error.code, .failedPrecondition) } @@ -157,10 +151,7 @@ final class InProcessClientTransportTests: XCTestCase { } group.addTask { - try await client.withStream( - descriptor: .init(service: "test", method: "test"), - options: .defaults - ) { stream in + try await client.withStream(descriptor: .testTest, options: .defaults) { stream in try await stream.outbound.write(.message([1])) await stream.outbound.finish() let receivedMessages = try await stream.inbound.reduce(into: []) { $0.append($1) } @@ -212,7 +203,7 @@ final class InProcessClientTransportTests: XCTestCase { serviceConfig: serviceConfig ) - let firstDescriptor = MethodDescriptor(service: "test", method: "first") + let firstDescriptor = MethodDescriptor(fullyQualifiedService: "test", method: "first") XCTAssertEqual( client.config(forMethod: firstDescriptor), serviceConfig.methodConfig.first @@ -236,7 +227,7 @@ final class InProcessClientTransportTests: XCTestCase { serviceConfig: serviceConfig ) - let secondDescriptor = MethodDescriptor(service: "test", method: "second") + let secondDescriptor = MethodDescriptor(fullyQualifiedService: "test", method: "second") XCTAssertEqual( client.config(forMethod: firstDescriptor), serviceConfig.methodConfig.first @@ -257,19 +248,13 @@ final class InProcessClientTransportTests: XCTestCase { } group.addTask { - try await client.withStream( - descriptor: .init(service: "test", method: "test"), - options: .defaults - ) { stream in + try await client.withStream(descriptor: .testTest, options: .defaults) { stream in try await Task.sleep(for: .milliseconds(100)) } } group.addTask { - try await client.withStream( - descriptor: .init(service: "test", method: "test"), - options: .defaults - ) { stream in + try await client.withStream(descriptor: .testTest, options: .defaults) { stream in try await Task.sleep(for: .milliseconds(100)) } } @@ -309,3 +294,7 @@ final class InProcessClientTransportTests: XCTestCase { ) } } + +extension MethodDescriptor { + static let testTest = Self(fullyQualifiedService: "test", method: "test") +} diff --git a/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift b/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift index 45baa66e4..9ecfc4a14 100644 --- a/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift +++ b/Tests/GRPCInProcessTransportTests/InProcessServerTransportTests.swift @@ -28,7 +28,7 @@ final class InProcessServerTransportTests: XCTestCase { RPCAsyncSequence, RPCWriter.Closable >( - descriptor: .init(service: "testService", method: "testMethod"), + descriptor: .testTest, inbound: RPCAsyncSequence( wrapping: AsyncThrowingStream { $0.yield(.message([42])) @@ -59,7 +59,7 @@ final class InProcessServerTransportTests: XCTestCase { let firstStream = RPCStream< RPCAsyncSequence, RPCWriter.Closable >( - descriptor: .init(service: "testService1", method: "testMethod1"), + descriptor: .testTest, inbound: RPCAsyncSequence( wrapping: AsyncThrowingStream { $0.yield(.message([42])) @@ -83,7 +83,7 @@ final class InProcessServerTransportTests: XCTestCase { let secondStream = RPCStream< RPCAsyncSequence, RPCWriter.Closable >( - descriptor: .init(service: "testService1", method: "testMethod1"), + descriptor: .testTest, inbound: RPCAsyncSequence( wrapping: AsyncThrowingStream { $0.yield(.message([42])) diff --git a/Tests/GRPCInProcessTransportTests/InProcessTransportTests.swift b/Tests/GRPCInProcessTransportTests/InProcessTransportTests.swift index 3396b259f..11edbfd6c 100644 --- a/Tests/GRPCInProcessTransportTests/InProcessTransportTests.swift +++ b/Tests/GRPCInProcessTransportTests/InProcessTransportTests.swift @@ -109,7 +109,10 @@ private struct TestService: RegistrableRPCService { } extension MethodDescriptor { - fileprivate static let testCancellation = Self(service: "test", method: "cancellation") + fileprivate static let testCancellation = Self( + fullyQualifiedService: "test", + method: "cancellation" + ) } private struct UTF8Serializer: MessageSerializer { From 3d109681e602f0f5986f3df1b90acb6e6ec4885d Mon Sep 17 00:00:00 2001 From: George Barnett Date: Thu, 21 Nov 2024 13:40:53 +0000 Subject: [PATCH 2/2] Add custom string convertible --- Sources/GRPCCore/MethodDescriptor.swift | 6 ++++++ Sources/GRPCCore/ServiceDescriptor.swift | 6 ++++++ Tests/GRPCCoreTests/MethodDescriptorTests.swift | 10 ++++++++++ Tests/GRPCCoreTests/ServiceDescriptorTests.swift | 6 ++++++ 4 files changed, 28 insertions(+) diff --git a/Sources/GRPCCore/MethodDescriptor.swift b/Sources/GRPCCore/MethodDescriptor.swift index dc4270e70..3cfc500c1 100644 --- a/Sources/GRPCCore/MethodDescriptor.swift +++ b/Sources/GRPCCore/MethodDescriptor.swift @@ -63,3 +63,9 @@ public struct MethodDescriptor: Sendable, Hashable { self.init(fullyQualifiedService: service, method: method) } } + +extension MethodDescriptor: CustomStringConvertible { + public var description: String { + self.fullyQualifiedMethod + } +} diff --git a/Sources/GRPCCore/ServiceDescriptor.swift b/Sources/GRPCCore/ServiceDescriptor.swift index 861eec14d..3a9709d83 100644 --- a/Sources/GRPCCore/ServiceDescriptor.swift +++ b/Sources/GRPCCore/ServiceDescriptor.swift @@ -59,3 +59,9 @@ public struct ServiceDescriptor: Sendable, Hashable { } } } + +extension ServiceDescriptor: CustomStringConvertible { + public var description: String { + self.fullyQualifiedService + } +} diff --git a/Tests/GRPCCoreTests/MethodDescriptorTests.swift b/Tests/GRPCCoreTests/MethodDescriptorTests.swift index 98740aa28..889a0c878 100644 --- a/Tests/GRPCCoreTests/MethodDescriptorTests.swift +++ b/Tests/GRPCCoreTests/MethodDescriptorTests.swift @@ -25,4 +25,14 @@ struct MethodDescriptorTests { #expect(descriptor.method == "Baz") #expect(descriptor.fullyQualifiedMethod == "foo.bar/Baz") } + + @Test("CustomStringConvertible") + func description() { + let descriptor = MethodDescriptor( + service: ServiceDescriptor(fullyQualifiedService: "foo.Foo"), + method: "Bar" + ) + + #expect(String(describing: descriptor) == "foo.Foo/Bar") + } } diff --git a/Tests/GRPCCoreTests/ServiceDescriptorTests.swift b/Tests/GRPCCoreTests/ServiceDescriptorTests.swift index 7124ae058..ef4ec8988 100644 --- a/Tests/GRPCCoreTests/ServiceDescriptorTests.swift +++ b/Tests/GRPCCoreTests/ServiceDescriptorTests.swift @@ -35,4 +35,10 @@ struct ServiceDescriptorTests { #expect(descriptor.package == package) #expect(descriptor.service == service) } + + @Test("CustomStringConvertible") + func description() { + let descriptor = ServiceDescriptor(fullyQualifiedService: "foo.Foo") + #expect(String(describing: descriptor) == "foo.Foo") + } }