Skip to content

Commit 8adce9f

Browse files
feat: add predicate and sugar syntax subjects for ConditionalInterceptor
1 parent 15ee677 commit 8adce9f

File tree

2 files changed

+85
-22
lines changed

2 files changed

+85
-22
lines changed

Sources/GRPCCore/Call/ConditionalInterceptor.swift

Lines changed: 63 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,50 +19,92 @@
1919
/// You can configure interceptors to be applied to:
2020
/// - all RPCs and services;
2121
/// - requests directed only to specific services; or
22-
/// - requests directed only to specific methods (of a specific service).
22+
/// - requests directed only to specific methods (of a specific service); or
23+
/// - requests directed only to specific services or methods (of a specific service); or
24+
/// - all requests excluding requests directed to specific services or methods (of a specific service); or
25+
/// - requests whose ``MethodDescriptor`` satisfies a predicate.
2326
///
2427
/// - SeeAlso: ``ClientInterceptor`` and ``ServerInterceptor`` for more information on client and
2528
/// server interceptors, respectively.
2629
@available(gRPCSwift 2.0, *)
2730
public struct ConditionalInterceptor<Interceptor: Sendable>: Sendable {
2831
public struct Subject: Sendable {
29-
internal enum Wrapped: Sendable {
30-
case all
31-
case services(Set<ServiceDescriptor>)
32-
case methods(Set<MethodDescriptor>)
33-
}
3432

35-
private let wrapped: Wrapped
33+
private let predicate: @Sendable (MethodDescriptor) -> Bool
3634

3735
/// An operation subject specifying an interceptor that applies to all RPCs across all services will be registered with this client.
38-
public static var all: Self { .init(wrapped: .all) }
36+
public static var all: Self {
37+
Self(predicate: { _ in true })
38+
}
3939

4040
/// An operation subject specifying an interceptor that will be applied only to RPCs directed to the specified services.
41+
///
4142
/// - Parameters:
42-
/// - services: The list of service names for which this interceptor should intercept RPCs.
43+
/// - services: The list of service descriptors for which this interceptor should intercept RPCs.
4344
public static func services(_ services: Set<ServiceDescriptor>) -> Self {
44-
Self(wrapped: .services(services))
45+
Self(predicate: { descriptor in
46+
services.contains(descriptor.service)
47+
})
4548
}
4649

4750
/// An operation subject specifying an interceptor that will be applied only to RPCs directed to the specified service methods.
51+
///
4852
/// - Parameters:
4953
/// - methods: The list of method descriptors for which this interceptor should intercept RPCs.
5054
public static func methods(_ methods: Set<MethodDescriptor>) -> Self {
51-
Self(wrapped: .methods(methods))
55+
Self(predicate: { descriptor in
56+
methods.contains(descriptor)
57+
})
5258
}
5359

54-
@usableFromInline
55-
package func applies(to descriptor: MethodDescriptor) -> Bool {
56-
switch self.wrapped {
57-
case .all:
58-
return true
60+
/// An operation subject specifying an interceptor that will be applied only to RPCs directed to the specified services or service methods.
61+
///
62+
/// - Parameters:
63+
/// - services: The list of service descriptors for which this interceptor should intercept RPCs.
64+
/// - methods: The list of method descriptors for which this interceptor should intercept RPCs.
65+
public static func only(
66+
services: Set<ServiceDescriptor>,
67+
methods: Set<MethodDescriptor>
68+
) -> Self {
69+
Self(predicate: { descriptor in
70+
services.contains(descriptor.service) || methods.contains(descriptor)
71+
})
72+
}
5973

60-
case .services(let services):
61-
return services.contains(descriptor.service)
74+
/// An operation subject specifying an interceptor that will be applied to all RPCs across all services for this client excluding the specified services or service methods.
75+
///
76+
/// - Parameters:
77+
/// - services: The list of service descriptors for which this interceptor should **not** intercept RPCs.
78+
/// - methods: The list of method descriptors for which this interceptor should **not** intercept RPCs.
79+
public static func allExcluding(
80+
services: Set<ServiceDescriptor>,
81+
methods: Set<MethodDescriptor>
82+
) -> Self {
83+
Self(predicate: { descriptor in
84+
!(services.contains(descriptor.service) || methods.contains(descriptor))
85+
})
86+
}
6287

63-
case .methods(let methods):
64-
return methods.contains(descriptor)
65-
}
88+
/// An operation subject specifying an interceptor that will be applied to RPCs whose ``MethodDescriptor`` satisfies a `predicate`.
89+
///
90+
/// - Important: The result of `predicate` is **cached per `MethodDescriptor`** by the client.
91+
/// The predicate is evaluated the first time a given method is encountered, and that result
92+
/// is reused for subsequent RPCs of the same method for the lifetime of the client.
93+
/// As a consequence, the `predicate` closure should be **deterministic**.
94+
/// Do **not** base it on dynamic state (time, session, feature flags, etc.).
95+
/// If you need per-call decisions, put that logic inside the interceptor itself.
96+
///
97+
/// - Parameters:
98+
/// - predicate: A `@Sendable` closure evaluated per RPC. Return `true` to intercept.
99+
public static func allMatching(
100+
_ predicate: @Sendable @escaping (MethodDescriptor) -> Bool
101+
) -> Self {
102+
Self(predicate: predicate)
103+
}
104+
105+
@usableFromInline
106+
package func applies(to descriptor: MethodDescriptor) -> Bool {
107+
self.predicate(descriptor)
66108
}
67109
}
68110

Tests/GRPCCoreTests/Call/ConditionalInterceptorTests.swift

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,21 @@ struct ConditionalInterceptorTests {
3737
[.barFoo],
3838
[.fooBar, .fooBaz, .barBaz]
3939
),
40+
(
41+
.only(services: [.foo], methods: [.barFoo]),
42+
[.fooBar, .fooBaz, .barFoo],
43+
[.barBaz]
44+
),
45+
(
46+
.allExcluding(services: [.foo], methods: [.barFoo]),
47+
[.barBaz],
48+
[.fooBar, .fooBaz, .barFoo]
49+
),
50+
(
51+
.allMatching({ methodDescriptor in methodDescriptor.method == "baz" }),
52+
[.fooBaz, .barBaz],
53+
[.fooBar, .barFoo]
54+
),
4055
] as [(ConditionalInterceptor<any Sendable>.Subject, [MethodDescriptor], [MethodDescriptor])]
4156
)
4257
@available(gRPCSwift 2.0, *)
@@ -55,10 +70,16 @@ struct ConditionalInterceptorTests {
5570
}
5671
}
5772

73+
@available(gRPCSwift 2.0, *)
74+
extension ServiceDescriptor {
75+
fileprivate static let foo = Self(fullyQualifiedService: "pkg.foo")
76+
fileprivate static let bar = Self(fullyQualifiedService: "pkg.bar")
77+
}
78+
5879
@available(gRPCSwift 2.0, *)
5980
extension MethodDescriptor {
6081
fileprivate static let fooBar = Self(fullyQualifiedService: "pkg.foo", method: "bar")
6182
fileprivate static let fooBaz = Self(fullyQualifiedService: "pkg.foo", method: "baz")
6283
fileprivate static let barFoo = Self(fullyQualifiedService: "pkg.bar", method: "foo")
63-
fileprivate static let barBaz = Self(fullyQualifiedService: "pkg.bar", method: "Baz")
84+
fileprivate static let barBaz = Self(fullyQualifiedService: "pkg.bar", method: "baz")
6485
}

0 commit comments

Comments
 (0)