Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix strict concurrency warnings #58

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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
5 changes: 4 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ let package = Package(
dependencies: [
"PapyrusPlugin"
],
path: "Papyrus/Sources"
path: "Papyrus/Sources",
swiftSettings: [
.enableExperimentalFeature("StrictConcurrency")
]
),
.testTarget(
name: "PapyrusTests",
Expand Down
36 changes: 31 additions & 5 deletions Papyrus/Sources/Coders.swift
Original file line number Diff line number Diff line change
@@ -1,12 +1,38 @@
enum Coders {

// MARK: HTTP Body

static var defaultHTTPBodyEncoder: HTTPBodyEncoder = .json()
static var defaultHTTPBodyDecoder: HTTPBodyDecoder = .json()
static let defaultHTTPBodyEncoder: HTTPBodyEncoder = .json()
static let defaultHTTPBodyDecoder: HTTPBodyDecoder = .json()
joshuawright11 marked this conversation as resolved.
Show resolved Hide resolved

// MARK: Query

static var defaultQueryEncoder = URLEncodedFormEncoder()
static var defaultQueryDecoder = URLEncodedFormDecoder()
static let defaultQueryEncoder = URLEncodedFormEncoder()
static let defaultQueryDecoder = URLEncodedFormDecoder()
}

public protocol CoderProvider: Sendable {
func provideHttpBodyEncoder() -> HTTPBodyEncoder
func provideHttpBodyDecoder() -> HTTPBodyDecoder
func provideQueryEncoder() -> URLEncodedFormEncoder
func provideQueryDecoder() -> URLEncodedFormDecoder
}

public struct DefaultProvider: CoderProvider {
public init() {}

public func provideHttpBodyEncoder() -> HTTPBodyEncoder {
return .json()
}

public func provideHttpBodyDecoder() -> HTTPBodyDecoder {
return .json()
}

public func provideQueryEncoder() -> URLEncodedFormEncoder {
return URLEncodedFormEncoder()
}

public func provideQueryDecoder() -> URLEncodedFormDecoder {
return URLEncodedFormDecoder()
}
}
4 changes: 2 additions & 2 deletions Papyrus/Sources/HTTPBodyDecoder.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

public protocol HTTPBodyDecoder: KeyMappable {
public protocol HTTPBodyDecoder: KeyMappable, Sendable {
func decode<D: Decodable>(_ type: D.Type, from: Data) throws -> D
}

Expand All @@ -12,7 +12,7 @@ extension HTTPBodyDecoder where Self == JSONDecoder {
}
}

extension JSONDecoder: HTTPBodyDecoder {
extension JSONDecoder: HTTPBodyDecoder, @unchecked Sendable {
public func with(keyMapping: KeyMapping) -> Self {
let new = JSONDecoder()
new.userInfo = userInfo
Expand Down
4 changes: 2 additions & 2 deletions Papyrus/Sources/HTTPBodyEncoder.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

public protocol HTTPBodyEncoder: KeyMappable {
public protocol HTTPBodyEncoder: KeyMappable, Sendable {
var contentType: String { get }
func encode<E: Encodable>(_ value: E) throws -> Data
}
Expand All @@ -13,7 +13,7 @@ extension HTTPBodyEncoder where Self == JSONEncoder {
}
}

extension JSONEncoder: HTTPBodyEncoder {
extension JSONEncoder: HTTPBodyEncoder, @unchecked Sendable {
public var contentType: String { "application/json" }

public func with(keyMapping: KeyMapping) -> Self {
Expand Down
3 changes: 2 additions & 1 deletion Papyrus/Sources/HTTPService.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import Foundation

/// A type that can perform arbitrary HTTP requests.
public protocol HTTPService {
public protocol HTTPService: Sendable {
/// Build a `Request` from the given components.
func build(method: String, url: URL, headers: [String: String], body: Data?) -> PapyrusRequest

/// Concurrency based API
@Sendable
func request(_ req: PapyrusRequest) async -> PapyrusResponse
}
9 changes: 5 additions & 4 deletions Papyrus/Sources/Interceptors/CurlInterceptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,22 @@ import Foundation

/// An `Interceptor` that logs requests based on a condition
public struct CurlLogger {
public enum Condition {
public typealias LogHandler = @Sendable (String) -> Void
public enum Condition: Sendable {
case always

/// only log when the request encountered an error
case onError
}

let logHandler: (String) -> Void
let logHandler: LogHandler
let condition: Condition

/// An `Interceptor` that calls a logHandler with a request based on a condition
/// - Parameters:
/// - condition: must be met for the logging function to be called
/// - logHandler: a function that implements logging. defaults to `print()`
public init(when condition: Condition, using logHandler: @escaping (String) -> Void = { print($0) }) {
public init(when condition: Condition, using logHandler: @escaping LogHandler = { print($0) }) {
self.condition = condition
self.logHandler = logHandler
}
Expand Down
6 changes: 3 additions & 3 deletions Papyrus/Sources/KeyMapping.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Foundation

/// Represents the mapping between your type's property names and
/// their corresponding request field key.
public enum KeyMapping {
public enum KeyMapping: Sendable {
joshuawright11 marked this conversation as resolved.
Show resolved Hide resolved
/// Use the literal name for all properties on a type as its field key.
case useDefaultKeys

Expand All @@ -12,8 +12,8 @@ public enum KeyMapping {
case snakeCase

/// A custom key mapping.
case custom(to: (String) -> String, from: (String) -> String)
case custom(to: @Sendable (String) -> String, from: @Sendable (String) -> String)

/// Encode String from camelCase to this KeyMapping strategy.
public func encode(_ string: String) -> String {
switch self {
Expand Down
2 changes: 1 addition & 1 deletion Papyrus/Sources/PapyrusRequest.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

public protocol PapyrusRequest {
public protocol PapyrusRequest: Sendable {
var url: URL? { get set }
var method: String { get set }
var headers: [String: String] { get set }
Expand Down
2 changes: 1 addition & 1 deletion Papyrus/Sources/PapyrusResponse.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

public protocol PapyrusResponse {
public protocol PapyrusResponse: Sendable {
var request: PapyrusRequest? { get }
var body: Data? { get }
var headers: [String: String]? { get }
Expand Down
54 changes: 34 additions & 20 deletions Papyrus/Sources/Provider.swift
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
import Foundation

/// Makes URL requests.
public final class Provider {
public final class Provider: Sendable {
public let baseURL: String
public let http: HTTPService
public var interceptors: [Interceptor]
public var modifiers: [RequestModifier]
public let provider: CoderProvider
private let interceptors: ResourceMutex<[Interceptor]>
private let modifiers: ResourceMutex<[RequestModifier]>

public init(baseURL: String, http: HTTPService, modifiers: [RequestModifier] = [], interceptors: [Interceptor] = []) {
public init(baseURL: String, http: HTTPService, modifiers: [RequestModifier] = [], interceptors: [Interceptor] = [], provider: CoderProvider = DefaultProvider()) {
self.baseURL = baseURL
self.http = http
self.interceptors = interceptors
self.modifiers = modifiers
self.provider = provider
self.interceptors = .init(resource: interceptors)
self.modifiers = .init(resource: modifiers)
}

public func newBuilder(method: String, path: String) -> RequestBuilder {
RequestBuilder(baseURL: baseURL, method: method, path: path)
}

public func add(interceptor: any Interceptor) {
interceptors.withLock { resource in
resource.append(interceptor)
}
}

public func modifyRequests(action: @escaping (inout RequestBuilder) throws -> Void) -> Self {
struct AnonymousModifier: RequestModifier {
let action: (inout RequestBuilder) throws -> Void
Expand All @@ -27,39 +35,45 @@ public final class Provider {
}
}

modifiers.append(AnonymousModifier(action: action))
modifiers.withLock { resource in
resource.append(AnonymousModifier(action: action))
}
return self
}

@discardableResult
public func intercept(action: @escaping (PapyrusRequest, (PapyrusRequest) async throws -> PapyrusResponse) async throws -> PapyrusResponse) -> Self {
public func intercept(action: @Sendable @escaping (PapyrusRequest, (PapyrusRequest) async throws -> PapyrusResponse) async throws -> PapyrusResponse) -> Self {
struct AnonymousInterceptor: Interceptor {
let action: (PapyrusRequest, Interceptor.Next) async throws -> PapyrusResponse
let action: @Sendable (PapyrusRequest, Interceptor.Next) async throws -> PapyrusResponse

func intercept(req: PapyrusRequest, next: Interceptor.Next) async throws -> PapyrusResponse {
try await action(req, next)
}
}

interceptors.append(AnonymousInterceptor(action: action))
interceptors.withLock { resource in
resource.append(AnonymousInterceptor(action: action))
}
return self
}

@discardableResult
public func request(_ builder: inout RequestBuilder) async throws -> PapyrusResponse {
let request = try createRequest(&builder)
var next: (PapyrusRequest) async throws -> PapyrusResponse = http.request
for interceptor in interceptors.reversed() {
let _next = next
next = { try await interceptor.intercept(req: $0, next: _next) }
var next: @Sendable (PapyrusRequest) async throws -> PapyrusResponse = http.request
interceptors.withLock { resource in
for interceptor in resource.reversed() {
let _next = next
next = { try await interceptor.intercept(req: $0, next: _next) }
}
}

return try await next(request)
}

private func createRequest(_ builder: inout RequestBuilder) throws -> PapyrusRequest {
for modifier in modifiers {
try modifier.modify(req: &builder)
try modifiers.withLock { resource in
for modifier in resource {
try modifier.modify(req: &builder)
}
}

let url = try builder.fullURL()
Expand All @@ -68,8 +82,8 @@ public final class Provider {
}
}

public protocol Interceptor {
typealias Next = (PapyrusRequest) async throws -> PapyrusResponse
public protocol Interceptor: Sendable {
typealias Next = @Sendable (PapyrusRequest) async throws -> PapyrusResponse
func intercept(req: PapyrusRequest, next: Next) async throws -> PapyrusResponse
}

Expand Down
20 changes: 12 additions & 8 deletions Papyrus/Sources/RequestBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ public struct RequestBuilder {
}

// MARK: Data

public var baseURL: String
public var method: String
public var path: String
Expand All @@ -85,34 +84,39 @@ public struct RequestBuilder {

// MARK: Configuration

private let provider: CoderProvider
public var keyMapping: KeyMapping?

public var queryEncoder: URLEncodedFormEncoder {
get { return _queryEncoder.with(keyMapping: keyMapping) }
set { _queryEncoder = newValue }
get { _queryEncoder.with(keyMapping: keyMapping) }
}

public var requestBodyEncoder: HTTPBodyEncoder {
get { return _requestBodyEncoder.with(keyMapping: keyMapping) }
set { _requestBodyEncoder = newValue }
get { _requestBodyEncoder.with(keyMapping: keyMapping) }
}

public var responseBodyDecoder: HTTPBodyDecoder {
get { return _responseBodyDecoder.with(keyMapping: keyMapping) }
set { _responseBodyDecoder = newValue }
get { _responseBodyDecoder.with(keyMapping: keyMapping) }
}

private var _queryEncoder: URLEncodedFormEncoder = Coders.defaultQueryEncoder
private var _requestBodyEncoder: HTTPBodyEncoder = Coders.defaultHTTPBodyEncoder
private var _responseBodyDecoder: HTTPBodyDecoder = Coders.defaultHTTPBodyDecoder
private var _queryEncoder: URLEncodedFormEncoder
private var _requestBodyEncoder: HTTPBodyEncoder
private var _responseBodyDecoder: HTTPBodyDecoder

public init(baseURL: String, method: String, path: String) {
public init(baseURL: String, method: String, path: String, provider: CoderProvider = DefaultProvider()) {
self.baseURL = baseURL
self.method = method
self.path = path
self.parameters = [:]
self.headers = [:]
self.queries = [:]
self.provider = provider
self._queryEncoder = provider.provideQueryEncoder()
self._requestBodyEncoder = provider.provideHttpBodyEncoder()
self._responseBodyDecoder = provider.provideHttpBodyDecoder()
self.body = nil
}

Expand Down
52 changes: 52 additions & 0 deletions Papyrus/Sources/ResourceMutex.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//
// ResourceMutex.swift
//
//
// Created by Kevin Pittevils on 11/07/2024.
//

import Foundation

// Note: Can be replaced with Synchronization framework starting with iOS 18.
final class ResourceMutex<R>: @unchecked Sendable {
private var resource: R
private let mutex: UnsafeMutablePointer<pthread_mutex_t>

init(resource: R) {
let mutexAttr = UnsafeMutablePointer<pthread_mutexattr_t>.allocate(capacity: 1)
pthread_mutexattr_init(mutexAttr)
pthread_mutexattr_settype(mutexAttr, Int32(PTHREAD_MUTEX_RECURSIVE))
mutex = UnsafeMutablePointer<pthread_mutex_t>.allocate(capacity: 1)
pthread_mutex_init(mutex, mutexAttr)
pthread_mutexattr_destroy(mutexAttr)
mutexAttr.deallocate()
self.resource = resource
}

deinit {
pthread_mutex_destroy(mutex)
mutex.deallocate()
}

func withLock<T>(method: (inout R) throws -> T) throws -> T {
defer { unlock() }
lock()
return try method(&resource)
}

func withLock<T>(method: (inout R) -> T) -> T {
defer { unlock() }
lock()
return method(&resource)
}
}

private extension ResourceMutex {
func lock() {
pthread_mutex_lock(mutex)
}

func unlock() {
pthread_mutex_unlock(mutex)
}
}
2 changes: 1 addition & 1 deletion Papyrus/Sources/URLEncoded/URLEncodedForm.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ internal enum URLEncodedForm {

@available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *)
/// ISO8601 data formatter used throughout URL encoded form code
static var iso8601Formatter: ISO8601DateFormatter = {
static let iso8601Formatter: ISO8601DateFormatter = {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = .withInternetDateTime
return formatter
Expand Down
Loading
Loading