Skip to content

Commit 5ec3bbb

Browse files
committed
Allow adding ClientInterceptors to specific services and methods
1 parent c3f09df commit 5ec3bbb

File tree

6 files changed

+313
-18
lines changed

6 files changed

+313
-18
lines changed

Sources/GRPCCore/Call/Client/ClientInterceptor.swift

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@
2121
/// received from the transport. They are typically used for cross-cutting concerns like injecting
2222
/// metadata, validating messages, logging additional data, and tracing.
2323
///
24-
/// Interceptors are registered with a client and apply to all RPCs. If you need to modify the
25-
/// behavior of an interceptor on a per-RPC basis then you can use the
26-
/// ``ClientContext/descriptor`` to determine which RPC is being called and
27-
/// conditionalise behavior accordingly.
24+
/// Interceptors are registered with the server via ``ClientInterceptorPipelineOperation``s.
25+
/// You may register them for all services registered with a server, for RPCs directed to specific services, or
26+
/// for RPCs directed to specific methods. If you need to modify the behavior of an interceptor on a
27+
/// per-RPC basis in more detail, then you can use the ``ClientContext/descriptor`` to determine
28+
/// which RPC is being called and conditionalise behavior accordingly.
2829
///
2930
/// - TODO: Update example and documentation to show how to register an interceptor.
3031
///
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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+
/// A `ClientInterceptorPipelineOperation` describes to which RPCs a client interceptor should be applied.
18+
///
19+
/// You can configure a client interceptor to be applied to:
20+
/// - all RPCs and services;
21+
/// - requests directed only to specific services; or
22+
/// - requests directed only to specific methods (of a specific service).
23+
///
24+
/// - SeeAlso: ``ClientInterceptor`` for more information on client interceptors, and
25+
/// ``ServerInterceptorPipelineOperation`` for the server-side version of this type.
26+
public struct ClientInterceptorPipelineOperation: Sendable {
27+
/// The subject of a ``ClientInterceptorPipelineOperation``.
28+
/// The subject of an interceptor can either be all services and methods, only specific services, or only specific methods.
29+
public struct Subject: Sendable {
30+
internal enum Wrapped: Sendable {
31+
case all
32+
case services(Set<ServiceDescriptor>)
33+
case methods(Set<MethodDescriptor>)
34+
}
35+
36+
private let wrapped: Wrapped
37+
38+
/// An operation subject specifying an interceptor that applies to all RPCs across all services will be registered with this client.
39+
public static var all: Self { .init(wrapped: .all) }
40+
41+
/// An operation subject specifying an interceptor that will be applied only to RPCs directed to the specified services.
42+
/// - Parameters:
43+
/// - services: The list of service names for which this interceptor should intercept RPCs.
44+
/// - Returns: A ``ClientInterceptorPipelineOperation``.
45+
public static func services(_ services: Set<ServiceDescriptor>) -> Self {
46+
Self(wrapped: .services(services))
47+
}
48+
49+
/// An operation subject specifying an interceptor that will be applied only to RPCs directed to the specified service methods.
50+
/// - Parameters:
51+
/// - methods: The list of method descriptors for which this interceptor should intercept RPCs.
52+
/// - Returns: A ``ClientInterceptorPipelineOperation``.
53+
public static func methods(_ methods: Set<MethodDescriptor>) -> Self {
54+
Self(wrapped: .methods(methods))
55+
}
56+
57+
@usableFromInline
58+
internal func applies(to descriptor: MethodDescriptor) -> Bool {
59+
switch self.wrapped {
60+
case .all:
61+
return true
62+
63+
case .services(let services):
64+
return services.map({ $0.fullyQualifiedService }).contains(descriptor.service)
65+
66+
case .methods(let methods):
67+
return methods.contains(descriptor)
68+
}
69+
}
70+
}
71+
72+
/// The interceptor specified for this operation.
73+
public let interceptor: any ClientInterceptor
74+
75+
@usableFromInline
76+
internal let subject: Subject
77+
78+
private init(interceptor: any ClientInterceptor, appliesTo: Subject) {
79+
self.interceptor = interceptor
80+
self.subject = appliesTo
81+
}
82+
83+
/// Create an operation, specifying which ``ClientInterceptor`` to apply and to which ``Subject``.
84+
/// - Parameters:
85+
/// - interceptor: The ``ClientInterceptor`` to register with the client.
86+
/// - subject: The ``Subject`` to which the `interceptor` applies.
87+
/// - Returns: A ``ClientInterceptorPipelineOperation``.
88+
public static func apply(_ interceptor: any ClientInterceptor, to subject: Subject) -> Self {
89+
Self(interceptor: interceptor, appliesTo: subject)
90+
}
91+
92+
/// Returns whether this ``ClientInterceptorPipelineOperation`` applies to the given `descriptor`.
93+
/// - Parameter descriptor: A ``MethodDescriptor`` for which to test whether this interceptor applies.
94+
/// - Returns: `true` if this interceptor applies to the given `descriptor`, or `false` otherwise.
95+
@inlinable
96+
internal func applies(to descriptor: MethodDescriptor) -> Bool {
97+
self.subject.applies(to: descriptor)
98+
}
99+
}

Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ struct ServerRPCExecutor {
2323
/// - stream: The accepted stream to execute the RPC on.
2424
/// - deserializer: A deserializer for messages received from the client.
2525
/// - serializer: A serializer for messages to send to the client.
26-
/// - interceptors: Server interceptors to apply to this RPC.
26+
/// - interceptors: Server interceptors to apply to this RPC. The
27+
/// interceptors will be called in the order of the array.
2728
/// - handler: A handler which turns the request into a response.
2829
@inlinable
2930
static func execute<Input, Output>(

Sources/GRPCCore/Call/Server/ServerInterceptorPipelineOperation.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
/// - requests directed only to specific services registered with your server; or
2222
/// - requests directed only to specific methods (of a specific service).
2323
///
24-
/// - SeeAlso: ``ServerInterceptor`` for more information on server interceptors.
24+
/// - SeeAlso: ``ServerInterceptor`` for more information on server interceptors, and
25+
/// ``ClientInterceptorPipelineOperation`` for the client-side version of this type.
2526
public struct ServerInterceptorPipelineOperation: Sendable {
2627
/// The subject of a ``ServerInterceptorPipelineOperation``.
2728
/// The subject of an interceptor can either be all services and methods, only specific services, or only specific methods.

Sources/GRPCCore/GRPCClient.swift

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -112,13 +112,18 @@ public final class GRPCClient: Sendable {
112112
/// The transport which provides a bidirectional communication channel with the server.
113113
private let transport: any ClientTransport
114114

115-
/// A collection of interceptors providing cross-cutting functionality to each accepted RPC.
115+
private let interceptorPipeline: [ClientInterceptorPipelineOperation]
116+
117+
/// A collection of interceptors providing cross-cutting functionality to each accepted RPC, keyed by the method to which they apply.
118+
///
119+
/// The list of interceptors for each method is computed from `interceptorsPipeline` when calling a method for the first time.
120+
/// This caching is done to avoid having to compute the applicable interceptors for each request made.
116121
///
117122
/// The order in which interceptors are added reflects the order in which they are called. The
118123
/// first interceptor added will be the first interceptor to intercept each request. The last
119124
/// interceptor added will be the final interceptor to intercept each request before calling
120125
/// the appropriate handler.
121-
private let interceptors: [any ClientInterceptor]
126+
private let interceptorsPerMethod: Mutex<[MethodDescriptor: [any ClientInterceptor]]>
122127

123128
/// The current state of the client.
124129
private let state: Mutex<State>
@@ -191,17 +196,37 @@ public final class GRPCClient: Sendable {
191196
///
192197
/// - Parameters:
193198
/// - transport: The transport used to establish a communication channel with a server.
194-
/// - interceptors: A collection of interceptors providing cross-cutting functionality to each
199+
/// - interceptors: A collection of ``ClientInterceptor``s providing cross-cutting functionality to each
195200
/// accepted RPC. The order in which interceptors are added reflects the order in which they
196201
/// are called. The first interceptor added will be the first interceptor to intercept each
197202
/// request. The last interceptor added will be the final interceptor to intercept each
198203
/// request before calling the appropriate handler.
199-
public init(
204+
convenience public init(
200205
transport: some ClientTransport,
201206
interceptors: [any ClientInterceptor] = []
207+
) {
208+
self.init(
209+
transport: transport,
210+
interceptorPipeline: interceptors.map { .apply($0, to: .all) }
211+
)
212+
}
213+
214+
/// Creates a new client with the given transport, interceptors and configuration.
215+
///
216+
/// - Parameters:
217+
/// - transport: The transport used to establish a communication channel with a server.
218+
/// - interceptorPipeline: A collection of ``ClientInterceptorPipelineOperation`` providing cross-cutting
219+
/// functionality to each accepted RPC. Only applicable interceptors from the pipeline will be applied to each RPC.
220+
/// The order in which interceptors are added reflects the order in which they are called.
221+
/// The first interceptor added will be the first interceptor to intercept each request.
222+
/// The last interceptor added will be the final interceptor to intercept each request before calling the appropriate handler.
223+
public init(
224+
transport: some ClientTransport,
225+
interceptorPipeline: [ClientInterceptorPipelineOperation]
202226
) {
203227
self.transport = transport
204-
self.interceptors = interceptors
228+
self.interceptorPipeline = interceptorPipeline
229+
self.interceptorsPerMethod = Mutex([:])
205230
self.state = Mutex(.notStarted)
206231
}
207232

@@ -361,14 +386,26 @@ public final class GRPCClient: Sendable {
361386
var options = options
362387
options.formUnion(with: methodConfig)
363388

389+
let applicableInterceptors = self.interceptorsPerMethod.withLock {
390+
if let interceptors = $0[descriptor] {
391+
return interceptors
392+
} else {
393+
let interceptors = self.interceptorPipeline
394+
.filter { $0.applies(to: descriptor) }
395+
.map { $0.interceptor }
396+
$0[descriptor] = interceptors
397+
return interceptors
398+
}
399+
}
400+
364401
return try await ClientRPCExecutor.execute(
365402
request: request,
366403
method: descriptor,
367404
options: options,
368405
serializer: serializer,
369406
deserializer: deserializer,
370407
transport: self.transport,
371-
interceptors: self.interceptors,
408+
interceptors: applicableInterceptors,
372409
handler: handler
373410
)
374411
}

0 commit comments

Comments
 (0)