Skip to content

Commit f8cd411

Browse files
Add predicate-based ConditionalInterceptor.Subject and sugar syntax (#15)
Add predicate and sugar syntax subjects for ConditionalInterceptor
1 parent 289fbe9 commit f8cd411

File tree

3 files changed

+123
-24
lines changed

3 files changed

+123
-24
lines changed

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ let dependencies: [Package.Dependency] = [
5050

5151
// This adds some build settings which allow us to map "@available(gRPCSwift 2.x, *)" to
5252
// the appropriate OS platforms.
53-
let nextMinorVersion = 1
53+
let nextMinorVersion = 2
5454
let availabilitySettings: [SwiftSetting] = (0 ... nextMinorVersion).map { minor in
5555
let name = "gRPCSwift"
5656
let version = "2.\(minor)"

Sources/GRPCCore/Call/ConditionalInterceptor.swift

Lines changed: 66 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,51 +19,96 @@
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 (_ descriptor: 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 { _ 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 { 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 { 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
59-
60-
case .services(let services):
61-
return services.contains(descriptor.service)
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+
@available(gRPCSwift 2.2, *)
66+
public static func only(
67+
services: Set<ServiceDescriptor>,
68+
methods: Set<MethodDescriptor>
69+
) -> Self {
70+
Self { descriptor in
71+
services.contains(descriptor.service) || methods.contains(descriptor)
72+
}
73+
}
6274

63-
case .methods(let methods):
64-
return methods.contains(descriptor)
75+
/// 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.
76+
///
77+
/// - Parameters:
78+
/// - services: The list of service descriptors for which this interceptor should **not** intercept RPCs.
79+
/// - methods: The list of method descriptors for which this interceptor should **not** intercept RPCs.
80+
@available(gRPCSwift 2.2, *)
81+
public static func allExcluding(
82+
services: Set<ServiceDescriptor>,
83+
methods: Set<MethodDescriptor>
84+
) -> Self {
85+
Self { descriptor in
86+
!(services.contains(descriptor.service) || methods.contains(descriptor))
6587
}
6688
}
89+
90+
/// An operation subject specifying an interceptor that will be applied to RPCs whose ``MethodDescriptor`` satisfies a `predicate`.
91+
///
92+
/// - Important: The result of `predicate` is **cached per `MethodDescriptor`** by the client.
93+
/// The predicate is evaluated the first time a given method is encountered, and that result
94+
/// is reused for subsequent RPCs of the same method for the lifetime of the client.
95+
/// As a consequence, the `predicate` closure should be **deterministic**.
96+
/// Do **not** base it on dynamic state (time, session, feature flags, etc.).
97+
/// If you need per-call decisions, put that logic inside the interceptor itself.
98+
///
99+
/// - Parameters:
100+
/// - predicate: A `@Sendable` closure evaluated per RPC. Return `true` to intercept.
101+
@available(gRPCSwift 2.2, *)
102+
public static func allMatching(
103+
_ predicate: @Sendable @escaping (_ descriptor: MethodDescriptor) -> Bool
104+
) -> Self {
105+
Self(predicate: predicate)
106+
}
107+
108+
@usableFromInline
109+
package func applies(to descriptor: MethodDescriptor) -> Bool {
110+
self.predicate(descriptor)
111+
}
67112
}
68113

69114
/// The interceptor.

Tests/GRPCCoreTests/Call/ConditionalInterceptorTests.swift

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import Testing
2020
@Suite("ConditionalInterceptor")
2121
struct ConditionalInterceptorTests {
2222
@Test(
23-
"Applies to",
23+
"Applies to all, services and methods",
2424
arguments: [
2525
(
2626
.all,
@@ -53,12 +53,66 @@ struct ConditionalInterceptorTests {
5353
#expect(!target.applies(to: notApplicableMethod))
5454
}
5555
}
56+
57+
@Test(
58+
"Applies to only and allExcluding",
59+
arguments: [
60+
(
61+
.only(services: [.foo], methods: [.barFoo]),
62+
[.fooBar, .fooBaz, .barFoo],
63+
[.barBaz]
64+
),
65+
(
66+
.allExcluding(services: [.foo], methods: [.barFoo]),
67+
[.barBaz],
68+
[.fooBar, .fooBaz, .barFoo]
69+
),
70+
] as [(ConditionalInterceptor<any Sendable>.Subject, [MethodDescriptor], [MethodDescriptor])]
71+
)
72+
@available(gRPCSwift 2.2, *)
73+
func appliesToOnlyAndAllExcluding(
74+
target: ConditionalInterceptor<any Sendable>.Subject,
75+
applicableMethods: [MethodDescriptor],
76+
notApplicableMethods: [MethodDescriptor]
77+
) {
78+
for applicableMethod in applicableMethods {
79+
#expect(target.applies(to: applicableMethod))
80+
}
81+
82+
for notApplicableMethod in notApplicableMethods {
83+
#expect(!target.applies(to: notApplicableMethod))
84+
}
85+
}
86+
87+
@Test("Applies to all matching")
88+
@available(gRPCSwift 2.2, *)
89+
func appliesToAllMatching() {
90+
let target = ConditionalInterceptor<any Sendable>.Subject.allMatching { descriptor in
91+
descriptor.method == "baz"
92+
}
93+
let applicableMethods: [MethodDescriptor] = [.fooBaz, .barBaz]
94+
let notApplicableMethods: [MethodDescriptor] = [.fooBar, .barFoo]
95+
96+
for applicableMethod in applicableMethods {
97+
#expect(target.applies(to: applicableMethod))
98+
}
99+
100+
for notApplicableMethod in notApplicableMethods {
101+
#expect(!target.applies(to: notApplicableMethod))
102+
}
103+
}
104+
}
105+
106+
@available(gRPCSwift 2.2, *)
107+
extension ServiceDescriptor {
108+
fileprivate static let foo = Self(fullyQualifiedService: "pkg.foo")
109+
fileprivate static let bar = Self(fullyQualifiedService: "pkg.bar")
56110
}
57111

58112
@available(gRPCSwift 2.0, *)
59113
extension MethodDescriptor {
60114
fileprivate static let fooBar = Self(fullyQualifiedService: "pkg.foo", method: "bar")
61115
fileprivate static let fooBaz = Self(fullyQualifiedService: "pkg.foo", method: "baz")
62116
fileprivate static let barFoo = Self(fullyQualifiedService: "pkg.bar", method: "foo")
63-
fileprivate static let barBaz = Self(fullyQualifiedService: "pkg.bar", method: "Baz")
117+
fileprivate static let barBaz = Self(fullyQualifiedService: "pkg.bar", method: "baz")
64118
}

0 commit comments

Comments
 (0)