diff --git a/.gitignore b/.gitignore index 5fbae6e..ae9498c 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ *.xcworkspace/ xcuserdata/ /.swiftpm +/.vscode diff --git a/Package.swift b/Package.swift index c164678..567572c 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.2 +// swift-tools-version:6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -6,10 +6,10 @@ import PackageDescription let package = Package( name: "HTTPFluent", platforms: [ - .macOS(.v10_15), - .iOS(.v13), - .tvOS(.v13), - .watchOS(.v6) + .macOS(.v13), + .iOS(.v16), + .tvOS(.v16), + .watchOS(.v9) ], products: [ // Products define the executables and libraries produced by a package, and make them visible to other packages. diff --git a/README.md b/README.md index 50754c7..5c713be 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,66 @@ # HTTPFluent -HTTPFluent provides a fluent interface over HTTP, primarily designed to work with APIs. HTTPFluent supports three styles: callback, `async` (with Swift >= 5.5) and Combine. +HTTPFluent provides a fluent interface over HTTP, primarily designed to work with APIs. HTTPFluent supports three styles: callback, `async` (with Swift >= 5.5) and Combine (on Apple platforms). + +## Integration + +HTTPFluent is available only via Swift Package Manager. + +## Usage + +HTTPFluent is extremely intuitive to use, so a few examples will suffice: + +```swift +let id = 2349713 +let jwt = "xyz123" + +let request = URLClient(url: "https://myapi.com") + .path("user", id) + .authorization(bearer: jwt) + .post(json: User(name: "Don Quixote")) + +// Callback style +request.receive(json: User.self) { result in + do { + let user = try result.get() + } catch { + // Oops, no user + } +} + +// Async style +let user = try await request.receive(json: User.self) + +// Combine style +request.receivePublisher(json: User.self) + .sink { completion in + // Handle completion + } receiveValue: { user in + // Do something with user + } + .store(in: &cancellables) +``` + +HTTPFluent can also be used to generate a `URLRequest` without invoking it. + +```swift +let urlRequest = URLClient(url: "https://myapi.com") + .path("user", id) + .authorization(bearer: jwt) + .put(data: data) // Here we put raw data instead of JSON. + .request +``` + +HTTPFluent uses immutable state. Each step in the chain to build the `URLRequest` copies a `URLRequestBuilder` struct. All operations are thus additive, encouraging reuse. + +```swift +// Set up the shared information about the request. +let fluent = URLClient(url: "https://myapi.com") + .authorization(bearer: jwt) + .path("user") + +// This adds the value of id as a path element, so the result is +// the path /user/123 or whatever the value of id is. +let postWithId = fluent.path(id).post(json: User.self) +let user = try await postWithId.receive(json: User.self) +``` diff --git a/Sources/HTTPFluent/Combine+Shim.swift b/Sources/HTTPFluent/Combine+Shim.swift deleted file mode 100644 index 550ce0e..0000000 --- a/Sources/HTTPFluent/Combine+Shim.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// Combine+Shim.swift -// HTTPFluent -// -// Created by Gregory Higley on 8/12/20. -// - -import Foundation - -#if !canImport(Combine) -public protocol TopLevelDecoder { - associatedtype Input - func decode(_ type: T.Type, from: Input) throws -> T -} - -public protocol TopLevelEncoder { - associatedtype Output - func encode(_ value: T) throws -> Output -} -#endif diff --git a/Sources/HTTPFluent/ConstantValue.swift b/Sources/HTTPFluent/ConstantValue.swift index a078402..bfb40b5 100644 --- a/Sources/HTTPFluent/ConstantValue.swift +++ b/Sources/HTTPFluent/ConstantValue.swift @@ -3,11 +3,15 @@ // HttpFluent // // Created by Gregory Higley on 2020-07-18. +// Copyright © 2020 Prosumma. +// This code is licensed under the MIT license (see LICENSE for details). // import Foundation -public protocol ConstantValue: RawRepresentable & Hashable & ExpressibleByStringLiteral & CustomStringConvertible where RawValue == String { +public protocol ConstantValue: + RawRepresentable, Hashable, ExpressibleByStringLiteral, CustomStringConvertible, Sendable where RawValue == String +{ init(constantValue value: String) } diff --git a/Sources/HTTPFluent/Decoders.swift b/Sources/HTTPFluent/Decoders.swift index b497f49..a24600c 100644 --- a/Sources/HTTPFluent/Decoders.swift +++ b/Sources/HTTPFluent/Decoders.swift @@ -2,14 +2,19 @@ // Decoder.swift // HTTPFluent // -// Created by Gregory Higley on 8/10/20. +// Created by Gregory Higley on 2020-08-12. +// Copyright © 2020 Prosumma. +// This code is licensed under the MIT license (see LICENSE for details). // -import Combine import Foundation -public struct Decoders { - public typealias Decode = (Data) throws -> T +#if canImport(Combine) +import Combine +#endif + +public struct Decoders: Sendable { + public typealias Decode = @Sendable (Data) throws -> T public static func string(encoding: String.Encoding) -> Decode { return { data in @@ -22,7 +27,7 @@ public struct Decoders { public static let string: Decode = Self.string(encoding: .utf8) - public static func decode(_ type: T.Type, with decoder: Decoder) -> Decode where Decoder.Input == Data { + public static func decode(_ type: T.Type, with decoder: Decoder) -> Decode where Decoder.Input == Data { return { data in do { return try decoder.decode(type, from: data) @@ -32,7 +37,7 @@ public struct Decoders { } } - public static func json(_ type: T.Type) -> Decode { + public static func json(_ type: T.Type) -> Decode { return decode(type, with: JSONDecoder()) } } diff --git a/Sources/HTTPFluent/FormData.swift b/Sources/HTTPFluent/FormData.swift index 29e380d..904d665 100644 --- a/Sources/HTTPFluent/FormData.swift +++ b/Sources/HTTPFluent/FormData.swift @@ -3,7 +3,8 @@ // HTTPFluent // // Created by Gregory Higley on 2020-05-09. -// Copyright © 2020 Prosumma. All rights reserved. +// Copyright © 2020 Prosumma. +// This code is licensed under the MIT license (see LICENSE for details). // import Foundation @@ -24,8 +25,8 @@ import Foundation let data = form.encoded() ``` */ -public struct FormData { - public enum Encoding { +public struct FormData: @unchecked Sendable { + public enum Encoding: Sendable { case urlEncoded case formEncoded } @@ -35,7 +36,7 @@ public struct FormData { `FormData` instance is encoded as "application/x-www-form-urlencoded", instances of this type are skipped without warning. */ - public struct File { + public struct File: Sendable { public let filename: String public let content: Data public let headers: [String: String] diff --git a/Sources/HTTPFluent/HTTPHeaderField.swift b/Sources/HTTPFluent/HTTPHeaderField.swift index cb3745b..00365ef 100644 --- a/Sources/HTTPFluent/HTTPHeaderField.swift +++ b/Sources/HTTPFluent/HTTPHeaderField.swift @@ -3,7 +3,8 @@ // HTTPFluent // // Created by Gregory Higley on 2020-05-08. -// Copyright © 2020 Prosumma. All rights reserved. +// Copyright © 2020 Prosumma. +// This code is licensed under the MIT license (see LICENSE for details). // import Foundation diff --git a/Sources/HTTPFluent/HTTPMethod.swift b/Sources/HTTPFluent/HTTPMethod.swift index 7b41eaa..2968e28 100644 --- a/Sources/HTTPFluent/HTTPMethod.swift +++ b/Sources/HTTPFluent/HTTPMethod.swift @@ -3,7 +3,8 @@ // HTTPFluent // // Created by Gregory Higley on 2020-05-08. -// Copyright © 2020 Prosumma. All rights reserved. +// Copyright © 2020 Prosumma. +// This code is licensed under the MIT license (see LICENSE for details). // import Foundation diff --git a/Sources/HTTPFluent/MimeType.swift b/Sources/HTTPFluent/MimeType.swift index 6ff6787..35755b4 100644 --- a/Sources/HTTPFluent/MimeType.swift +++ b/Sources/HTTPFluent/MimeType.swift @@ -3,7 +3,8 @@ // HTTPFluent // // Created by Gregory Higley on 2020-05-08. -// Copyright © 2020 Prosumma. All rights reserved. +// Copyright © 2020 Prosumma. +// This code is licensed under the MIT license (see LICENSE for details). // import Foundation diff --git a/Sources/HTTPFluent/Publisher.swift b/Sources/HTTPFluent/Publisher.swift index f890f95..18b9462 100644 --- a/Sources/HTTPFluent/Publisher.swift +++ b/Sources/HTTPFluent/Publisher.swift @@ -3,9 +3,10 @@ // HTTPFluent // // Created by Gregory Higley on 2020-07-13. +// Copyright © 2020 Prosumma. +// This code is licensed under the MIT license (see LICENSE for details). // -#if canImport(Combine) import Combine @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) @@ -26,4 +27,3 @@ public extension Publisher { return mapErrorIfNeeded(URLError.error) } } -#endif diff --git a/Sources/HTTPFluent/URLClient.swift b/Sources/HTTPFluent/URLClient.swift index 2f2bf8c..4fc6440 100644 --- a/Sources/HTTPFluent/URLClient.swift +++ b/Sources/HTTPFluent/URLClient.swift @@ -3,56 +3,47 @@ // HTTPFluent // // Created by Gregory Higley on 2020-05-08. -// Copyright © 2020 Prosumma. All rights reserved. +// Copyright © 2020 Prosumma. +// This code is licensed under the MIT license (see LICENSE for details). // -#if canImport(Combine) import Combine -#endif - import Foundation -/** - Perform HTTP operations on a base URL. - - Most uses of HTTP at Patron and other organizations - perform one or more operations against some API hosted - at a specific base URL. `HTTPClient` makes this very - easy and natural using a fluent interface. - - This is best illustrated with an example: +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif - ```swift - let jwt: String = "...imagine a JWT..." - let token = "xyz123" - let attendee = Attendee(token: token) - let client = HTTPClient(url: "https://api.patron.com/api") - client - .path("attendee", token) // api/attendee/xyz123 - .query(7, forName: "x") // ?x=7 - .authorization(bearer: jwt) - .post(json: attendee) - .dataTaskPublisher(decoding: AttendeeResponse.self) - ``` - */ -public struct URLClient { - public typealias ResponseHandler = (Data?, URLResponse?) throws -> Data +public struct URLClient: Sendable { + public typealias ResponseHandler = @Sendable (Data?, URLResponse?) throws -> Data let session: URLSession var builder: URLRequestBuilder let responseHandler: ResponseHandler - public init(builder: URLRequestBuilder, session: URLSession = URLSession(configuration: .ephemeral), responseHandler: @escaping ResponseHandler = URLClient.defaultResponseHandler) { + public init( + builder: URLRequestBuilder, + session: URLSession = URLSession(configuration: .ephemeral), + responseHandler: @escaping ResponseHandler = URLClient.defaultResponseHandler + ) { self.session = session self.builder = builder self.responseHandler = responseHandler } - public init(url: URL, session: URLSession = URLSession(configuration: .ephemeral), responseHandler: @escaping ResponseHandler = URLClient.defaultResponseHandler) { + public init( + url: URL, + session: URLSession = URLSession(configuration: .ephemeral), + responseHandler: @escaping ResponseHandler = URLClient.defaultResponseHandler + ) { self.init(builder: URLRequestBuilder(url: url), session: session, responseHandler: responseHandler) } - public init(url: String, session: URLSession = URLSession(configuration: .ephemeral), responseHandler: @escaping ResponseHandler = URLClient.defaultResponseHandler) { + public init( + url: String, + session: URLSession = URLSession(configuration: .ephemeral), + responseHandler: @escaping ResponseHandler = URLClient.defaultResponseHandler + ) { self.init(builder: URLRequestBuilder(url: url), session: session, responseHandler: responseHandler) } @@ -72,8 +63,7 @@ extension URLClient: URLClientProtocol { builder.request } - @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) - public var publisher: AnyPublisher { + public var receivePublisher: AnyPublisher { request.publisher.flatMap { req in self.session .dataTaskPublisher(for: req) @@ -82,14 +72,17 @@ extension URLClient: URLClientProtocol { }.eraseToAnyPublisher() } - public func receive(on queue: DispatchQueue = DispatchQueue.global(), callback: @escaping (URLResult) -> Void) { + public func receive(on queue: DispatchQueue = DispatchQueue.global(), callback: @escaping @Sendable (URLResult) -> Void) { switch request { case .failure(let error): queue.async { callback(.failure(error)) } case .success(let request): let task = session.dataTask(with: request) { (data, response, error) in var result: URLResult = .failure(.unknown) - defer { queue.async { callback(result) } } + defer { + let result = result + queue.async { callback(result) } + } if let error = error { return result = .failure(.error(error)) } @@ -105,17 +98,24 @@ extension URLClient: URLClientProtocol { } } - #if swift(>=5.5) - - @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) public func receive() async throws -> Data { switch request { case .failure(let error): throw error case .success(let request): - let (data, response): (Data, URLResponse) + let (data, response): (Data?, URLResponse?) do { - (data, response) = try await session.data(for: request) + #if os(Linux) + (data, response) = try await withCheckedThrowingContinuation { continuation in + let task = session.dataTask(with: request) { (data, response, error) in + if let error = error { continuation.resume(throwing: error) } + continuation.resume(returning: (data, response)) + } + task.resume() + } + #else + (data, response) = try await session.data(for: request) + #endif } catch { throw URLError.error(error) } @@ -123,9 +123,6 @@ extension URLClient: URLClientProtocol { } } - #endif - - public func build(_ apply: (inout URLRequestBuilder) -> Void) -> URLClient { var client = self apply(&client.builder) diff --git a/Sources/HTTPFluent/URLClientProtocol+Async.swift b/Sources/HTTPFluent/URLClientProtocol+Async.swift index 88ed029..4d32fe7 100644 --- a/Sources/HTTPFluent/URLClientProtocol+Async.swift +++ b/Sources/HTTPFluent/URLClientProtocol+Async.swift @@ -3,35 +3,28 @@ // HTTPFluent // // Created by Gregory Higley on 2021-12-25. -// Copyright © 2020 Prosumma. All rights reserved. +// Copyright © 2020 Prosumma. +// This code is licensed under the MIT license (see LICENSE for details). // -#if canImport(Combine) import Combine -#endif - import Foundation -#if swift(>=5.5) - -@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) public extension URLClientProtocol { - func receive(decode: @escaping Decoders.Decode) async throws -> Response { + func receive(decode: @escaping Decoders.Decode) async throws -> Response { try decode(await receive()) } - func receive( + func receive( decoding type: Response.Type = Response.self, decoder: Decoder ) async throws -> Response where Response: Decodable, Decoder: TopLevelDecoder, Decoder.Input == Data { try await receive(decode: Decoders.decode(type, with: decoder)) } - func receive( + func receive( json type: Response.Type ) async throws -> Response { try await receive(decode: Decoders.json(type)) } } - -#endif diff --git a/Sources/HTTPFluent/URLClientProtocol+Combine.swift b/Sources/HTTPFluent/URLClientProtocol+Combine.swift index d36a857..6438597 100644 --- a/Sources/HTTPFluent/URLClientProtocol+Combine.swift +++ b/Sources/HTTPFluent/URLClientProtocol+Combine.swift @@ -2,16 +2,14 @@ // URLClientProtocol+Combine.swift // HTTPFluent // -// Created by Gregory Higley on 8/12/20. +// Created by Gregory Higley on 2020-08-12. +// Copyright © 2020 Prosumma. +// This code is licensed under the MIT license (see LICENSE for details). // -#if canImport(Combine) import Combine -#endif - import Foundation -@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) public extension URLClientProtocol { /** Decodes the `Data` in the published stream using `decode`. @@ -29,10 +27,10 @@ public extension URLClientProtocol { `Decoders.string` is a function which decodes from `Data` to `String`, assuming the underlying `Data` is UTF-8. */ - func publisher( + func receivePublisher( decode: @escaping Decoders.Decode ) -> AnyPublisher { - publisher.tryMap(decode).mapErrorIfNeeded(URLError.decoding).eraseToAnyPublisher() + receivePublisher.tryMap(decode).mapErrorIfNeeded(URLError.decoding).eraseToAnyPublisher() } /** @@ -42,19 +40,18 @@ public extension URLClientProtocol { If `decoder` is `JSONDecoder`, the `Accept: application/json` is sent automatically with the request. */ - func publisher( + func receivePublisher( decoding type: Response.Type = Response.self, decoder: Decoder ) -> AnyPublisher where Response: Decodable, Decoder: TopLevelDecoder, Decoder.Input == Data { let fluent = decoder is JSONDecoder ? accept(.json) : self - return fluent.publisher(decode: Decoders.decode(type, with: decoder)) + return fluent.receivePublisher(decode: Decoders.decode(type, with: decoder)) } /// Decodes the `Data` in the published stream using a default `JSONDecoder`. - func publisher( + func receivePublisher( json type: Response.Type = Response.self ) -> AnyPublisher { - accept(.json).publisher(decode: Decoders.json(type)) + accept(.json).receivePublisher(decode: Decoders.json(type)) } - } diff --git a/Sources/HTTPFluent/URLClientProtocol+Receive.swift b/Sources/HTTPFluent/URLClientProtocol+Receive.swift index 198ab37..1daea23 100644 --- a/Sources/HTTPFluent/URLClientProtocol+Receive.swift +++ b/Sources/HTTPFluent/URLClientProtocol+Receive.swift @@ -2,13 +2,12 @@ // URLClientProtocol+Receive.swift // HTTPFluent // -// Created by Gregory Higley on 8/12/20. +// Created by Gregory Higley on 2020-08-12. +// Copyright © 2020 Prosumma. +// This code is licensed under the MIT license (see LICENSE for details). // -#if canImport(Combine) import Combine -#endif - import Foundation //swiftlint:disable function_default_parameter_at_end @@ -22,7 +21,7 @@ public extension URLClientProtocol { */ func receive( on queue: DispatchQueue = DispatchQueue.global(), - callback: @escaping (URLResult) -> Void) + callback: @escaping @Sendable (URLResult) -> Void) { receive(on: queue, callback: callback) } @@ -36,7 +35,7 @@ public extension URLClientProtocol { func receive( on queue: DispatchQueue = DispatchQueue.global(), decode: @escaping Decoders.Decode, - callback: @escaping (URLResult) -> Void + callback: @escaping @Sendable (URLResult) -> Void ) { receive(on: queue) { result in do { @@ -49,11 +48,11 @@ public extension URLClientProtocol { } } - func receive( + func receive( decoding type: Response.Type = Response.self, decoder: Decoder, on queue: DispatchQueue = DispatchQueue.global(), - callback: @escaping (URLResult) -> Void + callback: @escaping @Sendable (URLResult) -> Void ) where Response: Decodable, Decoder: TopLevelDecoder, Decoder.Input == Data { let fluent = decoder is JSONDecoder ? accept(.json) : self return fluent.receive(on: queue, decode: Decoders.decode(type, with: decoder), callback: callback) @@ -62,7 +61,7 @@ public extension URLClientProtocol { func receive( json type: Response.Type = Response.self, on queue: DispatchQueue = DispatchQueue.global(), - callback: @escaping (URLResult) -> Void + callback: @escaping @Sendable (URLResult) -> Void ) where Response: Decodable { receive(on: queue, decode: Decoders.json(type), callback: callback) } diff --git a/Sources/HTTPFluent/URLClientProtocol.swift b/Sources/HTTPFluent/URLClientProtocol.swift index 3f19c1a..292a60a 100644 --- a/Sources/HTTPFluent/URLClientProtocol.swift +++ b/Sources/HTTPFluent/URLClientProtocol.swift @@ -3,13 +3,11 @@ // HTTPFluent // // Created by Gregory Higley on 2020-05-08. -// Copyright © 2020 Prosumma. All rights reserved. +// Copyright © 2020 Prosumma. +// This code is licensed under the MIT license (see LICENSE for details). // -#if canImport(Combine) import Combine -#endif - import Foundation /** @@ -20,17 +18,16 @@ public protocol URLClientProtocol: URLRequestBuilderProtocol { /** Publishes the `Data` result of executing the underlying `URLRequest`. */ - @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) - var publisher: AnyPublisher { get } - -#if swift(>=5.5) - @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) + var receivePublisher: AnyPublisher { get } + + /** + Gets the result of the `URLRequest` asynchronously as `Data`. + */ func receive() async throws -> Data -#endif /** Executes the underlying `URLRequest` and calls `callback` on the given `queue` when it completes. */ - func receive(on queue: DispatchQueue, callback: @escaping (URLResult) -> Void) + func receive(on queue: DispatchQueue, callback: @escaping @Sendable (URLResult) -> Void) } diff --git a/Sources/HTTPFluent/URLError.swift b/Sources/HTTPFluent/URLError.swift index bd3716f..3119a46 100644 --- a/Sources/HTTPFluent/URLError.swift +++ b/Sources/HTTPFluent/URLError.swift @@ -3,11 +3,16 @@ // HTTPFluent // // Created by Gregory Higley on 2020-02-24. -// Copyright © 2020 Prosumma. All rights reserved. +// Copyright © 2020 Prosumma. +// This code is licensed under the MIT license (see LICENSE for details). // import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + /// An error that may occur as a result of constructing or publishing a `URLRequest`. public enum URLError: Error { case error(Error?) diff --git a/Sources/HTTPFluent/URLRequestBuilder.swift b/Sources/HTTPFluent/URLRequestBuilder.swift index fde4cf6..54c1b85 100644 --- a/Sources/HTTPFluent/URLRequestBuilder.swift +++ b/Sources/HTTPFluent/URLRequestBuilder.swift @@ -3,19 +3,24 @@ // HTTPFluent // // Created by Gregory Higley on 2020-05-08. -// Copyright © 2020 Prosumma. All rights reserved. +// Copyright © 2020 Prosumma. +// This code is licensed under the MIT license (see LICENSE for details). // import Foundation -public struct URLRequestBuilder { +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + +public struct URLRequestBuilder: Sendable { public typealias Apply = (inout Self) -> Void fileprivate var error: URLError? fileprivate var components: URLComponents fileprivate var method: String = "GET" fileprivate var headers: [String: String] = [:] - fileprivate var body: (() throws -> Data)? + fileprivate var body: (@Sendable () throws -> Data)? private init(components: URLComponents) { self.components = components @@ -104,7 +109,7 @@ extension URLRequestBuilder { } } - static func buildBody(_ body: @escaping () throws -> Data) -> URLRequestBuilder.Apply { + static func buildBody(_ body: @escaping @Sendable () throws -> Data) -> URLRequestBuilder.Apply { return { builder in builder.body = body } diff --git a/Sources/HTTPFluent/URLRequestBuilderProtocol+HTTPBody.swift b/Sources/HTTPFluent/URLRequestBuilderProtocol+HTTPBody.swift index c5f116c..871a00b 100644 --- a/Sources/HTTPFluent/URLRequestBuilderProtocol+HTTPBody.swift +++ b/Sources/HTTPFluent/URLRequestBuilderProtocol+HTTPBody.swift @@ -3,12 +3,17 @@ // HTTPFluent // // Created by Gregory Higley on 2020-05-09. -// Copyright © 2020 Prosumma. All rights reserved. +// Copyright © 2020 Prosumma. +// This code is licensed under the MIT license (see LICENSE for details). // import Combine import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + public extension URLRequestBuilderProtocol { /** Low-level function to set the HTTP body. @@ -16,7 +21,7 @@ public extension URLRequestBuilderProtocol { Don't use this. Use higher-level functions like `post(json:)`, which ultimately call this one. */ - func body(_ body: @escaping () throws -> Data) -> Self { + func body(_ body: @escaping @Sendable () throws -> Data) -> Self { build(URLRequestBuilder.buildBody(body)) } @@ -25,13 +30,18 @@ public extension URLRequestBuilderProtocol { body { data } } + /// Send a `String` without setting an HTTP method. + func send(string: String, encoding: String.Encoding = .utf8) -> Self { + send(data: string.data(using: .utf8)!) + } + /// Send `FormData` without setting an HTTP method. func send(form: FormData) -> Self { content(type: form.contentType).body { form.encoded() } } /// Encode a type in the HTTP body without setting an HTTP method. - func send( + func send( _ request: Request, encoder: Encoder ) -> Self where Encoder.Output == Data { @@ -39,7 +49,7 @@ public extension URLRequestBuilderProtocol { } /// Send JSON in the HTTP body without setting an HTTP method. - func send(json request: Request) -> Self { + func send(json request: Request) -> Self { send(request, encoder: JSONEncoder()).content(type: .json) } @@ -48,13 +58,18 @@ public extension URLRequestBuilderProtocol { send(data: data).method(.post) } + /// Post a `String` + func post(string: String, encoding: String.Encoding = .utf8) -> Self { + send(string: string, encoding: encoding).method(.post) + } + /// Post `FormData` func post(form: FormData) -> Self { send(form: form).method(.post) } /// Post `Request` encoded with `Encoder` - func post( + func post( _ request: Request, encoder: Encoder ) -> Self where Encoder.Output == Data { @@ -62,7 +77,7 @@ public extension URLRequestBuilderProtocol { } /// Post `Request` encoded as JSON - func post(json request: Request) -> Self { + func post(json request: Request) -> Self { send(json: request).method(.post) } @@ -71,13 +86,18 @@ public extension URLRequestBuilderProtocol { send(data: data).method(.patch) } + /// Patch a `String` + func patch(string: String, encoding: String.Encoding = .utf8) -> Self { + send(string: string, encoding: encoding).method(.patch) + } + /// Patch `FormData` func patch(form: FormData) -> Self { send(form: form).method(.patch) } /// Patch `Request` encoded with `Encoder` - func patch( + func patch( _ request: Request, encoder: Encoder ) -> Self where Encoder.Output == Data { @@ -85,7 +105,7 @@ public extension URLRequestBuilderProtocol { } /// Patch `Request` encoded as JSON - func patch(json request: Request) -> Self { + func patch(json request: Request) -> Self { send(json: request).method(.patch) } @@ -94,13 +114,18 @@ public extension URLRequestBuilderProtocol { send(data: data).method(.put) } + /// Put a `String` + func put(string: String, encoding: String.Encoding = .utf8) -> Self { + send(string: string, encoding: encoding).method(.put) + } + /// Put `FormData` func put(form: FormData) -> Self { send(form: form).method(.put) } /// Put `Request` encoded by `Encoder` - func put( + func put( _ request: Request, encoder: Encoder ) -> Self where Encoder.Output == Data { @@ -108,7 +133,7 @@ public extension URLRequestBuilderProtocol { } /// Put `Request` encoded as JSON - func put(json request: Request) -> Self { + func put(json request: Request) -> Self { send(json: request).method(.put) } } diff --git a/Sources/HTTPFluent/URLRequestBuilderProtocol+URL.swift b/Sources/HTTPFluent/URLRequestBuilderProtocol+URL.swift index 3be800c..5e6ecc7 100644 --- a/Sources/HTTPFluent/URLRequestBuilderProtocol+URL.swift +++ b/Sources/HTTPFluent/URLRequestBuilderProtocol+URL.swift @@ -3,7 +3,8 @@ // HTTPFluent // // Created by Gregory Higley on 2020-05-08. -// Copyright © 2020 Prosumma. All rights reserved. +// Copyright © 2020 Prosumma. +// This code is licensed under the MIT license (see LICENSE for details). // import Foundation diff --git a/Sources/HTTPFluent/URLRequestBuilderProtocol+URLRequest.swift b/Sources/HTTPFluent/URLRequestBuilderProtocol+URLRequest.swift index 0df3453..84aecd9 100644 --- a/Sources/HTTPFluent/URLRequestBuilderProtocol+URLRequest.swift +++ b/Sources/HTTPFluent/URLRequestBuilderProtocol+URLRequest.swift @@ -3,9 +3,11 @@ // HTTPFluent // // Created by Gregory Higley on 2020-05-08. -// Copyright © 2020 Prosumma. All rights reserved. +// Copyright © 2020 Prosumma. +// This code is licensed under the MIT license (see LICENSE for details). // +import CoreFoundation import Foundation public extension URLRequestBuilderProtocol { @@ -33,7 +35,8 @@ public extension URLRequestBuilderProtocol { var contentType: String = type.rawValue if let charset = charset { let enc = CFStringConvertNSStringEncodingToEncoding(charset.rawValue) - let iana = CFStringConvertEncodingToIANACharSetName(enc) as NSString as String + let cfiana: CFString = CFStringConvertEncodingToIANACharSetName(enc) + let iana = String(describing: cfiana) contentType += ";charset=\(iana)" } return header(contentType, forField: .contentType) diff --git a/Sources/HTTPFluent/URLRequestBuilderProtocol.swift b/Sources/HTTPFluent/URLRequestBuilderProtocol.swift index 0b37bb6..6bf9f55 100644 --- a/Sources/HTTPFluent/URLRequestBuilderProtocol.swift +++ b/Sources/HTTPFluent/URLRequestBuilderProtocol.swift @@ -3,11 +3,16 @@ // HTTPFluent // // Created by Gregory Higley on 2020-05-08. -// Copyright © 2020 Prosumma. All rights reserved. +// Copyright © 2020 Prosumma. +// This code is licensed under the MIT license (see LICENSE for details). // import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + /// A conforming type may be used to build a `URLRequest`. public protocol URLRequestBuilderProtocol { /** diff --git a/Sources/HTTPFluent/URLResult.swift b/Sources/HTTPFluent/URLResult.swift index 6de8c01..565199d 100644 --- a/Sources/HTTPFluent/URLResult.swift +++ b/Sources/HTTPFluent/URLResult.swift @@ -3,8 +3,10 @@ // HTTPFluent // // Created by Gregory Higley on 2020-08-02. +// Copyright © 2020 Prosumma. +// This code is licensed under the MIT license (see LICENSE for details). // import Foundation -public typealias URLResult = Result +public typealias URLResult = Result diff --git a/Tests/HTTPFluentTests/Const.swift b/Tests/HTTPFluentTests/Const.swift index d9bbbe6..e9b92e6 100644 --- a/Tests/HTTPFluentTests/Const.swift +++ b/Tests/HTTPFluentTests/Const.swift @@ -2,7 +2,9 @@ // Const.swift // HTTPFluent // -// Created by Gregory Higley on 4/1/20. +// Created by Gregory Higley on 2020-04-01. +// Copyright © 2020 Prosumma. +// This code is licensed under the MIT license (see LICENSE for details). // import HTTPFluent diff --git a/Tests/HTTPFluentTests/HTTPBinResponse.swift b/Tests/HTTPFluentTests/HTTPBinResponse.swift index 74f4c71..0113109 100644 --- a/Tests/HTTPFluentTests/HTTPBinResponse.swift +++ b/Tests/HTTPFluentTests/HTTPBinResponse.swift @@ -2,7 +2,9 @@ // HTTPBinResponse.swift // HTTPFluent // -// Created by Gregory Higley on 4/2/20. +// Created by Gregory Higley on 2020-04-02. +// Copyright © 2020 Prosumma. +// This code is licensed under the MIT license (see LICENSE for details). // import Foundation diff --git a/Tests/HTTPFluentTests/HTTPFluentAsyncTests.swift b/Tests/HTTPFluentTests/HTTPFluentAsyncTests.swift index 492d157..a606e49 100644 --- a/Tests/HTTPFluentTests/HTTPFluentAsyncTests.swift +++ b/Tests/HTTPFluentTests/HTTPFluentAsyncTests.swift @@ -2,24 +2,23 @@ // HTTPFluentAsyncTests.swift // // -// Created by Gregory Higley on 12/25/21. +// Created by Gregory Higley on 2021-12-25. +// Copyright © 2020 Prosumma. +// This code is licensed under the MIT license (see LICENSE for details). // -#if canImport(Combine) import Combine -#endif - -import XCTest import HTTPFluent +import Testing -#if swift(>=5.5) - -@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) -class HTTPFluentAsyncTests: XCTestCase { - func testGetJSON() async throws { +struct HTTPFluentAsyncTests { + @Test func testGetJSON() async throws { let slideshows = try await URLClient.bin.path("json").receive(json: Slideshows.self) - XCTAssertEqual(slideshows.slideshow.title, "Sample Slide Show") + #expect(slideshows.slideshow.title == "Sample Slide Show") + } + + @Test func testHeaders() async throws { + let response = try await URLClient.bin.path("headers").content(type: .json, charset: .utf8).receive(json: HTTPHeaderResponse.self) + #expect(response.headers["Content-Type"] == "application/json;charset=utf-8") } } - -#endif diff --git a/Tests/HTTPFluentTests/HTTPFluentCombineTests.swift b/Tests/HTTPFluentTests/HTTPFluentCombineTests.swift index ca1cb58..31cce67 100644 --- a/Tests/HTTPFluentTests/HTTPFluentCombineTests.swift +++ b/Tests/HTTPFluentTests/HTTPFluentCombineTests.swift @@ -1,10 +1,16 @@ -#if canImport(Combine) +// +// HTTPFluentCombineTests.swift +// HTTPFluent +// +// Created by Gregory Higley on 2020-04-01. +// Copyright © 2020 Prosumma. +// This code is licensed under the MIT license (see LICENSE for details). +// import Combine import XCTest import HTTPFluent -@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) func fulfill(_ e: XCTestExpectation, expectError: Bool = false) -> (Subscribers.Completion) -> Void { return { c in if expectError { @@ -21,7 +27,6 @@ func fulfill(_ e: XCTestExpectation, expectError: Bool = false) -> (Su } } -@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) final class HttpFluentCombineTests: XCTestCase { func testGetJSON() { @@ -31,7 +36,7 @@ final class HttpFluentCombineTests: XCTestCase { } let cancellable = URLClient.bin .path("json") - .publisher(json: Slideshows.self) + .receivePublisher(json: Slideshows.self) .sink( receiveCompletion: fulfill(e), receiveValue: print(slideshows:) @@ -47,7 +52,7 @@ final class HttpFluentCombineTests: XCTestCase { .path("post") .post(json: slide) .accept(.json) - .publisher(decode: Decoders.string) + .receivePublisher(decode: Decoders.string) .sink(receiveCompletion: fulfill(e)) { s in print(s) } @@ -62,7 +67,7 @@ final class HttpFluentCombineTests: XCTestCase { .path("status", statusCode) .accept(.json) .method(.put) - .publisher(decode: Decoders.string) + .receivePublisher(decode: Decoders.string) .sink( receiveCompletion: fulfill(e, expectError: true), receiveValue: { _ in XCTFail("Expected HTTP Status \(statusCode).") } @@ -71,5 +76,3 @@ final class HttpFluentCombineTests: XCTestCase { cancellable.cancel() } } - -#endif diff --git a/Tests/HTTPFluentTests/HTTPFluentReceiveTests.swift b/Tests/HTTPFluentTests/HTTPFluentReceiveTests.swift index 440bfe2..03c5859 100644 --- a/Tests/HTTPFluentTests/HTTPFluentReceiveTests.swift +++ b/Tests/HTTPFluentTests/HTTPFluentReceiveTests.swift @@ -2,27 +2,22 @@ // HTTPFluentReceiveTests.swift // HTTPFluentTests // -// Created by Gregory Higley on 8/12/20. +// Created by Gregory Higley on 2020-08-12. +// Copyright © 2020 Prosumma. +// This code is licensed under the MIT license (see LICENSE for details). // -import XCTest import HTTPFluent +import Testing -class HTTPFluentReceiveTests: XCTestCase { - - func testGetJSON() { - let e = expectation(description: "http") - URLClient.bin - .path("json") - .receive(json: Slideshows.self) { result in - do { - try print(result.get()) - } catch { - XCTFail("\(error)") +struct HTTPFluentReceiveTests { + @Test func testGetJSON() async throws { + _ = try await withCheckedThrowingContinuation { cont in + URLClient.bin + .path("json") + .receive(json: Slideshows.self) { result in + cont.resume(with: result) } - e.fulfill() - } - wait(for: [e], timeout: 10) + } } - -} +} \ No newline at end of file diff --git a/Tests/HTTPFluentTests/HTTPHeaderResponse.swift b/Tests/HTTPFluentTests/HTTPHeaderResponse.swift new file mode 100644 index 0000000..48eab1f --- /dev/null +++ b/Tests/HTTPFluentTests/HTTPHeaderResponse.swift @@ -0,0 +1,14 @@ +// +// HTTPHeaderResponse.swift +// HTTPFluent +// +// Created by Gregory Higley on 2023-03-24. +// Copyright © 2023 Prosumma. +// This code is licensed under the MIT license (see LICENSE for details). +// + +import Foundation + +struct HTTPHeaderResponse: Decodable { + let headers: [String: String] +} diff --git a/Tests/HTTPFluentTests/Slide.swift b/Tests/HTTPFluentTests/Slide.swift index b234c92..74ad792 100644 --- a/Tests/HTTPFluentTests/Slide.swift +++ b/Tests/HTTPFluentTests/Slide.swift @@ -1,8 +1,10 @@ // -// File.swift -// +// Slide.swift +// HTTPFluent // -// Created by Gregory Higley on 4/1/20. +// Created by Gregory Higley on 2020-04-01. +// Copyright © 2020 Prosumma. +// This code is licensed under the MIT license (see LICENSE for details). // struct Slide: Codable {