Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ let dependencies: [Package.Dependency] = [

// This adds some build settings which allow us to map "@available(gRPCSwift 2.x, *)" to
// the appropriate OS platforms.
let nextMinorVersion = 1
let nextMinorVersion = 2
let availabilitySettings: [SwiftSetting] = (0 ... nextMinorVersion).map { minor in
let name = "gRPCSwift"
let version = "2.\(minor)"
Expand Down
87 changes: 66 additions & 21 deletions Sources/GRPCCore/Call/ConditionalInterceptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,51 +19,96 @@
/// You can configure interceptors to be applied to:
/// - all RPCs and services;
/// - requests directed only to specific services; or
/// - requests directed only to specific methods (of a specific service).
/// - requests directed only to specific methods (of a specific service); or
/// - requests directed only to specific services or methods (of a specific service); or
/// - all requests excluding requests directed to specific services or methods (of a specific service); or
/// - requests whose ``MethodDescriptor`` satisfies a predicate.
///
/// - SeeAlso: ``ClientInterceptor`` and ``ServerInterceptor`` for more information on client and
/// server interceptors, respectively.
@available(gRPCSwift 2.0, *)
public struct ConditionalInterceptor<Interceptor: Sendable>: Sendable {
public struct Subject: Sendable {
internal enum Wrapped: Sendable {
case all
case services(Set<ServiceDescriptor>)
case methods(Set<MethodDescriptor>)
}

private let wrapped: Wrapped
private let predicate: @Sendable (_ descriptor: MethodDescriptor) -> Bool

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

/// An operation subject specifying an interceptor that will be applied only to RPCs directed to the specified services.
///
/// - Parameters:
/// - services: The list of service names for which this interceptor should intercept RPCs.
/// - services: The list of service descriptors for which this interceptor should intercept RPCs.
public static func services(_ services: Set<ServiceDescriptor>) -> Self {
Self(wrapped: .services(services))
Self { descriptor in
services.contains(descriptor.service)
}
}

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

@usableFromInline
package func applies(to descriptor: MethodDescriptor) -> Bool {
switch self.wrapped {
case .all:
return true

case .services(let services):
return services.contains(descriptor.service)
/// An operation subject specifying an interceptor that will be applied only to RPCs directed to the specified services or service methods.
///
/// - Parameters:
/// - services: The list of service descriptors for which this interceptor should intercept RPCs.
/// - methods: The list of method descriptors for which this interceptor should intercept RPCs.
@available(gRPCSwift 2.2, *)
public static func only(
services: Set<ServiceDescriptor>,
methods: Set<MethodDescriptor>
) -> Self {
Self { descriptor in
services.contains(descriptor.service) || methods.contains(descriptor)
}
}

case .methods(let methods):
return methods.contains(descriptor)
/// 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.
///
/// - Parameters:
/// - services: The list of service descriptors for which this interceptor should **not** intercept RPCs.
/// - methods: The list of method descriptors for which this interceptor should **not** intercept RPCs.
@available(gRPCSwift 2.2, *)
public static func allExcluding(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you annotate this with @available(gRPCSwift 2.2, *)?

services: Set<ServiceDescriptor>,
methods: Set<MethodDescriptor>
) -> Self {
Self { descriptor in
!(services.contains(descriptor.service) || methods.contains(descriptor))
}
}

/// An operation subject specifying an interceptor that will be applied to RPCs whose ``MethodDescriptor`` satisfies a `predicate`.
///
/// - Important: The result of `predicate` is **cached per `MethodDescriptor`** by the client.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice note 👍

/// The predicate is evaluated the first time a given method is encountered, and that result
/// is reused for subsequent RPCs of the same method for the lifetime of the client.
/// As a consequence, the `predicate` closure should be **deterministic**.
/// Do **not** base it on dynamic state (time, session, feature flags, etc.).
/// If you need per-call decisions, put that logic inside the interceptor itself.
///
/// - Parameters:
/// - predicate: A `@Sendable` closure evaluated per RPC. Return `true` to intercept.
@available(gRPCSwift 2.2, *)
public static func allMatching(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you annotate this with @available(gRPCSwift 2.2, *)?

_ predicate: @Sendable @escaping (_ descriptor: MethodDescriptor) -> Bool
) -> Self {
Self(predicate: predicate)
}

@usableFromInline
package func applies(to descriptor: MethodDescriptor) -> Bool {
self.predicate(descriptor)
}
}

/// The interceptor.
Expand Down
58 changes: 56 additions & 2 deletions Tests/GRPCCoreTests/Call/ConditionalInterceptorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import Testing
@Suite("ConditionalInterceptor")
struct ConditionalInterceptorTests {
@Test(
"Applies to",
"Applies to all, services and methods",
arguments: [
(
.all,
Expand Down Expand Up @@ -53,12 +53,66 @@ struct ConditionalInterceptorTests {
#expect(!target.applies(to: notApplicableMethod))
}
}

@Test(
"Applies to only and allExcluding",
arguments: [
(
.only(services: [.foo], methods: [.barFoo]),
[.fooBar, .fooBaz, .barFoo],
[.barBaz]
),
(
.allExcluding(services: [.foo], methods: [.barFoo]),
[.barBaz],
[.fooBar, .fooBaz, .barFoo]
),
] as [(ConditionalInterceptor<any Sendable>.Subject, [MethodDescriptor], [MethodDescriptor])]
)
@available(gRPCSwift 2.2, *)
func appliesToOnlyAndAllExcluding(
target: ConditionalInterceptor<any Sendable>.Subject,
applicableMethods: [MethodDescriptor],
notApplicableMethods: [MethodDescriptor]
) {
for applicableMethod in applicableMethods {
#expect(target.applies(to: applicableMethod))
}

for notApplicableMethod in notApplicableMethods {
#expect(!target.applies(to: notApplicableMethod))
}
}

@Test("Applies to all matching")
@available(gRPCSwift 2.2, *)
func appliesToAllMatching() {
let target = ConditionalInterceptor<any Sendable>.Subject.allMatching { descriptor in
descriptor.method == "baz"
}
let applicableMethods: [MethodDescriptor] = [.fooBaz, .barBaz]
let notApplicableMethods: [MethodDescriptor] = [.fooBar, .barFoo]

for applicableMethod in applicableMethods {
#expect(target.applies(to: applicableMethod))
}

for notApplicableMethod in notApplicableMethods {
#expect(!target.applies(to: notApplicableMethod))
}
}
}

@available(gRPCSwift 2.2, *)
extension ServiceDescriptor {
fileprivate static let foo = Self(fullyQualifiedService: "pkg.foo")
fileprivate static let bar = Self(fullyQualifiedService: "pkg.bar")
}

@available(gRPCSwift 2.0, *)
extension MethodDescriptor {
fileprivate static let fooBar = Self(fullyQualifiedService: "pkg.foo", method: "bar")
fileprivate static let fooBaz = Self(fullyQualifiedService: "pkg.foo", method: "baz")
fileprivate static let barFoo = Self(fullyQualifiedService: "pkg.bar", method: "foo")
fileprivate static let barBaz = Self(fullyQualifiedService: "pkg.bar", method: "Baz")
fileprivate static let barBaz = Self(fullyQualifiedService: "pkg.bar", method: "baz")
}