Skip to content

Conversation

MathieuTricoire
Copy link
Contributor

@MathieuTricoire MathieuTricoire commented Aug 18, 2025

Motivation

When using ConditionalInterceptor.Subject it's currently only possible to target .all, .services, or .methods.
This makes it difficult to express common cases like "apply this interceptor to all methods except a small set" without manually listing every included method or service. For example applying authentication everywhere except health check and authentication services.

Modifications

  • Refactored the internal representation of ConditionalInterceptor.Subject to store a single predicate closure instead of an enum of cases.
    This simplifies the matching mechanism: every variant is now expressed as a predicate over MethodDescriptor.
  • Re-implemented existing subjects (.all, .services, .methods) as convenience "sugar" static functions that construct the appropriate predicate.
  • Added new subjects:
    • .only(services:methods:) — applies only to the given services and/or methods.
    • .allExcluding(services:methods:) — applies to all except the given services and/or methods.
    • .allMatching(_:) — applies to all methods satisfying a user-provided predicate.

Result

Users can now express inclusion/exclusion rules more naturally:

.apply(requestCounterInterceptor, to: .only(services: countedServices, methods: countedMethods))
.apply(credentialsInterceptor, to: .allExcluding(services: [CredentialsService.descriptor], methods: []))
.apply(specialInterceptor, to: .allMatching { $0.method == "special" })

Fixes #14

@MathieuTricoire
Copy link
Contributor Author

Should I add something like @available(*, introduced: 2.2) to the new ConditionalInterceptor.Subject static functions?
Should I set the ServiceDescriptor extension in Tests/GRPCCoreTests/Call/ConditionalInterceptorTests.swift only available for 2.2?

Copy link
Collaborator

@glbrntt glbrntt left a comment

Choose a reason for hiding this comment

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

I left a couple of small notes but otherwise this looks great.

Comment on lines 45 to 47
Self(predicate: { descriptor in
services.contains(descriptor.service)
})
Copy link
Collaborator

Choose a reason for hiding this comment

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

You can use trailing closure syntax here (and in other places):

Suggested change
Self(predicate: { descriptor in
services.contains(descriptor.service)
})
Self { descriptor in
services.contains(descriptor.service)
}

/// - Parameters:
/// - predicate: A `@Sendable` closure evaluated per RPC. Return `true` to intercept.
public static func allMatching(
_ predicate: @Sendable @escaping (MethodDescriptor) -> Bool
Copy link
Collaborator

Choose a reason for hiding this comment

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

The Swift API Design Guidelines recommend naming closure parameters:

Suggested change
_ predicate: @Sendable @escaping (MethodDescriptor) -> Bool
_ predicate: @Sendable @escaping (_ descriptor: MethodDescriptor) -> Bool

This is nice because tooling can take advantage of it to suggest the parameter name.

}
/// 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 👍

/// - 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.
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, *)?

///
/// - Parameters:
/// - predicate: A `@Sendable` closure evaluated per RPC. Return `true` to intercept.
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, *)?

@MathieuTricoire
Copy link
Contributor Author

I updated the PR Result section, the correct API for .allExcluding is:

.apply(credentialsInterceptor, to: .allExcluding(services: [CredentialsService.descriptor], methods: []))

@glbrntt glbrntt added the 🆕 semver/minor Adds new public API. label Aug 18, 2025
Copy link
Collaborator

@glbrntt glbrntt left a comment

Choose a reason for hiding this comment

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

Looks great, thanks very much @MathieuTricoire!

@glbrntt glbrntt enabled auto-merge (squash) August 18, 2025 16:49
auto-merge was automatically disabled August 18, 2025 17:49

Head branch was pushed to by a user without write access

@MathieuTricoire
Copy link
Contributor Author

There was an issue in CI: https://github.com/grpc/grpc-swift-2/actions/runs/17046668344/job/48324929547
I don't really understand why, seems related to closure in @Test? I took the opportunity to change syntax for something more idiomatic. I'll see if I can have a look at it tomorrow.

@glbrntt
Copy link
Collaborator

glbrntt commented Aug 19, 2025

There was an issue in CI: https://github.com/grpc/grpc-swift-2/actions/runs/17046668344/job/48324929547 I don't really understand why, seems related to closure in @Test? I took the opportunity to change syntax for something more idiomatic. I'll see if I can have a look at it tomorrow.

Agh, Swift 6.0 only. I'd try adding type annotations to the closure to see if that helps.

Comment on lines 49 to 70
),
(
.allMatching { (_ descriptor: MethodDescriptor) -> Bool in
descriptor.method == "baz"
},
[.fooBaz, .barBaz],
[.fooBar, .barFoo]
),
] as [(ConditionalInterceptor<any Sendable>.Subject, [MethodDescriptor], [MethodDescriptor])]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Give it's fine on 6.1+ and newer, I think it's fine in this instance to compile out that test case on Swift 6.0:

Suggested change
),
(
.allMatching { (_ descriptor: MethodDescriptor) -> Bool in
descriptor.method == "baz"
},
[.fooBaz, .barBaz],
[.fooBar, .barFoo]
),
] as [(ConditionalInterceptor<any Sendable>.Subject, [MethodDescriptor], [MethodDescriptor])]
),
#if compiler(>=6.1)
(
.allMatching { (_ descriptor: MethodDescriptor) -> Bool in
descriptor.method == "baz"
},
[.fooBaz, .barBaz],
[.fooBar, .barFoo]
),
#endif
] as [(ConditionalInterceptor<any Sendable>.Subject, [MethodDescriptor], [MethodDescriptor])]

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What do you think about moving the allMatching case into its own test? (As it seems the issue is about the closure in @Test)

It also made me notice that the current test is annotated with @available(gRPCSwift 2.0, *) while the new subjects are available from 2.2. I wouldn’t expect that to be the cause of the error, but it does feel odd to test 2.2 APIs inside a test marked 2.0? Or are we ok with that?

Proposal: split the tests by availability, like this (Let me know how you would name the functions and tests):

@Suite("ConditionalInterceptor")
struct ConditionalInterceptorTests {
  @Test(
    "Applies to all, services and methods",
    arguments: [
      (
        .all,
        [.fooBar, .fooBaz, .barFoo, .barBaz],
        []
      ),
      (
        .services([ServiceDescriptor(package: "pkg", service: "foo")]),
        [.fooBar, .fooBaz],
        [.barFoo, .barBaz]
      ),
      (
        .methods([.barFoo]),
        [.barFoo],
        [.fooBar, .fooBaz, .barBaz]
      ),
    ] as [(ConditionalInterceptor<any Sendable>.Subject, [MethodDescriptor], [MethodDescriptor])]
  )
  @available(gRPCSwift 2.0, *)
  func appliesTo(
    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 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))
    }
  }
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm fine with that. The availability in tests doesn't matter much to be honest.

Copy link
Collaborator

Choose a reason for hiding this comment

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

FWIW: the Check generated code (pull_request) job is fixed on main so if you update your branch with the base branch the job should pass.

@glbrntt glbrntt merged commit f8cd411 into grpc:main Aug 19, 2025
41 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🆕 semver/minor Adds new public API.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Negative matching nor predicate support in ConditionalInterceptor.Subject
2 participants