From fc746631b6ea851810ee05206253e7dfe17d872c Mon Sep 17 00:00:00 2001 From: Luiz Barbosa Date: Thu, 21 May 2026 10:34:00 +0100 Subject: [PATCH] Bump FP to 1.8.1 + propagate Sendable through RequestPublisher MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FP 1.8.1 makes FunctionWrapper refine Sendable and requires @Sendable closures across composition. Updates here: - Package.swift: bump FP from 1.7.0 to 1.8.1. - RequestPublisher's stored `run` closure and init are now @Sendable (required by FunctionWrapper). - Functor / Applicative / Monad methods on RequestPublisher take @Sendable closures: map, fmap, replace, mapError, flatMap, bind, kleisli, kleisliBack, flatMapError; pure requires A: Sendable; apply takes @Sendable inner closure. - Operator overloads (<£>, <&>, £>, <£, >>-, -<<, >=>, <=<, <*>) match: @Sendable closure parameters, @Sendable curried returns where applicable. - TemplateValue enum (Context's value type) now conforms Sendable so the renderToken context capture is valid in a @Sendable closure. - Test closures and typed lets annotated with @Sendable where the new signatures require it. Build + tests green. --- Package.swift | 2 +- Sources/HTMLTemplating/TemplateEngine.swift | 2 +- .../RequestPublisher+Applicative.swift | 4 +-- ...equestPublisher+ApplicativeOperators.swift | 2 +- .../RequestPublisher+Functor.swift | 10 +++---- .../RequestPublisher+FunctorOperators.swift | 8 +++--- .../RequestPublisher+Monad.swift | 18 ++++++------- .../RequestPublisher+MonadOperators.swift | 16 ++++++------ Sources/NetworkClient/RequestPublisher.swift | 4 +-- .../NetworkClientTests.swift | 26 +++++++++---------- 10 files changed, 46 insertions(+), 46 deletions(-) diff --git a/Package.swift b/Package.swift index 1255b1c..16be457 100644 --- a/Package.swift +++ b/Package.swift @@ -17,7 +17,7 @@ let package = Package( .library(name: "NetworkServer", targets: ["NetworkServer"]), ], dependencies: [ - .package(url: "https://github.com/luizmb/FP.git", from: "1.7.0"), + .package(url: "https://github.com/luizmb/FP.git", from: "1.8.1"), .package(url: "https://github.com/apple/swift-nio.git", from: "2.65.0"), .package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.4.0"), ], diff --git a/Sources/HTMLTemplating/TemplateEngine.swift b/Sources/HTMLTemplating/TemplateEngine.swift index 87d86a8..0a696a1 100644 --- a/Sources/HTMLTemplating/TemplateEngine.swift +++ b/Sources/HTMLTemplating/TemplateEngine.swift @@ -62,7 +62,7 @@ public struct HTMLEnvironment { public typealias Context = [String: TemplateValue] -public indirect enum TemplateValue { +public indirect enum TemplateValue: Sendable { case string(String) case list([Context]) case bool(Bool) diff --git a/Sources/NetworkClient/RequestPublisher+Applicative.swift b/Sources/NetworkClient/RequestPublisher+Applicative.swift index 9f2ba3d..b25f59b 100644 --- a/Sources/NetworkClient/RequestPublisher+Applicative.swift +++ b/Sources/NetworkClient/RequestPublisher+Applicative.swift @@ -6,7 +6,7 @@ import Foundation public extension RequestPublisher { /// Lifts a pure value into `RequestPublisher`, ignoring the `URLRequest`. - static func pure(_ value: A) -> RequestPublisher { + static func pure(_ value: A) -> RequestPublisher where A: Sendable { RequestPublisher { _ in Just(value).setFailureType(to: HTTPError.self).eraseToAnyPublisher() } @@ -14,7 +14,7 @@ public extension RequestPublisher { /// Applies a request-dependent function to a request-dependent value. /// Both are run against the same `URLRequest` and zipped. - static func apply(_ f: RequestPublisher<(A) -> B>, _ r: RequestPublisher) -> RequestPublisher { + static func apply(_ f: RequestPublisher<@Sendable (A) -> B>, _ r: RequestPublisher) -> RequestPublisher { RequestPublisher { request in f.run(request) .zip(r.run(request)) diff --git a/Sources/NetworkClient/RequestPublisher+ApplicativeOperators.swift b/Sources/NetworkClient/RequestPublisher+ApplicativeOperators.swift index a0aa8d3..0d2f489 100644 --- a/Sources/NetworkClient/RequestPublisher+ApplicativeOperators.swift +++ b/Sources/NetworkClient/RequestPublisher+ApplicativeOperators.swift @@ -6,7 +6,7 @@ import FP // MARK: - RequestPublisher Applicative Operators // (<*>) :: RequestPublisher<(a -> b)> -> RequestPublisher -> RequestPublisher -public func <*> (_ f: RequestPublisher<(A) -> B>, _ r: RequestPublisher) -> RequestPublisher { +public func <*> (_ f: RequestPublisher<@Sendable (A) -> B>, _ r: RequestPublisher) -> RequestPublisher { RequestPublisher.apply(f, r) } diff --git a/Sources/NetworkClient/RequestPublisher+Functor.swift b/Sources/NetworkClient/RequestPublisher+Functor.swift index 60c81e7..e96063b 100644 --- a/Sources/NetworkClient/RequestPublisher+Functor.swift +++ b/Sources/NetworkClient/RequestPublisher+Functor.swift @@ -6,22 +6,22 @@ import Foundation public extension RequestPublisher { /// Transforms the output value; the same `URLRequest` is threaded through unchanged. - func map(_ f: @escaping (A) -> B) -> RequestPublisher { + func map(_ f: @escaping @Sendable (A) -> B) -> RequestPublisher { RequestPublisher { request in run(request).map(f).eraseToAnyPublisher() } } /// Curried fmap for point-free composition. - static func fmap(_ f: @escaping (A) -> B) -> (RequestPublisher) -> RequestPublisher { + static func fmap(_ f: @escaping @Sendable (A) -> B) -> @Sendable (RequestPublisher) -> RequestPublisher { { $0.map(f) } } /// Replaces the output with a constant value. - func replace(with value: B) -> RequestPublisher { - map { _ in value } + func replace(with value: B) -> RequestPublisher { + map { @Sendable _ in value } } /// Maps the failure side of the underlying publisher. - func mapError(_ f: @escaping (HTTPError) -> HTTPError) -> RequestPublisher { + func mapError(_ f: @escaping @Sendable (HTTPError) -> HTTPError) -> RequestPublisher { RequestPublisher { request in run(request).mapError(f).eraseToAnyPublisher() } } } diff --git a/Sources/NetworkClient/RequestPublisher+FunctorOperators.swift b/Sources/NetworkClient/RequestPublisher+FunctorOperators.swift index 05e36e8..8a4e8aa 100644 --- a/Sources/NetworkClient/RequestPublisher+FunctorOperators.swift +++ b/Sources/NetworkClient/RequestPublisher+FunctorOperators.swift @@ -6,23 +6,23 @@ import FP // MARK: - RequestPublisher Functor Operators // (<$>) :: (a -> b) -> RequestPublisher -> RequestPublisher -public func <£> (_ f: @escaping (A) -> B, _ r: RequestPublisher) -> RequestPublisher { +public func <£> (_ f: @escaping @Sendable (A) -> B, _ r: RequestPublisher) -> RequestPublisher { r.map(f) } // (<&>) :: RequestPublisher -> (a -> b) -> RequestPublisher -public func <&> (_ r: RequestPublisher, _ f: @escaping (A) -> B) -> RequestPublisher { +public func <&> (_ r: RequestPublisher, _ f: @escaping @Sendable (A) -> B) -> RequestPublisher { r.map(f) } // ($>) :: RequestPublisher -> b -> RequestPublisher // swiftlint:disable:next identifier_name -public func £> (_ r: RequestPublisher, _ value: B) -> RequestPublisher { +public func £> (_ r: RequestPublisher, _ value: B) -> RequestPublisher { r.replace(with: value) } // (<$) :: b -> RequestPublisher -> RequestPublisher -public func <£ (_ value: B, _ r: RequestPublisher) -> RequestPublisher { +public func <£ (_ value: B, _ r: RequestPublisher) -> RequestPublisher { r £> value } #endif diff --git a/Sources/NetworkClient/RequestPublisher+Monad.swift b/Sources/NetworkClient/RequestPublisher+Monad.swift index 02016e1..b1aac40 100644 --- a/Sources/NetworkClient/RequestPublisher+Monad.swift +++ b/Sources/NetworkClient/RequestPublisher+Monad.swift @@ -7,7 +7,7 @@ import Foundation public extension RequestPublisher { /// Chains two `RequestPublisher`s, threading the same `URLRequest` through both /// (Reader + Publisher monad stack). - func flatMap(_ f: @escaping (A) -> RequestPublisher) -> RequestPublisher { + func flatMap(_ f: @escaping @Sendable (A) -> RequestPublisher) -> RequestPublisher { RequestPublisher { request in self.run(request) .flatMap { a in f(a).run(request) } @@ -16,7 +16,7 @@ public extension RequestPublisher { } /// Curried bind for point-free composition. - static func bind(_ f: @escaping (A) -> RequestPublisher) -> (RequestPublisher) -> RequestPublisher { + static func bind(_ f: @escaping @Sendable (A) -> RequestPublisher) -> @Sendable (RequestPublisher) -> RequestPublisher { { $0.flatMap(f) } } @@ -32,23 +32,23 @@ public extension RequestPublisher { /// Kleisli composition (left-to-right): `(X -> m A) >=> (A -> m B) = X -> m B`. static func kleisli( - _ f: @escaping (X) -> RequestPublisher, - _ g: @escaping (A) -> RequestPublisher - ) -> (X) -> RequestPublisher { + _ f: @escaping @Sendable (X) -> RequestPublisher, + _ g: @escaping @Sendable (A) -> RequestPublisher + ) -> @Sendable (X) -> RequestPublisher { { x in f(x).flatMap(g) } } /// Kleisli composition (right-to-left). static func kleisliBack( - _ g: @escaping (A) -> RequestPublisher, - _ f: @escaping (X) -> RequestPublisher - ) -> (X) -> RequestPublisher { + _ g: @escaping @Sendable (A) -> RequestPublisher, + _ f: @escaping @Sendable (X) -> RequestPublisher + ) -> @Sendable (X) -> RequestPublisher { RequestPublisher.kleisli(f, g) } /// Recovers from failure by running an alternative `RequestPublisher` /// against the same `URLRequest`. - func flatMapError(_ f: @escaping (HTTPError) -> RequestPublisher) -> RequestPublisher { + func flatMapError(_ f: @escaping @Sendable (HTTPError) -> RequestPublisher) -> RequestPublisher { RequestPublisher { request in self.run(request) .catch { error in f(error).run(request) } diff --git a/Sources/NetworkClient/RequestPublisher+MonadOperators.swift b/Sources/NetworkClient/RequestPublisher+MonadOperators.swift index 4291cd1..f121331 100644 --- a/Sources/NetworkClient/RequestPublisher+MonadOperators.swift +++ b/Sources/NetworkClient/RequestPublisher+MonadOperators.swift @@ -6,28 +6,28 @@ import FP // MARK: - RequestPublisher Monad Operators // (>>-) :: RequestPublisher -> (a -> RequestPublisher) -> RequestPublisher -public func >>- (_ r: RequestPublisher, _ f: @escaping (A) -> RequestPublisher) -> RequestPublisher { +public func >>- (_ r: RequestPublisher, _ f: @escaping @Sendable (A) -> RequestPublisher) -> RequestPublisher { r.flatMap(f) } // (-<<) :: (a -> RequestPublisher) -> RequestPublisher -> RequestPublisher -public func -<< (_ f: @escaping (A) -> RequestPublisher, _ r: RequestPublisher) -> RequestPublisher { +public func -<< (_ f: @escaping @Sendable (A) -> RequestPublisher, _ r: RequestPublisher) -> RequestPublisher { r.flatMap(f) } // (>=>) :: (x -> RequestPublisher) -> (a -> RequestPublisher) -> x -> RequestPublisher public func >=> ( - _ f: @escaping (X) -> RequestPublisher, - _ g: @escaping (A) -> RequestPublisher -) -> (X) -> RequestPublisher { + _ f: @escaping @Sendable (X) -> RequestPublisher, + _ g: @escaping @Sendable (A) -> RequestPublisher +) -> @Sendable (X) -> RequestPublisher { RequestPublisher.kleisli(f, g) } // (<=<) :: (a -> RequestPublisher) -> (x -> RequestPublisher) -> x -> RequestPublisher public func <=< ( - _ g: @escaping (A) -> RequestPublisher, - _ f: @escaping (X) -> RequestPublisher -) -> (X) -> RequestPublisher { + _ g: @escaping @Sendable (A) -> RequestPublisher, + _ f: @escaping @Sendable (X) -> RequestPublisher +) -> @Sendable (X) -> RequestPublisher { RequestPublisher.kleisliBack(g, f) } #endif diff --git a/Sources/NetworkClient/RequestPublisher.swift b/Sources/NetworkClient/RequestPublisher.swift index 8aba61e..b50b5e4 100644 --- a/Sources/NetworkClient/RequestPublisher.swift +++ b/Sources/NetworkClient/RequestPublisher.swift @@ -8,9 +8,9 @@ import FP /// Combines the Reader monad (threading the same `URLRequest` through a chain) /// with the Publisher monad (async response streaming with typed errors). public struct RequestPublisher: FunctionWrapper { - public let run: (URLRequest) -> AnyPublisher + public let run: @Sendable (URLRequest) -> AnyPublisher - public init(_ fn: @escaping (URLRequest) -> AnyPublisher) { + public init(_ fn: @escaping @Sendable (URLRequest) -> AnyPublisher) { run = fn } diff --git a/Tests/NetworkClientTests/NetworkClientTests.swift b/Tests/NetworkClientTests/NetworkClientTests.swift index d4397f8..a644dee 100644 --- a/Tests/NetworkClientTests/NetworkClientTests.swift +++ b/Tests/NetworkClientTests/NetworkClientTests.swift @@ -44,7 +44,7 @@ private func firstResult(of publisher: AnyPublisher) -> Resul // MARK: - RequestPublisher builders -private func just(_ value: A) -> RequestPublisher { +private func just(_ value: A) -> RequestPublisher { RequestPublisher { _ in Just(value).setFailureType(to: HTTPError.self).eraseToAnyPublisher() } } @@ -284,17 +284,17 @@ struct RequestPublisherFunctorTests { @Suite("RequestPublisher — Applicative") struct RequestPublisherApplicativeTests { @Test func apply_combinesFunctionAndValue() { - let f = RequestPublisher<(Int) -> String>.pure(String.init) + let f = RequestPublisher<@Sendable (Int) -> String>.pure(String.init) #expect(run(RequestPublisher.apply(f, just(42)))?.successValue == "42") } @Test func apply_propagatesLeftFailure() { - let f = fail(.badStatus(500, Data())) as RequestPublisher<(Int) -> Int> + let f = fail(.badStatus(500, Data())) as RequestPublisher<@Sendable (Int) -> Int> #expect(run(RequestPublisher.apply(f, just(1)))?.isFailure == true) } @Test func apply_propagatesRightFailure() { - let f = just { (n: Int) in n + 1 } + let f = just({ @Sendable (n: Int) in n + 1 } as @Sendable (Int) -> Int) let a = fail(.badStatus(500, Data())) as RequestPublisher #expect(run(RequestPublisher.apply(f, a))?.isFailure == true) } @@ -326,14 +326,14 @@ struct RequestPublisherMonadTests { } @Test func kleisli_composes() { - let f: (String) -> RequestPublisher = { s in just(s.count) } - let g: (Int) -> RequestPublisher = { n in just("\(n)") } + let f: @Sendable (String) -> RequestPublisher = { s in just(s.count) } + let g: @Sendable (Int) -> RequestPublisher = { n in just("\(n)") } #expect(run(RequestPublisher.kleisli(f, g)("hello"))?.successValue == "5") } @Test func kleisliBack_composes() { - let f: (String) -> RequestPublisher = { s in just(s.count) } - let g: (Int) -> RequestPublisher = { n in just("\(n)") } + let f: @Sendable (String) -> RequestPublisher = { s in just(s.count) } + let g: @Sendable (Int) -> RequestPublisher = { n in just("\(n)") } #expect(run(RequestPublisher.kleisliBack(g, f)("hello"))?.successValue == "5") } @@ -356,7 +356,7 @@ struct RequestPublisherOperatorTests { @Test func replaceLeftOp() { #expect(run("r" <£ just(5))?.successValue == "r") } @Test func applyOp() { - let f = just { (n: Int) in n + 1 } + let f = just({ @Sendable (n: Int) in n + 1 } as @Sendable (Int) -> Int) #expect(run(f <*> just(41))?.successValue == 42) } @Test func seqRightOp() { #expect(run(just(1) *> just("k"))?.successValue == "k") } @@ -366,13 +366,13 @@ struct RequestPublisherOperatorTests { @Test func flippedBindOp() { #expect(run({ n in just(n * n) } -<< just(3))?.successValue == 9) } @Test func kleisliOp() { - let f: (Int) -> RequestPublisher = { n in just(n + 1) } - let g: (Int) -> RequestPublisher = { n in just("\(n)") } + let f: @Sendable (Int) -> RequestPublisher = { n in just(n + 1) } + let g: @Sendable (Int) -> RequestPublisher = { n in just("\(n)") } #expect(run((f >=> g)(41))?.successValue == "42") } @Test func kleisliBackOp() { - let f: (Int) -> RequestPublisher = { n in just(n + 1) } - let g: (Int) -> RequestPublisher = { n in just("\(n)") } + let f: @Sendable (Int) -> RequestPublisher = { n in just(n + 1) } + let g: @Sendable (Int) -> RequestPublisher = { n in just("\(n)") } #expect(run((g <=< f)(41))?.successValue == "42") } }