Skip to content

Commit 45b2420

Browse files
Adopt Sendable throughout API to support -strict-concurrency=complete (#22)
### Motivation When using `-strict-concurrency=targeted`, `ClientMiddleware` will emit warnings if an actor is conformed to it: ```swift import OpenAPIRuntime import Foundation actor Middleware: ClientMiddleware { func intercept( _ request: Request, baseURL: URL, operationID: String, /// Need to add `@Sendable` or the warnings don't go away even with this PR. Code-completion won't add it. next: @sendable (Request, URL) async throws -> Response ) async throws -> Response { try await next(request, baseURL) } } ``` The `intercept` function will emit the following warning with the current implementation: > Sendability of function types in instance method 'intercept(_:baseURL:operationID:next:)' does not match requirement in protocol 'ClientMiddleware' Repro package: https://github.com/MahdiBM/OAPIRClientWarningsRepro You can change between this PR and the main branch by commenting out the dependency declarations i've already put in the `Package.swift`. Then observe there are no warnings with this PR, unlike with the main branch. ### Modifications Generally adopt Sendable throughout the whole API to support -strict-concurrency=complete. For the most part, this means using @sendable for closures, and using `any Sendable` instead of `Any`. As an example for closures, currently we have: ```swift public protocol ClientMiddleware: Sendable { func intercept( _ request: Request, baseURL: URL, operationID: String, next: (Request, URL) async throws -> Response ) async throws -> Response } ``` With this PR we'll have: ```swift public protocol ClientMiddleware: Sendable { func intercept( _ request: Request, baseURL: URL, operationID: String, next: @sendable (Request, URL) async throws -> Response /// ~~~^ notice `@Sendable` ) async throws -> Response } ``` ### Result Safer Sendable code + being more ready for Swift 6 + no more warnings when using `-strict-concurrency=targeted` then conforming an actor to `ClientMiddleware`. ### Test Plan Adopted all tests to the changes and added the strict-concurrency flag to CI. --------- Co-authored-by: Si Beaumont <simonjbeaumont@gmail.com> Co-authored-by: Si Beaumont <beaumont@apple.com>
1 parent fbd56ae commit 45b2420

14 files changed

+76
-49
lines changed

Sources/OpenAPIRuntime/Base/OpenAPIValue.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public struct OpenAPIValueContainer: Codable, Equatable, Hashable, Sendable {
5252
/// - Parameter unvalidatedValue: A value of a JSON-compatible type,
5353
/// such as `String`, `[Any]`, and `[String: Any]`.
5454
/// - Throws: When the value is not supported.
55-
public init(unvalidatedValue: Any? = nil) throws {
55+
public init(unvalidatedValue: (any Sendable)? = nil) throws {
5656
try self.init(validatedValue: Self.tryCast(unvalidatedValue))
5757
}
5858

@@ -62,14 +62,14 @@ public struct OpenAPIValueContainer: Codable, Equatable, Hashable, Sendable {
6262
/// - Parameter value: An untyped value.
6363
/// - Returns: A cast value if supported.
6464
/// - Throws: When the value is not supported.
65-
static func tryCast(_ value: Any?) throws -> Sendable? {
65+
static func tryCast(_ value: (any Sendable)?) throws -> Sendable? {
6666
guard let value = value else {
6767
return nil
6868
}
69-
if let array = value as? [Any?] {
69+
if let array = value as? [(any Sendable)?] {
7070
return try array.map(tryCast(_:))
7171
}
72-
if let dictionary = value as? [String: Any?] {
72+
if let dictionary = value as? [String: (any Sendable)?] {
7373
return try dictionary.mapValues(tryCast(_:))
7474
}
7575
if let value = tryCastPrimitiveType(value) {
@@ -87,7 +87,7 @@ public struct OpenAPIValueContainer: Codable, Equatable, Hashable, Sendable {
8787
/// Returns the specified value cast to a supported primitive type.
8888
/// - Parameter value: An untyped value.
8989
/// - Returns: A cast value if supported, nil otherwise.
90-
static func tryCastPrimitiveType(_ value: Any) -> Sendable? {
90+
static func tryCastPrimitiveType(_ value: any Sendable) -> (any Sendable)? {
9191
switch value {
9292
case is String, is Int, is Bool, is Double:
9393
return value
@@ -327,7 +327,7 @@ public struct OpenAPIObjectContainer: Codable, Equatable, Hashable, Sendable {
327327

328328
public func encode(to encoder: Encoder) throws {
329329
var container = encoder.singleValueContainer()
330-
try container.encode(value.mapValues(OpenAPIValueContainer.init))
330+
try container.encode(value.mapValues(OpenAPIValueContainer.init(validatedValue:)))
331331
}
332332

333333
// MARK: Equatable
@@ -430,7 +430,7 @@ public struct OpenAPIArrayContainer: Codable, Equatable, Hashable, Sendable {
430430

431431
public func encode(to encoder: Encoder) throws {
432432
var container = encoder.singleValueContainer()
433-
try container.encode(value.map(OpenAPIValueContainer.init))
433+
try container.encode(value.map(OpenAPIValueContainer.init(validatedValue:)))
434434
}
435435

436436
// MARK: Equatable

Sources/OpenAPIRuntime/Conversion/Converter.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#if canImport(Darwin)
1515
import Foundation
1616
#else
17+
// `@preconcrrency` is for `JSONDecoder`/`JSONEncoder`.
1718
@preconcurrency import Foundation
1819
#endif
1920

Sources/OpenAPIRuntime/Errors/ClientError.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@
1111
// SPDX-License-Identifier: Apache-2.0
1212
//
1313
//===----------------------------------------------------------------------===//
14+
#if canImport(Darwin)
1415
import Foundation
16+
#else
17+
// `@preconcrrency` is for `URL`.
18+
@preconcurrency import Foundation
19+
#endif
1520

1621
/// An error thrown by a client performing an OpenAPI operation.
1722
///
@@ -24,7 +29,7 @@ public struct ClientError: Error {
2429
public var operationID: String
2530

2631
/// The operation-specific Input value.
27-
public var operationInput: Any
32+
public var operationInput: any Sendable
2833

2934
/// The HTTP request created during the operation.
3035
///
@@ -56,7 +61,7 @@ public struct ClientError: Error {
5661
/// - underlyingError: The underlying error that caused the operation to fail.
5762
public init(
5863
operationID: String,
59-
operationInput: Any,
64+
operationInput: any Sendable,
6065
request: Request? = nil,
6166
baseURL: URL? = nil,
6267
response: Response? = nil,

Sources/OpenAPIRuntime/Errors/ServerError.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,12 @@ public struct ServerError: Error {
2727
/// Operation-specific Input value.
2828
///
2929
/// Is nil if error was thrown during request -> Input conversion.
30-
public var operationInput: Any?
30+
public var operationInput: (any Sendable)?
3131

3232
/// Operation-specific Output value.
3333
///
3434
/// Is nil if error was thrown before/during Output -> response conversion.
35-
public var operationOutput: Any?
35+
public var operationOutput: (any Sendable)?
3636

3737
/// The underlying error that caused the operation to fail.
3838
public var underlyingError: Error
@@ -50,8 +50,8 @@ public struct ServerError: Error {
5050
operationID: String,
5151
request: Request,
5252
requestMetadata: ServerRequestMetadata,
53-
operationInput: Any? = nil,
54-
operationOutput: Any? = nil,
53+
operationInput: (any Sendable)? = nil,
54+
operationOutput: (any Sendable)? = nil,
5555
underlyingError: Error
5656
) {
5757
self.operationID = operationID

Sources/OpenAPIRuntime/Interface/ClientTransport.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,6 @@ public protocol ClientMiddleware: Sendable {
233233
_ request: Request,
234234
baseURL: URL,
235235
operationID: String,
236-
next: (Request, URL) async throws -> Response
236+
next: @Sendable (Request, URL) async throws -> Response
237237
) async throws -> Response
238238
}

Sources/OpenAPIRuntime/Interface/CurrencyTypes.swift

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,42 @@
1414
#if canImport(Darwin)
1515
import Foundation
1616
#else
17+
// `@preconcrrency` is for `Data`/`URLQueryItem`.
1718
@preconcurrency import Foundation
1819
#endif
1920

21+
/// A protected-by-locks storage for ``redactedHeaderFields``.
22+
private class RedactedHeadersStorage: @unchecked Sendable {
23+
/// The underlying storage of ``redactedHeaderFields``,
24+
/// protected by a lock.
25+
private var _locked_redactedHeaderFields: Set<String> = HeaderField.defaultRedactedHeaderFields
26+
27+
/// The header fields to be redacted.
28+
var redactedHeaderFields: Set<String> {
29+
get {
30+
lock.lock()
31+
defer {
32+
lock.unlock()
33+
}
34+
return _locked_redactedHeaderFields
35+
}
36+
set {
37+
lock.lock()
38+
defer {
39+
lock.unlock()
40+
}
41+
_locked_redactedHeaderFields = newValue
42+
}
43+
}
44+
45+
/// The lock used for protecting access to `_locked_redactedHeaderFields`.
46+
private let lock: NSLock = {
47+
let lock = NSLock()
48+
lock.name = "com.apple.swift-openapi-runtime.lock.redactedHeaderFields"
49+
return lock
50+
}()
51+
}
52+
2053
/// A header field used in an HTTP request or response.
2154
public struct HeaderField: Equatable, Hashable, Sendable {
2255

@@ -47,20 +80,12 @@ extension HeaderField {
4780
/// Use this to avoid leaking sensitive tokens into application logs.
4881
public static var redactedHeaderFields: Set<String> {
4982
set {
50-
_lock_redactedHeaderFields.lock()
51-
defer {
52-
_lock_redactedHeaderFields.unlock()
53-
}
5483
// Save lowercased versions of the header field names to make
5584
// membership checking O(1).
56-
_locked_redactedHeaderFields = Set(newValue.map { $0.lowercased() })
85+
redactedHeadersStorage.redactedHeaderFields = Set(newValue.map { $0.lowercased() })
5786
}
5887
get {
59-
_lock_redactedHeaderFields.lock()
60-
defer {
61-
_lock_redactedHeaderFields.unlock()
62-
}
63-
return _locked_redactedHeaderFields
88+
return redactedHeadersStorage.redactedHeaderFields
6489
}
6590
}
6691

@@ -71,16 +96,7 @@ extension HeaderField {
7196
"set-cookie",
7297
]
7398

74-
/// The lock used for protecting access to `_locked_redactedHeaderFields`.
75-
private static let _lock_redactedHeaderFields: NSLock = {
76-
let lock = NSLock()
77-
lock.name = "com.apple.swift-openapi-runtime.lock.redactedHeaderFields"
78-
return lock
79-
}()
80-
81-
/// The underlying storage of ``HeaderField/redactedHeaderFields``,
82-
/// protected by a lock.
83-
private static var _locked_redactedHeaderFields: Set<String> = defaultRedactedHeaderFields
99+
private static let redactedHeadersStorage = RedactedHeadersStorage()
84100
}
85101

86102
/// Describes the HTTP method used in an OpenAPI operation.

Sources/OpenAPIRuntime/Interface/ServerTransport.swift

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,7 @@ public protocol ServerTransport {
112112
/// - queryItemNames: The names of query items to be extracted
113113
/// from the request URL that matches the provided HTTP operation.
114114
func register(
115-
_ handler: @Sendable @escaping (
116-
Request, ServerRequestMetadata
117-
) async throws -> Response,
115+
_ handler: @Sendable @escaping (Request, ServerRequestMetadata) async throws -> Response,
118116
method: HTTPMethod,
119117
path: [RouterPathComponent],
120118
queryItemNames: Set<String>
@@ -219,6 +217,6 @@ public protocol ServerMiddleware: Sendable {
219217
_ request: Request,
220218
metadata: ServerRequestMetadata,
221219
operationID: String,
222-
next: (Request, ServerRequestMetadata) async throws -> Response
220+
next: @Sendable (Request, ServerRequestMetadata) async throws -> Response
223221
) async throws -> Response
224222
}

Sources/OpenAPIRuntime/Interface/UniversalClient.swift

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#if canImport(Darwin)
1515
import Foundation
1616
#else
17+
// `@preconcrrency` is for `URL`.
1718
@preconcurrency import Foundation
1819
#endif
1920

@@ -86,9 +87,10 @@ public struct UniversalClient: Sendable {
8687
public func send<OperationInput, OperationOutput>(
8788
input: OperationInput,
8889
forOperation operationID: String,
89-
serializer: (OperationInput) throws -> Request,
90-
deserializer: (Response) throws -> OperationOutput
91-
) async throws -> OperationOutput {
90+
serializer: @Sendable (OperationInput) throws -> Request,
91+
deserializer: @Sendable (Response) throws -> OperationOutput
92+
) async throws -> OperationOutput where OperationInput: Sendable, OperationOutput: Sendable {
93+
@Sendable
9294
func wrappingErrors<R>(
9395
work: () async throws -> R,
9496
mapError: (Error) -> Error
@@ -121,7 +123,7 @@ public struct UniversalClient: Sendable {
121123
makeError(error: error)
122124
}
123125
let response: Response = try await wrappingErrors {
124-
var next: (Request, URL) async throws -> Response = { (_request, _url) in
126+
var next: @Sendable (Request, URL) async throws -> Response = { (_request, _url) in
125127
try await wrappingErrors {
126128
try await transport.send(
127129
_request,

Sources/OpenAPIRuntime/Interface/UniversalServer.swift

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,11 @@ public struct UniversalServer<APIHandler: Sendable>: Sendable {
8888
request: Request,
8989
with metadata: ServerRequestMetadata,
9090
forOperation operationID: String,
91-
using handlerMethod: @escaping (APIHandler) -> ((OperationInput) async throws -> OperationOutput),
92-
deserializer: @escaping (Request, ServerRequestMetadata) throws -> OperationInput,
93-
serializer: @escaping (OperationOutput, Request) throws -> Response
94-
) async throws -> Response {
91+
using handlerMethod: @Sendable @escaping (APIHandler) -> ((OperationInput) async throws -> OperationOutput),
92+
deserializer: @Sendable @escaping (Request, ServerRequestMetadata) throws -> OperationInput,
93+
serializer: @Sendable @escaping (OperationOutput, Request) throws -> Response
94+
) async throws -> Response where OperationInput: Sendable, OperationOutput: Sendable {
95+
@Sendable
9596
func wrappingErrors<R>(
9697
work: () async throws -> R,
9798
mapError: (Error) -> Error
@@ -102,6 +103,7 @@ public struct UniversalServer<APIHandler: Sendable>: Sendable {
102103
throw mapError(error)
103104
}
104105
}
106+
@Sendable
105107
func makeError(
106108
input: OperationInput? = nil,
107109
output: OperationOutput? = nil,
@@ -116,7 +118,7 @@ public struct UniversalServer<APIHandler: Sendable>: Sendable {
116118
underlyingError: error
117119
)
118120
}
119-
var next: (Request, ServerRequestMetadata) async throws -> Response = { _request, _metadata in
121+
var next: @Sendable (Request, ServerRequestMetadata) async throws -> Response = { _request, _metadata in
120122
let input: OperationInput = try await wrappingErrors {
121123
try deserializer(_request, _metadata)
122124
} mapError: { error in

docker/docker-compose.2204.58.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ services:
1313
environment:
1414
- WARN_AS_ERROR_ARG=-Xswiftc -warnings-as-errors
1515
- IMPORT_CHECK_ARG=--explicit-target-dependency-import-check error
16+
- STRICT_CONCURRENCY_ARG=-Xswiftc -strict-concurrency=complete
1617

1718
shell:
1819
image: *image

docker/docker-compose.2204.59.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ services:
1212
environment:
1313
- WARN_AS_ERROR_ARG=-Xswiftc -warnings-as-errors
1414
- IMPORT_CHECK_ARG=--explicit-target-dependency-import-check error
15+
- STRICT_CONCURRENCY_ARG=-Xswiftc -strict-concurrency=complete
1516

1617
shell:
1718
image: *image

docker/docker-compose.2204.main.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ services:
1313
environment:
1414
- WARN_AS_ERROR_ARG=-Xswiftc -warnings-as-errors
1515
- IMPORT_CHECK_ARG=--explicit-target-dependency-import-check error
16+
- STRICT_CONCURRENCY_ARG=-Xswiftc -strict-concurrency=complete
1617

1718
shell:
1819
image: *image

docker/docker-compose.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ services:
3030

3131
test:
3232
<<: *common
33-
command: /bin/bash -xcl "swift $${SWIFT_TEST_VERB-test} $${WARN_AS_ERROR_ARG-} $${SANITIZER_ARG-} $${IMPORT_CHECK_ARG-}"
33+
command: /bin/bash -xcl "swift $${SWIFT_TEST_VERB-test} $${WARN_AS_ERROR_ARG-} $${SANITIZER_ARG-} $${IMPORT_CHECK_ARG-} $${STRICT_CONCURRENCY_ARG-}"
3434

3535
shell:
3636
<<: *common

scripts/run-integration-test.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,6 @@ swift package --package-path "${INTEGRATION_TEST_PACKAGE_PATH}" \
4343
edit "${PACKAGE_NAME}" --path "${PACKAGE_PATH}"
4444

4545
log "Building integration test package: ${INTEGRATION_TEST_PACKAGE_PATH}"
46-
swift build --package-path "${INTEGRATION_TEST_PACKAGE_PATH}"
46+
swift build --package-path "${INTEGRATION_TEST_PACKAGE_PATH}" -Xswiftc -strict-concurrency=complete
4747

4848
log "✅ Successfully built integration test package."

0 commit comments

Comments
 (0)