From d4e167ff7774e182d1937a1fc07af8fa3a7e54bd Mon Sep 17 00:00:00 2001 From: Ben Shutt Date: Thu, 2 Jan 2025 10:32:35 +0000 Subject: [PATCH 01/14] Breaking change: Refactor `URLRequestMaker` so that the HTTP body is defined as a property Use `self` in `DecodableRequest` as the `URLRequestConvertible`. By default, the "Accept: application/json" is no longer added to `DecodableRequest`. Refactor HTTP headers, removing `additionalHeaders` --- README.md | 10 +-- Sources/Body/DataBody.swift | 23 ------- Sources/Body/JSONBody.swift | 45 ------------- Sources/Body/RequestBody.swift | 20 ------ Sources/DecodableRequest.swift | 31 ++++----- .../Extensions/HTTPHeader+Extensions.swift | 5 ++ Sources/HTTPBody.swift | 61 ++++++++++++++++++ Sources/URLRequestMaker.swift | 64 ++++++++++++------- Tests/Extensions/DateFormatter+ISO8601.swift | 49 -------------- Tests/Extensions/Session+Extensions.swift | 19 ------ Tests/WorldTime/GetWorldTime.swift | 33 +++++++--- Tests/WorldTime/WorldTime.swift | 1 + ...equestTests.swift => WorldTimeTests.swift} | 11 ++-- 13 files changed, 153 insertions(+), 219 deletions(-) delete mode 100644 Sources/Body/DataBody.swift delete mode 100644 Sources/Body/JSONBody.swift delete mode 100644 Sources/Body/RequestBody.swift create mode 100644 Sources/HTTPBody.swift delete mode 100644 Tests/Extensions/DateFormatter+ISO8601.swift delete mode 100644 Tests/Extensions/Session+Extensions.swift rename Tests/{JSONDataRequestTests.swift => WorldTimeTests.swift} (58%) diff --git a/README.md b/README.md index a74b344..857bce8 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ Modularization of data requests using Alamofire with concurrency. -This package encourages a design pattern where the configuration of an endpoint is encapsulated into the properties of a structure. +This package encourages a design pattern where the _description_ of an endpoint is encapsulated into the properties of a structure. A similar design to a SwiftUI `View`. -It adds rather than replaces; direct use of Alamofire (or vanilla `URLSession`) is still encouraged. +It adds rather than replaces; direct use of Alamofire (or `URLSession`) is still encouraged. There is also some helpful shorthand. ## Example Usage @@ -46,13 +46,9 @@ dependencies: [ ] ``` -## Notes - -* The `URLRequestMaker` checks for conformance of `RequestBody` and adds the HTTP body accordingly -* A `DecodableRequest` is a `URLRequestMaker` with the configuration properties of a data request defaulted - ## Uploads +A `DecodableRequest` is a `URLRequestMaker` with the configuration properties of a data request defaulted. Since `URLRequestMaker` conforms to `URLRequestConvertible` you can use Alamofire directly: ```swift diff --git a/Sources/Body/DataBody.swift b/Sources/Body/DataBody.swift deleted file mode 100644 index 0e6a6f7..0000000 --- a/Sources/Body/DataBody.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// DataBody.swift -// DataRequest -// -// Created by Ben Shutt on 18/09/2023. -// Copyright © 2023 Ben Shutt. All rights reserved. -// - -import Foundation -import Alamofire - -/// Defines a data request body -public protocol DataBody: RequestBody {} - -// MARK: - Extensions - -public extension DataBody { - - /// Defaults to `.contentTypeData` - var contentType: HTTPHeader { - .contentTypeData - } -} diff --git a/Sources/Body/JSONBody.swift b/Sources/Body/JSONBody.swift deleted file mode 100644 index 5475c66..0000000 --- a/Sources/Body/JSONBody.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// JSONBody.swift -// DataRequest -// -// Created by Ben Shutt on 18/09/2023. -// Copyright © 2023 Ben Shutt. All rights reserved. -// - -import Foundation -import Alamofire - -/// Defines a JSON request body -public protocol JSONBody: RequestBody { - - /// The request body that will be encoded into JSON data - associatedtype Body: Encodable - - /// Make request body that will be encoded into JSON data - var jsonBody: Body { get } - - /// The encoder which makes the JSON data - var encoder: JSONEncoder { get } -} - -// MARK: - Extensions - -public extension JSONBody { - - /// Defaults to `.contentTypeJSON` - var contentType: HTTPHeader { - .contentTypeJSON - } - - /// Defaults to `JSONEncoder()` - var encoder: JSONEncoder { - JSONEncoder() - } - - /// Encode `jsonBody` with `encoder` - var body: Data { - get throws { - try encoder.encode(jsonBody) - } - } -} diff --git a/Sources/Body/RequestBody.swift b/Sources/Body/RequestBody.swift deleted file mode 100644 index 44a52ab..0000000 --- a/Sources/Body/RequestBody.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// RequestBody.swift -// DataRequest -// -// Created by Ben Shutt on 18/09/2023. -// Copyright © 2023 Ben Shutt. All rights reserved. -// - -import Foundation -import Alamofire - -/// Defines a HTTP request body -public protocol RequestBody { - - /// `HTTPHeader` for the content type of the body - var contentType: HTTPHeader { get } - - /// Request body data - var body: Data { get throws } -} diff --git a/Sources/DecodableRequest.swift b/Sources/DecodableRequest.swift index 8c45e44..f4a4a86 100644 --- a/Sources/DecodableRequest.swift +++ b/Sources/DecodableRequest.swift @@ -11,23 +11,23 @@ import Alamofire /// An endpoint that executes a URL request and decodes the response into a model. public protocol DecodableRequest: URLRequestMaker { - + /// Model expected in the response body that will be decoded from data. /// - Note: `Empty` can be used when a response is empty. E.g. on HTTP status code 204. associatedtype ResponseBody: Decodable - + /// The Alamofire session. /// Defaults to `.default`. var session: Session { get } - + /// Define how the response data should be decoded. /// Defaults to `JSONDecoder()`. var decoder: DataDecoder { get } - + /// The request interceptor. /// Defaults to `nil`. var interceptor: RequestInterceptor? { get } - + /// Validate the response. /// Defaults to `true`. var validate: Bool { get } @@ -36,42 +36,35 @@ public protocol DecodableRequest: URLRequestMaker { // MARK: - Extensions public extension DecodableRequest { - + /// Defaults to `.default` var session: Session { .default } - + /// Defaults to `JSONDecoder()` var decoder: DataDecoder { JSONDecoder() } - + /// Defaults to `nil` var interceptor: RequestInterceptor? { nil } - + /// Defaults to `true` var validate: Bool { true } - - // MARK: URLRequestMaker - - /// Defaults to `[.acceptJSON]` - var additionalHeaders: HTTPHeaders { - [.acceptJSON] - } - + // MARK: Request - + /// Execute the data request decoding the response /// - Returns: The response body @discardableResult func request() async throws -> ResponseBody { try await session.request( - urlRequest, + self, interceptor: interceptor ) .decodeValue( diff --git a/Sources/Extensions/HTTPHeader+Extensions.swift b/Sources/Extensions/HTTPHeader+Extensions.swift index 6478b96..e197c13 100644 --- a/Sources/Extensions/HTTPHeader+Extensions.swift +++ b/Sources/Extensions/HTTPHeader+Extensions.swift @@ -19,4 +19,9 @@ public extension HTTPHeader { /// `"Content-Type: application/octet-stream"` static let contentTypeData: HTTPHeader = .contentType("application/octet-stream") + + /// `"Content-Type: application/x-www-form-urlencoded; charset=utf-8"` + static let contentTypeForm: HTTPHeader = .contentType( + "application/x-www-form-urlencoded; charset=utf-8" + ) } diff --git a/Sources/HTTPBody.swift b/Sources/HTTPBody.swift new file mode 100644 index 0000000..4a83749 --- /dev/null +++ b/Sources/HTTPBody.swift @@ -0,0 +1,61 @@ +// +// HTTPBody.swift +// DataRequest +// +// Created by Ben Shutt on 18/09/2023. +// Copyright © 2023 Ben Shutt. All rights reserved. +// + +import Foundation +import Alamofire + +/// Defines a HTTP request body +public struct HTTPBody { + + /// `HTTPHeader` for the content type of the body + public var contentType: HTTPHeader + + /// Request body data + public var body: Data + + /// Public memberwise initializer + public init(contentType: HTTPHeader, body: Data) { + self.contentType = contentType + self.body = body + } +} + +// MARK: - HTTPBody + JSON + +extension HTTPBody { + + /// Make a JSON request body + /// - Parameters: + /// - model: Object that will be encoded into JSON data + /// - encoder: The encoder which makes the JSON data + /// - Returns: The JSON request body + static func json( + _ encodable: some Encodable, + encoder: JSONEncoder = JSONEncoder() + ) throws -> HTTPBody { + try HTTPBody( + contentType: .contentTypeJSON, + body: encoder.encode(encodable) + ) + } +} + +// MARK: - HTTPBody + Data + +extension HTTPBody { + + /// Make a `Data` request body + /// - Parameter data: The data of the body + /// - Returns: The `Data` request body + static func data(_ data: Data) -> HTTPBody { + HTTPBody( + contentType: .contentTypeData, + body: data + ) + } +} diff --git a/Sources/URLRequestMaker.swift b/Sources/URLRequestMaker.swift index d60f778..8954b2a 100644 --- a/Sources/URLRequestMaker.swift +++ b/Sources/URLRequestMaker.swift @@ -10,60 +10,80 @@ import Foundation import Alamofire /// An entity which builds a `URLRequest`. -/// If the entity (also) conforms to `RequestBody` then the HTTP body and content type header is set. public protocol URLRequestMaker: URLRequestConvertible { /// The components of a URL. var urlComponents: URLComponents { get } /// The HTTP method. + /// /// Defaults to `.get`. var method: HTTPMethod { get } - /// Additional headers to append to the default. - /// Defaults to empty. - var additionalHeaders: HTTPHeaders { get } + /// The default HTTP headers. + /// + /// Defaults to `.default` (from Alamofire). + /// + /// - Note: The final `URLRequest` should be considered the source of truth. + /// For example, the content type might be added while making the `URLRequest`. + var headers: HTTPHeaders { get } + + /// The HTTP body. + /// + /// Defaults to `nil` + var body: HTTPBody? { get throws } + + /// The `URLRequest` constructed from the other properties, before updates + var urlRequest: URLRequest { get throws } + + // MARK: URLRequestConvertible + + /// Apply updates to the `urlRequest` before returning. + /// For example, applying `URLEncoding`. + /// + /// By default, returns `urlRequest`. + /// Conformers may provide their own implementation using `urlRequest`. + func asURLRequest() throws -> URLRequest } -// MARK: - Extensions +// MARK: - Defaults public extension URLRequestMaker { - - /// Defaults to `.get` var method: HTTPMethod { .get } - /// Defaults to empty - var additionalHeaders: HTTPHeaders { - [] + var headers: HTTPHeaders { + .default } - /// Get all HTTP headers - var headers: HTTPHeaders { - var headers: HTTPHeaders = .default - if let requestBody = self as? RequestBody { - headers.append(requestBody.contentType) - } - headers.append(additionalHeaders) - return headers + var body: HTTPBody? { + nil } - /// Make the URL request var urlRequest: URLRequest { get throws { + let body = try body + + var headers = headers + if let contentType = body?.contentType { + headers.append(contentType) + } + var request = try URLRequest( url: urlComponents, method: method, headers: headers ) - if let requestBody = self as? RequestBody { - request.httpBody = try requestBody.body - } + request.httpBody = body?.body return request } } +} + +// MARK: - URLRequestConvertible +public extension URLRequestMaker { func asURLRequest() throws -> URLRequest { try urlRequest } diff --git a/Tests/Extensions/DateFormatter+ISO8601.swift b/Tests/Extensions/DateFormatter+ISO8601.swift deleted file mode 100644 index 9d3edc3..0000000 --- a/Tests/Extensions/DateFormatter+ISO8601.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// ISO8601DateFormatter+Extensions.swift -// DataRequest -// -// Created by Ben Shutt on 18/09/2023. -// Copyright © 2023 Ben Shutt. All rights reserved. -// - -import Foundation - -// MARK: - DateFormatter + ISO8601 - -extension DateFormatter { - - /// An ISO8601 `DateFormatter` with milliseconds. - static var iso8601Millis: DateFormatter { - iso8601Formatter(dateFormat: .iso8601Millis) - } - - /// An ISO8601 `DateFormatter` with: - /// - Calendar identifier `.iso8601` - /// - Locale identifier `"en_US_POSIX"` - /// - Given `timeZone`, defaults to `.current` - /// - Given `dateFormat` - /// - /// - Parameters: - /// - timeZone: The time zone, defaults to `.current` - /// - dateFormat: The date format - /// - Returns: `DateFormatter` - private static func iso8601Formatter( - timeZone: TimeZone? = .current, - dateFormat: String - ) -> DateFormatter { - let formatter = DateFormatter() - formatter.calendar = Calendar(identifier: .iso8601) - formatter.locale = Locale(identifier: "en_US_POSIX") - formatter.timeZone = timeZone - formatter.dateFormat = dateFormat - return formatter - } -} - -// MARK: - String + ISO8601 - -private extension String { - - /// Date format for ISO8601 including milliseconds - static let iso8601Millis: String = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ" -} diff --git a/Tests/Extensions/Session+Extensions.swift b/Tests/Extensions/Session+Extensions.swift deleted file mode 100644 index 4227154..0000000 --- a/Tests/Extensions/Session+Extensions.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// Session+Extensions.swift -// DataRequest -// -// Created by Ben Shutt on 18/09/2023. -// Copyright © 2023 Ben Shutt. All rights reserved. -// - -import Foundation -import Alamofire -import DataRequest - -extension Session { - - /// Make custom `Session` with event monitors - static let worldTime = Session(eventMonitors: [ - ResponseEventMonitor() - ]) -} diff --git a/Tests/WorldTime/GetWorldTime.swift b/Tests/WorldTime/GetWorldTime.swift index 1aeadae..c2a42c6 100644 --- a/Tests/WorldTime/GetWorldTime.swift +++ b/Tests/WorldTime/GetWorldTime.swift @@ -16,23 +16,24 @@ struct GetWorldTime: DecodableRequest { /// Decode response as `WorldTime` typealias ResponseBody = WorldTime - /// The time zone to fetch, defaults to `"Europe/London"` - var timeZone: String + /// The time zone to fetch + let timeZone: String /// The Alamofire session - var session: Session { - .worldTime - } + let session: Session = .worldTime - /// Define a specific `JSONDecoder` - var decoder: DataDecoder { + /// Define a specific `JSONDecoder` with snake case and ISO8601 dates + let decoder: DataDecoder = { let decoder = JSONDecoder() - decoder.dateDecodingStrategy = .formatted(.iso8601Millis) decoder.keyDecodingStrategy = .convertFromSnakeCase + decoder.dateDecodingStrategy = .custom { decoder in + let iso8601 = try decoder.singleValueContainer().decode(String.self) + let format = Date.ISO8601FormatStyle(includingFractionalSeconds: true) + return try format.parse(iso8601) + } return decoder - } + }() - /// `URLComponents` for World Time var urlComponents: URLComponents { var urlComponents = URLComponents() urlComponents.scheme = "https" @@ -40,4 +41,16 @@ struct GetWorldTime: DecodableRequest { urlComponents.path = "/api/timezone/\(timeZone)" return urlComponents } + + var headers: HTTPHeaders { + .default.appending(.acceptJSON) + } +} + +// MARK: - Session + WorldTime + +private extension Session { + static let worldTime = Session(eventMonitors: [ + ResponseEventMonitor() + ]) } diff --git a/Tests/WorldTime/WorldTime.swift b/Tests/WorldTime/WorldTime.swift index d295411..05ccc12 100644 --- a/Tests/WorldTime/WorldTime.swift +++ b/Tests/WorldTime/WorldTime.swift @@ -12,6 +12,7 @@ import Foundation /// http://worldtimeapi.org struct WorldTime: Decodable { var abbreviation: String + var unixtime: Int var datetime: Date var dstOffset: Int var rawOffset: Int diff --git a/Tests/JSONDataRequestTests.swift b/Tests/WorldTimeTests.swift similarity index 58% rename from Tests/JSONDataRequestTests.swift rename to Tests/WorldTimeTests.swift index 930825a..34e7a37 100644 --- a/Tests/JSONDataRequestTests.swift +++ b/Tests/WorldTimeTests.swift @@ -1,19 +1,20 @@ // -// JSONDataRequestTests.swift +// WorldTimeTests.swift // DataRequestTests // // Created by Ben Shutt on 18/09/2023. // Copyright © 2023 Ben Shutt. All rights reserved. // -import XCTest +import Testing @testable import DataRequest -final class JSONDataRequestTests: XCTestCase { +/// _Integration_ tests using the World Time API +@Suite struct WorldTimeTests { private let timeZone = "Europe/London" - func test() async throws { + @Test func test() async throws { let worldTime = try await GetWorldTime(timeZone: timeZone).request() - XCTAssertEqual(worldTime.timezone, timeZone) + #expect(worldTime.timezone == timeZone) } } From 699569b1a9cb650118d3d22ac8c38f84a8d8a285 Mon Sep 17 00:00:00 2001 From: Ben Shutt Date: Thu, 2 Jan 2025 11:01:04 +0000 Subject: [PATCH 02/14] Swift 6 (Sendable) and linting updates --- Package.resolved | 13 ++++++++-- Package.swift | 25 +++++++++++++------ Sources/DecodableRequest.swift | 24 +++++++++--------- .../Extensions/DataRequest+Extensions.swift | 2 +- 4 files changed, 41 insertions(+), 23 deletions(-) diff --git a/Package.resolved b/Package.resolved index 8d8c472..3c3f27f 100644 --- a/Package.resolved +++ b/Package.resolved @@ -5,8 +5,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/Alamofire/Alamofire.git", "state" : { - "revision" : "b2fa556e4e48cbf06cf8c63def138c98f4b811fa", - "version" : "5.8.0" + "revision" : "513364f870f6bfc468f9d2ff0a95caccc10044c5", + "version" : "5.10.2" + } + }, + { + "identity" : "swiftlintplugins", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SimplyDanny/SwiftLintPlugins", + "state" : { + "revision" : "f9731bef175c3eea3a0ca960f1be78fcc2bc7853", + "version" : "0.57.1" } } ], diff --git a/Package.swift b/Package.swift index 29a48b4..0a52405 100644 --- a/Package.swift +++ b/Package.swift @@ -1,35 +1,44 @@ -// swift-tools-version: 5.9 +// swift-tools-version: 6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription +let name = "DataRequest" let package = Package( - name: "DataRequest", + name: name, platforms: [ .macOS(.v13), .iOS(.v16) ], products: [ .library( - name: "DataRequest", - targets: ["DataRequest"] + name: name, + targets: [name] ) ], dependencies: [ .package( url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.8.0") + ), + .package( + url: "https://github.com/SimplyDanny/SwiftLintPlugins", + .upToNextMajor(from: "0.0.0") ) ], targets: [ .target( - name: "DataRequest", + name: name, dependencies: ["Alamofire"], - path: "Sources" + path: "Sources", + plugins: [.plugin( + name: "SwiftLintBuildToolPlugin", + package: "SwiftLintPlugins" + )] ), .testTarget( - name: "DataRequestTests", - dependencies: ["DataRequest"], + name: "\(name)Tests", + dependencies: [.byName(name: name)], path: "Tests" ) ] diff --git a/Sources/DecodableRequest.swift b/Sources/DecodableRequest.swift index f4a4a86..fe6c111 100644 --- a/Sources/DecodableRequest.swift +++ b/Sources/DecodableRequest.swift @@ -11,23 +11,23 @@ import Alamofire /// An endpoint that executes a URL request and decodes the response into a model. public protocol DecodableRequest: URLRequestMaker { - + /// Model expected in the response body that will be decoded from data. /// - Note: `Empty` can be used when a response is empty. E.g. on HTTP status code 204. - associatedtype ResponseBody: Decodable - + associatedtype ResponseBody: Decodable & Sendable + /// The Alamofire session. /// Defaults to `.default`. var session: Session { get } - + /// Define how the response data should be decoded. /// Defaults to `JSONDecoder()`. var decoder: DataDecoder { get } - + /// The request interceptor. /// Defaults to `nil`. var interceptor: RequestInterceptor? { get } - + /// Validate the response. /// Defaults to `true`. var validate: Bool { get } @@ -36,29 +36,29 @@ public protocol DecodableRequest: URLRequestMaker { // MARK: - Extensions public extension DecodableRequest { - + /// Defaults to `.default` var session: Session { .default } - + /// Defaults to `JSONDecoder()` var decoder: DataDecoder { JSONDecoder() } - + /// Defaults to `nil` var interceptor: RequestInterceptor? { nil } - + /// Defaults to `true` var validate: Bool { true } - + // MARK: Request - + /// Execute the data request decoding the response /// - Returns: The response body @discardableResult diff --git a/Sources/Extensions/DataRequest+Extensions.swift b/Sources/Extensions/DataRequest+Extensions.swift index 40f7a5a..6a67771 100644 --- a/Sources/Extensions/DataRequest+Extensions.swift +++ b/Sources/Extensions/DataRequest+Extensions.swift @@ -24,7 +24,7 @@ public extension DataRequest { /// - validate: If true, validate the response, defaults to `true` /// - decoder: The data decoder to use, defaults to `JSONDecoder()` /// - Returns: `ResponseBody` - func decodeValue( + func decodeValue( _ responseBody: ResponseBody.Type, validate: Bool = true, decoder: DataDecoder = JSONDecoder() From 4fb43cd41066676ae6ab8e8cf0257c7446aa9a7a Mon Sep 17 00:00:00 2001 From: Ben Shutt Date: Thu, 2 Jan 2025 11:11:14 +0000 Subject: [PATCH 03/14] Update WorldTimeTests.swift --- Tests/WorldTimeTests.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Tests/WorldTimeTests.swift b/Tests/WorldTimeTests.swift index 34e7a37..bae5188 100644 --- a/Tests/WorldTimeTests.swift +++ b/Tests/WorldTimeTests.swift @@ -12,9 +12,23 @@ import Testing /// _Integration_ tests using the World Time API @Suite struct WorldTimeTests { private let timeZone = "Europe/London" + private let expectedHeaderKeys = [ + "Accept", + "Accept-Encoding", + "User-Agent", + "Accept-Language" + ] @Test func test() async throws { let worldTime = try await GetWorldTime(timeZone: timeZone).request() #expect(worldTime.timezone == timeZone) } + + @Test func headers() async throws { + let endpoint = GetWorldTime(timeZone: timeZone) + let urlRequest = try endpoint.asURLRequest() + let headers = urlRequest.allHTTPHeaderFields ?? [:] + #expect(Set(expectedHeaderKeys) == Set(headers.keys)) + #expect(headers["Accept"] == "application/json") + } } From 2d8073ca2dacecca2e596f9f008dc19394e14280 Mon Sep 17 00:00:00 2001 From: Ben Shutt Date: Thu, 2 Jan 2025 11:20:16 +0000 Subject: [PATCH 04/14] Update docs --- Sources/URLRequestMaker.swift | 11 ++++++----- Tests/WorldTimeTests.swift | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Sources/URLRequestMaker.swift b/Sources/URLRequestMaker.swift index 8954b2a..2a0ec99 100644 --- a/Sources/URLRequestMaker.swift +++ b/Sources/URLRequestMaker.swift @@ -9,7 +9,7 @@ import Foundation import Alamofire -/// An entity which builds a `URLRequest`. +/// An entity that builds a `URLRequest`. public protocol URLRequestMaker: URLRequestConvertible { /// The components of a URL. @@ -25,15 +25,16 @@ public protocol URLRequestMaker: URLRequestConvertible { /// Defaults to `.default` (from Alamofire). /// /// - Note: The final `URLRequest` should be considered the source of truth. - /// For example, the content type might be added while making the `URLRequest`. + /// For example, the content type might be added while making the `URLRequest` + /// but may not be defined here. var headers: HTTPHeaders { get } /// The HTTP body. /// - /// Defaults to `nil` + /// Defaults to `nil`. var body: HTTPBody? { get throws } - /// The `URLRequest` constructed from the other properties, before updates + /// The `URLRequest` constructed from the other properties, before updates. var urlRequest: URLRequest { get throws } // MARK: URLRequestConvertible @@ -42,7 +43,7 @@ public protocol URLRequestMaker: URLRequestConvertible { /// For example, applying `URLEncoding`. /// /// By default, returns `urlRequest`. - /// Conformers may provide their own implementation using `urlRequest`. + /// Conformers may provide their own implementation mutating `urlRequest`. func asURLRequest() throws -> URLRequest } diff --git a/Tests/WorldTimeTests.swift b/Tests/WorldTimeTests.swift index bae5188..6334cf4 100644 --- a/Tests/WorldTimeTests.swift +++ b/Tests/WorldTimeTests.swift @@ -15,8 +15,8 @@ import Testing private let expectedHeaderKeys = [ "Accept", "Accept-Encoding", + "Accept-Language", "User-Agent", - "Accept-Language" ] @Test func test() async throws { From d1dcd394cc12e4bf3e3b46e117de6f2d3dbfb723 Mon Sep 17 00:00:00 2001 From: Ben Shutt Date: Thu, 2 Jan 2025 11:23:10 +0000 Subject: [PATCH 05/14] Update README.md --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 857bce8..e7debfb 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ There is also some helpful shorthand. Define a decodable model returned in a response: ```swift -struct Model: Decodable { ... } +struct Model: Decodable, Sendable { ... } ``` Specify the configuration of the request endpoint: @@ -21,9 +21,10 @@ Specify the configuration of the request endpoint: struct GetModel: DecodableRequest { typealias ResponseBody = Model - var urlComponents: URLComponents { - ... - } + var urlComponents: URLComponents { ... } + var method: HTTPMethod { ... } + var headers: HTTPHeaders { ... } + var body: HTTPBody? { ... } } ``` From 9fccc9869ebfd1d0cf1f45464f31255c3e833a46 Mon Sep 17 00:00:00 2001 From: Ben Shutt Date: Thu, 2 Jan 2025 14:34:45 +0000 Subject: [PATCH 06/14] Doc updates --- Package.resolved | 3 ++- Sources/DecodableRequest.swift | 9 ++++----- Sources/URLRequestMaker.swift | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Package.resolved b/Package.resolved index 3c3f27f..a37a32b 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,4 +1,5 @@ { + "originHash" : "31df4364fdc6ae19e3af4f8ee53be6d4ab3d21a274ea58ef7aea638537a35ba4", "pins" : [ { "identity" : "alamofire", @@ -19,5 +20,5 @@ } } ], - "version" : 2 + "version" : 3 } diff --git a/Sources/DecodableRequest.swift b/Sources/DecodableRequest.swift index fe6c111..9b4bec5 100644 --- a/Sources/DecodableRequest.swift +++ b/Sources/DecodableRequest.swift @@ -17,18 +17,22 @@ public protocol DecodableRequest: URLRequestMaker { associatedtype ResponseBody: Decodable & Sendable /// The Alamofire session. + /// /// Defaults to `.default`. var session: Session { get } /// Define how the response data should be decoded. + /// /// Defaults to `JSONDecoder()`. var decoder: DataDecoder { get } /// The request interceptor. + /// /// Defaults to `nil`. var interceptor: RequestInterceptor? { get } /// Validate the response. + /// /// Defaults to `true`. var validate: Bool { get } } @@ -36,23 +40,18 @@ public protocol DecodableRequest: URLRequestMaker { // MARK: - Extensions public extension DecodableRequest { - - /// Defaults to `.default` var session: Session { .default } - /// Defaults to `JSONDecoder()` var decoder: DataDecoder { JSONDecoder() } - /// Defaults to `nil` var interceptor: RequestInterceptor? { nil } - /// Defaults to `true` var validate: Bool { true } diff --git a/Sources/URLRequestMaker.swift b/Sources/URLRequestMaker.swift index 2a0ec99..66b5d18 100644 --- a/Sources/URLRequestMaker.swift +++ b/Sources/URLRequestMaker.swift @@ -40,10 +40,10 @@ public protocol URLRequestMaker: URLRequestConvertible { // MARK: URLRequestConvertible /// Apply updates to the `urlRequest` before returning. + /// Conformers may provide their own implementation mutating `urlRequest`. /// For example, applying `URLEncoding`. /// /// By default, returns `urlRequest`. - /// Conformers may provide their own implementation mutating `urlRequest`. func asURLRequest() throws -> URLRequest } From 8b786c7e706505c5341675ec59b6291b4ee35ba2 Mon Sep 17 00:00:00 2001 From: Ben Shutt Date: Thu, 2 Jan 2025 15:03:23 +0000 Subject: [PATCH 07/14] Add SwiftyMocky dependency and add script WIP Commit before swiftymocky setup --- Package.resolved | 92 +++++++++++++++++++++++++++++++++++++++++++++++- Package.swift | 4 +++ README.md | 6 ++++ Scripts/mocks.sh | 28 +++++++++++++++ 4 files changed, 129 insertions(+), 1 deletion(-) create mode 100755 Scripts/mocks.sh diff --git a/Package.resolved b/Package.resolved index a37a32b..b984142 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,6 +1,15 @@ { - "originHash" : "31df4364fdc6ae19e3af4f8ee53be6d4ab3d21a274ea58ef7aea638537a35ba4", + "originHash" : "8ad217aede9c548580609389bcaab5b4f07942fea62c262879da274ef79a6fc1", "pins" : [ + { + "identity" : "aexml", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tadija/AEXML.git", + "state" : { + "revision" : "38f7d00b23ecd891e1ee656fa6aeebd6ba04ecc3", + "version" : "4.6.1" + } + }, { "identity" : "alamofire", "kind" : "remoteSourceControl", @@ -10,6 +19,60 @@ "version" : "5.10.2" } }, + { + "identity" : "chalk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/luoxiu/Chalk", + "state" : { + "revision" : "8a9d3373bd754fb62f7881d9f639d376f5e4d5a5", + "version" : "0.2.1" + } + }, + { + "identity" : "commander", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kylef/Commander", + "state" : { + "revision" : "4a1f2fb82fb6cef613c4a25d2e38f702e4d812c2", + "version" : "0.9.2" + } + }, + { + "identity" : "pathkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kylef/PathKit", + "state" : { + "revision" : "3bfd2737b700b9a36565a8c94f4ad2b050a5e574", + "version" : "1.0.1" + } + }, + { + "identity" : "rainbow", + "kind" : "remoteSourceControl", + "location" : "https://github.com/luoxiu/Rainbow", + "state" : { + "revision" : "9d8fcb4c64e816a135c31162a0c319e9f1f09c35", + "version" : "0.1.1" + } + }, + { + "identity" : "shellout", + "kind" : "remoteSourceControl", + "location" : "https://github.com/JohnSundell/ShellOut", + "state" : { + "revision" : "e1577acf2b6e90086d01a6d5e2b8efdaae033568", + "version" : "2.3.0" + } + }, + { + "identity" : "spectre", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kylef/Spectre.git", + "state" : { + "revision" : "26cc5e9ae0947092c7139ef7ba612e34646086c7", + "version" : "0.10.1" + } + }, { "identity" : "swiftlintplugins", "kind" : "remoteSourceControl", @@ -18,6 +81,33 @@ "revision" : "f9731bef175c3eea3a0ca960f1be78fcc2bc7853", "version" : "0.57.1" } + }, + { + "identity" : "swiftymocky", + "kind" : "remoteSourceControl", + "location" : "https://github.com/MakeAWishFoundation/SwiftyMocky", + "state" : { + "revision" : "1e81c0c566c26d2d4e4cc2d799afad7d3ef931ab", + "version" : "4.2.0" + } + }, + { + "identity" : "xcodeproj", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tuist/xcodeproj", + "state" : { + "revision" : "6e971133653f069b7699d5fb081e5db1e5f81559", + "version" : "8.26.1" + } + }, + { + "identity" : "yams", + "kind" : "remoteSourceControl", + "location" : "https://github.com/jpsim/Yams", + "state" : { + "revision" : "81a65c4069c28011ee432f2858ba0de49b086677", + "version" : "3.0.1" + } } ], "version" : 3 diff --git a/Package.swift b/Package.swift index 0a52405..e400b3c 100644 --- a/Package.swift +++ b/Package.swift @@ -24,6 +24,10 @@ let package = Package( .package( url: "https://github.com/SimplyDanny/SwiftLintPlugins", .upToNextMajor(from: "0.0.0") + ), + .package( + url: "https://github.com/MakeAWishFoundation/SwiftyMocky", + .upToNextMajor(from: "4.0.0") ) ], targets: [ diff --git a/README.md b/README.md index e7debfb..b20f7cf 100644 --- a/README.md +++ b/README.md @@ -75,3 +75,9 @@ extension Session { ``` This can be returned in the `session` property of the `DecodableRequest`. + +## Dependencies + +The unit tests use [SwiftyMocky](https://github.com/MakeAWishFoundation/SwiftyMocky). +As documented in the [README](https://github.com/MakeAWishFoundation/SwiftyMocky?tab=readme-ov-file#installation), this uses the CLI `mint`. + diff --git a/Scripts/mocks.sh b/Scripts/mocks.sh new file mode 100755 index 0000000..bfa75df --- /dev/null +++ b/Scripts/mocks.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +# +# Script: mocks.sh +# Usage: ./mocks.sh +# +# Description: +# Generates mock file(s) using SwiftyMocky. +# +# Installation: +# https://github.com/MakeAWishFoundation/SwiftyMocky?tab=readme-ov-file#2-installing-swiftymocky-cli +# + +# Set defaults +set -o nounset -o errexit -o errtrace -o pipefail + +# Path to swiftymocky binary +EXE="${HOME}/.mint/bin/swiftymocky" + +# Check the swiftymocky executable exists +if ! [[ -x "$(command -v "${EXE}")" ]]; then + echo "Command not found '${EXE}'." 1>&2 + echo "Please see the docs of $0 for installation steps." 1>&2 + exit 1 +fi + +# Validate the setup +"${EXE}" doctor \ No newline at end of file From c13e1886f6635954b29ac83246072c861fe2fb19 Mon Sep 17 00:00:00 2001 From: Ben Shutt Date: Thu, 2 Jan 2025 18:06:12 +0000 Subject: [PATCH 08/14] SwiftyMocky WIP --- Mintfile | 2 ++ Mockfile | 13 +++++++++++++ Package.resolved | 10 +++++----- Package.swift | 7 +++++-- Scripts/mocks.sh | 16 +++++++++++----- 5 files changed, 36 insertions(+), 12 deletions(-) create mode 100644 Mintfile create mode 100644 Mockfile diff --git a/Mintfile b/Mintfile new file mode 100644 index 0000000..82d421c --- /dev/null +++ b/Mintfile @@ -0,0 +1,2 @@ +MakeAWishFoundation/SwiftyMocky@master +krzysztofzablocki/Sourcery@2.2.6 diff --git a/Mockfile b/Mockfile new file mode 100644 index 0000000..3e6a482 --- /dev/null +++ b/Mockfile @@ -0,0 +1,13 @@ +unit.tests.mock: + sources: + include: + - ./Sources + output: + ./Tests/Mocks/Mock.generated.swift + targets: + - DataRequestTests + testable: + - DataRequest + import: + - Foundation + - Alamofire \ No newline at end of file diff --git a/Package.resolved b/Package.resolved index b984142..3796bbc 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "8ad217aede9c548580609389bcaab5b4f07942fea62c262879da274ef79a6fc1", + "originHash" : "9b920edae1514aefb776c527caffbc6acff227b34ec8ed7c177749cbf6ef8b7e", "pins" : [ { "identity" : "aexml", @@ -87,8 +87,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/MakeAWishFoundation/SwiftyMocky", "state" : { - "revision" : "1e81c0c566c26d2d4e4cc2d799afad7d3ef931ab", - "version" : "4.2.0" + "branch" : "master", + "revision" : "3672eea08c7098214ac54ee26c7a7e39ea01a2f1" } }, { @@ -105,8 +105,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/jpsim/Yams", "state" : { - "revision" : "81a65c4069c28011ee432f2858ba0de49b086677", - "version" : "3.0.1" + "revision" : "3036ba9d69cf1fd04d433527bc339dc0dc75433d", + "version" : "5.1.3" } } ], diff --git a/Package.swift b/Package.swift index e400b3c..d20dfe5 100644 --- a/Package.swift +++ b/Package.swift @@ -27,7 +27,7 @@ let package = Package( ), .package( url: "https://github.com/MakeAWishFoundation/SwiftyMocky", - .upToNextMajor(from: "4.0.0") + branch: "master" // Should match Mintfile ) ], targets: [ @@ -42,7 +42,10 @@ let package = Package( ), .testTarget( name: "\(name)Tests", - dependencies: [.byName(name: name)], + dependencies: [ + .byName(name: name), + "SwiftyMocky" + ], path: "Tests" ) ] diff --git a/Scripts/mocks.sh b/Scripts/mocks.sh index bfa75df..1a4819a 100755 --- a/Scripts/mocks.sh +++ b/Scripts/mocks.sh @@ -5,7 +5,7 @@ # Usage: ./mocks.sh # # Description: -# Generates mock file(s) using SwiftyMocky. +# Generates mock files using SwiftyMocky. # # Installation: # https://github.com/MakeAWishFoundation/SwiftyMocky?tab=readme-ov-file#2-installing-swiftymocky-cli @@ -14,10 +14,13 @@ # Set defaults set -o nounset -o errexit -o errtrace -o pipefail -# Path to swiftymocky binary -EXE="${HOME}/.mint/bin/swiftymocky" +# Command that runs the swiftymocky binary +EXE="mint" -# Check the swiftymocky executable exists +# Repository name for SwiftyMocky +SWIFTY_MOCKY="MakeAWishFoundation/SwiftyMocky" + +# Check the executable exists if ! [[ -x "$(command -v "${EXE}")" ]]; then echo "Command not found '${EXE}'." 1>&2 echo "Please see the docs of $0 for installation steps." 1>&2 @@ -25,4 +28,7 @@ if ! [[ -x "$(command -v "${EXE}")" ]]; then fi # Validate the setup -"${EXE}" doctor \ No newline at end of file +"${EXE}" run "${SWIFTY_MOCKY}" doctor + +# Generate the mocks +"${EXE}" run "${SWIFTY_MOCKY}" generate From 918f87033b976cb4209d113eb0f24aabfd4eb2b3 Mon Sep 17 00:00:00 2001 From: Ben Shutt Date: Thu, 2 Jan 2025 20:41:52 +0000 Subject: [PATCH 09/14] Ensure SwiftyMocky@Master runs using Sourcery@2.2.6 --- Mockfile | 6 +++++- Package.resolved | 2 +- Package.swift | 2 +- README.md | 4 +--- Scripts/mocks.sh | 3 +++ Tests/Mocks/Mock.generated.swift | 17 +++++++++++++++++ 6 files changed, 28 insertions(+), 6 deletions(-) create mode 100644 Tests/Mocks/Mock.generated.swift diff --git a/Mockfile b/Mockfile index 3e6a482..eb05e3f 100644 --- a/Mockfile +++ b/Mockfile @@ -1,3 +1,7 @@ +# Version should match Mintfile +sourceryCommand: mint run krzysztofzablocki/Sourcery@2.2.6 sourcery +sourceryTemplate: null + unit.tests.mock: sources: include: @@ -10,4 +14,4 @@ unit.tests.mock: - DataRequest import: - Foundation - - Alamofire \ No newline at end of file + - Alamofire diff --git a/Package.resolved b/Package.resolved index 3796bbc..7c2db69 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "9b920edae1514aefb776c527caffbc6acff227b34ec8ed7c177749cbf6ef8b7e", + "originHash" : "abcac73efe1f2c1608abbbc35333eb3b0df178d368a9ddcd43be692c6fbb0e29", "pins" : [ { "identity" : "aexml", diff --git a/Package.swift b/Package.swift index d20dfe5..0969b1a 100644 --- a/Package.swift +++ b/Package.swift @@ -27,7 +27,7 @@ let package = Package( ), .package( url: "https://github.com/MakeAWishFoundation/SwiftyMocky", - branch: "master" // Should match Mintfile + branch: "master" // Version should match Mintfile ) ], targets: [ diff --git a/README.md b/README.md index b20f7cf..7402de7 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,4 @@ This can be returned in the `session` property of the `DecodableRequest`. ## Dependencies -The unit tests use [SwiftyMocky](https://github.com/MakeAWishFoundation/SwiftyMocky). -As documented in the [README](https://github.com/MakeAWishFoundation/SwiftyMocky?tab=readme-ov-file#installation), this uses the CLI `mint`. - +The unit tests use [SwiftyMocky](https://github.com/MakeAWishFoundation/SwiftyMocky) with the CLI installed using [Mint](https://github.com/yonaskolb/Mint) as documented in the [README](https://github.com/MakeAWishFoundation/SwiftyMocky?tab=readme-ov-file#installation). diff --git a/Scripts/mocks.sh b/Scripts/mocks.sh index 1a4819a..bae6677 100755 --- a/Scripts/mocks.sh +++ b/Scripts/mocks.sh @@ -32,3 +32,6 @@ fi # Generate the mocks "${EXE}" run "${SWIFTY_MOCKY}" generate + +# Print success +echo "Mocks generated successfully" diff --git a/Tests/Mocks/Mock.generated.swift b/Tests/Mocks/Mock.generated.swift new file mode 100644 index 0000000..4134363 --- /dev/null +++ b/Tests/Mocks/Mock.generated.swift @@ -0,0 +1,17 @@ +// Generated using Sourcery 2.2.6 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT + + +// Generated with SwiftyMocky 4.2.0 +// Required Sourcery: 1.8.0 + + +import SwiftyMocky +import XCTest +import Foundation +import Alamofire +@testable import DataRequest + + +// SwiftyMocky: no AutoMockable found. +// Please define and inherit from AutoMockable, or annotate protocols to be mocked From eceb77835cb52a754d5e3a48de36ca709499680a Mon Sep 17 00:00:00 2001 From: Ben Shutt Date: Fri, 3 Jan 2025 10:52:21 +0000 Subject: [PATCH 10/14] Remove SwiftyMocky references --- Mintfile | 2 - Mockfile | 17 ------ Package.resolved | 92 +------------------------------- Package.swift | 9 +--- README.md | 4 -- Scripts/mocks.sh | 37 ------------- Tests/Mocks/Mock.generated.swift | 17 ------ 7 files changed, 2 insertions(+), 176 deletions(-) delete mode 100644 Mintfile delete mode 100644 Mockfile delete mode 100755 Scripts/mocks.sh delete mode 100644 Tests/Mocks/Mock.generated.swift diff --git a/Mintfile b/Mintfile deleted file mode 100644 index 82d421c..0000000 --- a/Mintfile +++ /dev/null @@ -1,2 +0,0 @@ -MakeAWishFoundation/SwiftyMocky@master -krzysztofzablocki/Sourcery@2.2.6 diff --git a/Mockfile b/Mockfile deleted file mode 100644 index eb05e3f..0000000 --- a/Mockfile +++ /dev/null @@ -1,17 +0,0 @@ -# Version should match Mintfile -sourceryCommand: mint run krzysztofzablocki/Sourcery@2.2.6 sourcery -sourceryTemplate: null - -unit.tests.mock: - sources: - include: - - ./Sources - output: - ./Tests/Mocks/Mock.generated.swift - targets: - - DataRequestTests - testable: - - DataRequest - import: - - Foundation - - Alamofire diff --git a/Package.resolved b/Package.resolved index 7c2db69..8336368 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,15 +1,6 @@ { - "originHash" : "abcac73efe1f2c1608abbbc35333eb3b0df178d368a9ddcd43be692c6fbb0e29", + "originHash" : "25b484c8cb9974d7fa135e264aa55275ab78d92febdf49b1c153087ed853a610", "pins" : [ - { - "identity" : "aexml", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tadija/AEXML.git", - "state" : { - "revision" : "38f7d00b23ecd891e1ee656fa6aeebd6ba04ecc3", - "version" : "4.6.1" - } - }, { "identity" : "alamofire", "kind" : "remoteSourceControl", @@ -19,60 +10,6 @@ "version" : "5.10.2" } }, - { - "identity" : "chalk", - "kind" : "remoteSourceControl", - "location" : "https://github.com/luoxiu/Chalk", - "state" : { - "revision" : "8a9d3373bd754fb62f7881d9f639d376f5e4d5a5", - "version" : "0.2.1" - } - }, - { - "identity" : "commander", - "kind" : "remoteSourceControl", - "location" : "https://github.com/kylef/Commander", - "state" : { - "revision" : "4a1f2fb82fb6cef613c4a25d2e38f702e4d812c2", - "version" : "0.9.2" - } - }, - { - "identity" : "pathkit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/kylef/PathKit", - "state" : { - "revision" : "3bfd2737b700b9a36565a8c94f4ad2b050a5e574", - "version" : "1.0.1" - } - }, - { - "identity" : "rainbow", - "kind" : "remoteSourceControl", - "location" : "https://github.com/luoxiu/Rainbow", - "state" : { - "revision" : "9d8fcb4c64e816a135c31162a0c319e9f1f09c35", - "version" : "0.1.1" - } - }, - { - "identity" : "shellout", - "kind" : "remoteSourceControl", - "location" : "https://github.com/JohnSundell/ShellOut", - "state" : { - "revision" : "e1577acf2b6e90086d01a6d5e2b8efdaae033568", - "version" : "2.3.0" - } - }, - { - "identity" : "spectre", - "kind" : "remoteSourceControl", - "location" : "https://github.com/kylef/Spectre.git", - "state" : { - "revision" : "26cc5e9ae0947092c7139ef7ba612e34646086c7", - "version" : "0.10.1" - } - }, { "identity" : "swiftlintplugins", "kind" : "remoteSourceControl", @@ -81,33 +18,6 @@ "revision" : "f9731bef175c3eea3a0ca960f1be78fcc2bc7853", "version" : "0.57.1" } - }, - { - "identity" : "swiftymocky", - "kind" : "remoteSourceControl", - "location" : "https://github.com/MakeAWishFoundation/SwiftyMocky", - "state" : { - "branch" : "master", - "revision" : "3672eea08c7098214ac54ee26c7a7e39ea01a2f1" - } - }, - { - "identity" : "xcodeproj", - "kind" : "remoteSourceControl", - "location" : "https://github.com/tuist/xcodeproj", - "state" : { - "revision" : "6e971133653f069b7699d5fb081e5db1e5f81559", - "version" : "8.26.1" - } - }, - { - "identity" : "yams", - "kind" : "remoteSourceControl", - "location" : "https://github.com/jpsim/Yams", - "state" : { - "revision" : "3036ba9d69cf1fd04d433527bc339dc0dc75433d", - "version" : "5.1.3" - } } ], "version" : 3 diff --git a/Package.swift b/Package.swift index 0969b1a..0a52405 100644 --- a/Package.swift +++ b/Package.swift @@ -24,10 +24,6 @@ let package = Package( .package( url: "https://github.com/SimplyDanny/SwiftLintPlugins", .upToNextMajor(from: "0.0.0") - ), - .package( - url: "https://github.com/MakeAWishFoundation/SwiftyMocky", - branch: "master" // Version should match Mintfile ) ], targets: [ @@ -42,10 +38,7 @@ let package = Package( ), .testTarget( name: "\(name)Tests", - dependencies: [ - .byName(name: name), - "SwiftyMocky" - ], + dependencies: [.byName(name: name)], path: "Tests" ) ] diff --git a/README.md b/README.md index 7402de7..e7debfb 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,3 @@ extension Session { ``` This can be returned in the `session` property of the `DecodableRequest`. - -## Dependencies - -The unit tests use [SwiftyMocky](https://github.com/MakeAWishFoundation/SwiftyMocky) with the CLI installed using [Mint](https://github.com/yonaskolb/Mint) as documented in the [README](https://github.com/MakeAWishFoundation/SwiftyMocky?tab=readme-ov-file#installation). diff --git a/Scripts/mocks.sh b/Scripts/mocks.sh deleted file mode 100755 index bae6677..0000000 --- a/Scripts/mocks.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env bash - -# -# Script: mocks.sh -# Usage: ./mocks.sh -# -# Description: -# Generates mock files using SwiftyMocky. -# -# Installation: -# https://github.com/MakeAWishFoundation/SwiftyMocky?tab=readme-ov-file#2-installing-swiftymocky-cli -# - -# Set defaults -set -o nounset -o errexit -o errtrace -o pipefail - -# Command that runs the swiftymocky binary -EXE="mint" - -# Repository name for SwiftyMocky -SWIFTY_MOCKY="MakeAWishFoundation/SwiftyMocky" - -# Check the executable exists -if ! [[ -x "$(command -v "${EXE}")" ]]; then - echo "Command not found '${EXE}'." 1>&2 - echo "Please see the docs of $0 for installation steps." 1>&2 - exit 1 -fi - -# Validate the setup -"${EXE}" run "${SWIFTY_MOCKY}" doctor - -# Generate the mocks -"${EXE}" run "${SWIFTY_MOCKY}" generate - -# Print success -echo "Mocks generated successfully" diff --git a/Tests/Mocks/Mock.generated.swift b/Tests/Mocks/Mock.generated.swift deleted file mode 100644 index 4134363..0000000 --- a/Tests/Mocks/Mock.generated.swift +++ /dev/null @@ -1,17 +0,0 @@ -// Generated using Sourcery 2.2.6 — https://github.com/krzysztofzablocki/Sourcery -// DO NOT EDIT - - -// Generated with SwiftyMocky 4.2.0 -// Required Sourcery: 1.8.0 - - -import SwiftyMocky -import XCTest -import Foundation -import Alamofire -@testable import DataRequest - - -// SwiftyMocky: no AutoMockable found. -// Please define and inherit from AutoMockable, or annotate protocols to be mocked From d4a6fb9b5ef8b4a93ef7f4875b1314b5684bb314 Mon Sep 17 00:00:00 2001 From: Ben Shutt Date: Fri, 3 Jan 2025 10:52:23 +0000 Subject: [PATCH 11/14] Update ResponseEventMonitor.swift --- Sources/Utilities/ResponseEventMonitor.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Utilities/ResponseEventMonitor.swift b/Sources/Utilities/ResponseEventMonitor.swift index a2c8c44..6eabcf3 100644 --- a/Sources/Utilities/ResponseEventMonitor.swift +++ b/Sources/Utilities/ResponseEventMonitor.swift @@ -12,7 +12,7 @@ import Alamofire /// `EventMonitor` that logs responses public struct ResponseEventMonitor: EventMonitor { - /// `DispatchQueue` to execute on + /// The `DispatchQueue` onto which Alamofire's root `CompositeEventMonitor` will dispatch events public var queue: DispatchQueue { .main } /// Public initializer From 5e6db0588ab5e0a82a86fca6ae364cc8442299db Mon Sep 17 00:00:00 2001 From: Ben Shutt Date: Fri, 3 Jan 2025 11:57:59 +0000 Subject: [PATCH 12/14] Write unit tests for URLRequestMaker and HTTPBody --- .../Extensions/HTTPHeader+Extensions.swift | 5 - Tests/HTTPBodyTests.swift | 62 ++++++++++++ Tests/URLRequestMakerTests.swift | 99 +++++++++++++++++++ Tests/Utilities/Atomic.swift | 48 +++++++++ Tests/WorldTimeTests.swift | 18 +--- 5 files changed, 211 insertions(+), 21 deletions(-) create mode 100644 Tests/HTTPBodyTests.swift create mode 100644 Tests/URLRequestMakerTests.swift create mode 100644 Tests/Utilities/Atomic.swift diff --git a/Sources/Extensions/HTTPHeader+Extensions.swift b/Sources/Extensions/HTTPHeader+Extensions.swift index e197c13..6478b96 100644 --- a/Sources/Extensions/HTTPHeader+Extensions.swift +++ b/Sources/Extensions/HTTPHeader+Extensions.swift @@ -19,9 +19,4 @@ public extension HTTPHeader { /// `"Content-Type: application/octet-stream"` static let contentTypeData: HTTPHeader = .contentType("application/octet-stream") - - /// `"Content-Type: application/x-www-form-urlencoded; charset=utf-8"` - static let contentTypeForm: HTTPHeader = .contentType( - "application/x-www-form-urlencoded; charset=utf-8" - ) } diff --git a/Tests/HTTPBodyTests.swift b/Tests/HTTPBodyTests.swift new file mode 100644 index 0000000..72c7dad --- /dev/null +++ b/Tests/HTTPBodyTests.swift @@ -0,0 +1,62 @@ +// +// HTTPBodyTests.swift +// DataRequest +// +// Created by Ben Shutt on 03/01/2025. +// Copyright © 2025 Ben Shutt. All rights reserved. +// + +import Foundation +import Testing +@testable import DataRequest + +// MARK: - EncodableBody + +struct EncodableBody: Encodable { + var int = 1 + var string = "123" +} + +// MARK: - HTTPBodyTests + +@Suite("Unit tests for HTTPBody") +struct HTTPBodyTests { + + // MARK: - Data + + @Test func dataHeader() throws { + let httpBody = HTTPBody.data(Data()) + #expect(httpBody.contentType == .init( + name: "Content-Type", + value: "application/octet-stream" + )) + } + + @Test func dataBody() throws { + let httpBody = HTTPBody.data(Data()) + #expect(httpBody.body == Data()) + } + + // MARK: - JSON + + @Test func jsonHeader() throws { + let httpBody = try HTTPBody.json(EncodableBody()) + #expect(httpBody.contentType == .init( + name: "Content-Type", + value: "application/json" + )) + } + + @Test func jsonBody() throws { + let encoder = JSONEncoder() + encoder.outputFormatting = [.sortedKeys] + + let httpBody = try HTTPBody.json(EncodableBody(), encoder: encoder) + let json = try #require(String(data: httpBody.body, encoding: .utf8)) + + #expect(json == """ + {"int":1,"string":"123"} + """ + ) + } +} diff --git a/Tests/URLRequestMakerTests.swift b/Tests/URLRequestMakerTests.swift new file mode 100644 index 0000000..f7fb2d3 --- /dev/null +++ b/Tests/URLRequestMakerTests.swift @@ -0,0 +1,99 @@ +// +// URLRequestMakerTests.swift +// DataRequest +// +// Created by Ben Shutt on 03/01/2025. +// Copyright © 2025 Ben Shutt. All rights reserved. +// + +import Foundation +import Alamofire +import Testing +@testable import DataRequest + +// MARK: - EmptyURLRequestMaker + +struct EmptyURLRequestMaker: URLRequestMaker { + let urlComponents = URLComponents() +} + +// MARK: - URLRequestMakerWithBody + +struct URLRequestMakerWithBody: URLRequestMaker { + let urlComponents = URLComponents() + let method: HTTPMethod = .post + + @Atomic var bodyCount = 0 + + var headers: HTTPHeaders { + .default.appending(.acceptJSON) + } + + var body: HTTPBody? { + bodyCount += 1 + return HTTPBody( + contentType: .contentTypeData, + body: Data() + ) + } +} + +// MARK: - URLRequestMakerTests + +@Suite("Unit tests for URLRequestMaker") +struct URLRequestMakerTests { + + // MARK: - Defaults + + @Test func defaultMethod() throws { + let sut = EmptyURLRequestMaker() + let urlRequest = try sut.urlRequest + #expect(urlRequest.httpMethod == "GET") + } + + @Test func defaultHeaders() throws { + let sut = EmptyURLRequestMaker() + let urlRequest = try sut.urlRequest + let headers = urlRequest.allHTTPHeaderFields ?? [:] + #expect(Set(headers.keys) == [ + "Accept-Encoding", + "Accept-Language", + "User-Agent", + ]) + } + + @Test func defaultBody() throws { + let sut = EmptyURLRequestMaker() + let urlRequest = try sut.urlRequest + #expect(urlRequest.httpBody == nil) + } + + // MARK: - Defaults + + @Test func asURLRequestCallsBodyOnce() throws { + let sut = URLRequestMakerWithBody() + _ = try sut.asURLRequest() + #expect(sut.bodyCount == 1) + } + + @Test func withBodyMethod() throws { + let sut = URLRequestMakerWithBody() + let urlRequest = try sut.asURLRequest() + #expect(urlRequest.httpMethod == "POST") + } + + @Test func withBodyHeaders() throws { + let sut = URLRequestMakerWithBody() + let urlRequest = try sut.asURLRequest() + let headers = urlRequest.allHTTPHeaderFields ?? [:] + #expect(Set(headers.keys) == [ + "Accept", + "Accept-Encoding", + "Accept-Language", + "Content-Type", + "User-Agent" + ]) + #expect(headers["Accept"] == "application/json") + #expect(headers["Content-Type"] == "application/octet-stream") + } +} diff --git a/Tests/Utilities/Atomic.swift b/Tests/Utilities/Atomic.swift new file mode 100644 index 0000000..e45b3d6 --- /dev/null +++ b/Tests/Utilities/Atomic.swift @@ -0,0 +1,48 @@ +// +// Atomic.swift +// DataRequest +// +// Created by Ben Shutt on 03/01/2025. +// Copyright © 2025 Ben Shutt. All rights reserved. +// + +import Foundation + +/// Example of making a thread safe class using GCD +@propertyWrapper +final class Atomic: @unchecked Sendable { + + /// Concurrent dispatch queue + private let queue = DispatchQueue( + label: "\(Atomic.self)", + attributes: .concurrent + ) + + /// A property that is thread safe and accessed only on the queue + private var storedValue: Stored + + /// Memberwise initializer + /// - Parameter wrappedValue: The value to store + init(wrappedValue: Stored) { + storedValue = wrappedValue + } + + /// Get and set the stored value in a thread safe manner + var wrappedValue: Stored { + get { + // Add operation to the queue. + // Wait for the operation to finish. + queue.sync { + storedValue + } + } + set { + // Add operation to the queue. + // Stop anything else running on the queue while this operation runs. + // Do not wait for the operation to finish. + queue.async(flags: .barrier) { [weak self] in + self?.storedValue = newValue + } + } + } +} diff --git a/Tests/WorldTimeTests.swift b/Tests/WorldTimeTests.swift index 6334cf4..c16a816 100644 --- a/Tests/WorldTimeTests.swift +++ b/Tests/WorldTimeTests.swift @@ -9,26 +9,12 @@ import Testing @testable import DataRequest -/// _Integration_ tests using the World Time API -@Suite struct WorldTimeTests { +@Suite("Integration tests for the World Time API") +struct WorldTimeTests { private let timeZone = "Europe/London" - private let expectedHeaderKeys = [ - "Accept", - "Accept-Encoding", - "Accept-Language", - "User-Agent", - ] @Test func test() async throws { let worldTime = try await GetWorldTime(timeZone: timeZone).request() #expect(worldTime.timezone == timeZone) } - - @Test func headers() async throws { - let endpoint = GetWorldTime(timeZone: timeZone) - let urlRequest = try endpoint.asURLRequest() - let headers = urlRequest.allHTTPHeaderFields ?? [:] - #expect(Set(expectedHeaderKeys) == Set(headers.keys)) - #expect(headers["Accept"] == "application/json") - } } From 0d681a24e4ae3a03f95c7b12f82efac322fff3f7 Mon Sep 17 00:00:00 2001 From: Ben Shutt Date: Fri, 3 Jan 2025 12:36:26 +0000 Subject: [PATCH 13/14] Add .swiftlint.yml and update formatting --- .swiftlint.yml | 48 +++++++++++ .../xcschemes/DataRequest.xcscheme | 79 +++++++++++++++++++ Package.swift | 19 +++-- Sources/DecodableRequest.swift | 5 +- .../Extensions/DataRequest+Extensions.swift | 3 +- .../Extensions/HTTPHeader+Extensions.swift | 3 +- .../Extensions/HTTPHeaders+Extensions.swift | 3 +- Sources/HTTPBody.swift | 5 +- Sources/URLRequestMaker.swift | 3 +- Sources/Utilities/ResponseEventMonitor.swift | 9 ++- Tests/HTTPBodyTests.swift | 3 +- Tests/URLRequestMakerTests.swift | 7 +- Tests/Utilities/Atomic.swift | 13 ++- Tests/WorldTime/GetWorldTime.swift | 3 +- Tests/WorldTimeTests.swift | 2 +- 15 files changed, 165 insertions(+), 40 deletions(-) create mode 100644 .swiftlint.yml create mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/DataRequest.xcscheme diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 0000000..73b0a76 --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,48 @@ +excluded: + - .build +analyzer_rules: + - unused_declaration + - unused_import +opt_in_rules: + - all +disabled_rules: + - anonymous_argument_in_multiline_closure + - balanced_xctest_lifecycle + - conditional_returns_on_newline + - contrasted_opening_brace + - explicit_acl + - explicit_enum_raw_value + - explicit_top_level_acl + - explicit_type_interface + - file_header + - inert_defer # Deprecated + - missing_docs + - no_extension_access_modifier + - no_grouping_extension + - one_declaration_per_file + - prefer_nimble + - required_deinit + - sorted_enum_cases + - switch_case_on_newline + - trailing_closure + - unused_capture_list # Deprecated + - vertical_whitespace_between_cases +attributes: + attributes_with_arguments_always_on_line_above: true + always_on_line_above: + - "@MainActor" + - "@Option" + - "@Suite" + always_on_same_line: + - "@ViewBuilder" + - "@Environment" + - "@EnvironmentObject" + - "@Test" +closure_body_length: + warning: 10 + error: 20 +function_body_length: 40 +large_tuple: 3 +number_separator: + minimum_length: 5 +type_body_length: 300 diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/DataRequest.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/DataRequest.xcscheme new file mode 100644 index 0000000..27ddb74 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/DataRequest.xcscheme @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Package.swift b/Package.swift index 0a52405..9f349db 100644 --- a/Package.swift +++ b/Package.swift @@ -31,15 +31,24 @@ let package = Package( name: name, dependencies: ["Alamofire"], path: "Sources", - plugins: [.plugin( - name: "SwiftLintBuildToolPlugin", - package: "SwiftLintPlugins" - )] + plugins: [.swiftlint] ), .testTarget( name: "\(name)Tests", dependencies: [.byName(name: name)], - path: "Tests" + path: "Tests", + plugins: [.swiftlint] ) ] ) + +// MARK: - Plugins + +private extension PackageDescription.Target.PluginUsage { + static var swiftlint: Self { + .plugin( + name: "SwiftLintBuildToolPlugin", + package: "SwiftLintPlugins" + ) + } +} diff --git a/Sources/DecodableRequest.swift b/Sources/DecodableRequest.swift index 9b4bec5..30fc76a 100644 --- a/Sources/DecodableRequest.swift +++ b/Sources/DecodableRequest.swift @@ -6,15 +6,14 @@ // Copyright © 2023 Ben Shutt. All rights reserved. // -import Foundation import Alamofire +import Foundation /// An endpoint that executes a URL request and decodes the response into a model. public protocol DecodableRequest: URLRequestMaker { - /// Model expected in the response body that will be decoded from data. /// - Note: `Empty` can be used when a response is empty. E.g. on HTTP status code 204. - associatedtype ResponseBody: Decodable & Sendable + associatedtype ResponseBody: Decodable, Sendable /// The Alamofire session. /// diff --git a/Sources/Extensions/DataRequest+Extensions.swift b/Sources/Extensions/DataRequest+Extensions.swift index 6a67771..bf64bd9 100644 --- a/Sources/Extensions/DataRequest+Extensions.swift +++ b/Sources/Extensions/DataRequest+Extensions.swift @@ -6,11 +6,10 @@ // Copyright © 2023 Ben Shutt. All rights reserved. // -import Foundation import Alamofire +import Foundation public extension DataRequest { - /// Validate if required /// - Parameter value: Should validate /// - Returns: `Self` diff --git a/Sources/Extensions/HTTPHeader+Extensions.swift b/Sources/Extensions/HTTPHeader+Extensions.swift index 6478b96..92f8d76 100644 --- a/Sources/Extensions/HTTPHeader+Extensions.swift +++ b/Sources/Extensions/HTTPHeader+Extensions.swift @@ -6,11 +6,10 @@ // Copyright © 2023 Ben Shutt. All rights reserved. // -import Foundation import Alamofire +import Foundation public extension HTTPHeader { - /// `"Accept: application/json"` static let acceptJSON: HTTPHeader = .accept("application/json") diff --git a/Sources/Extensions/HTTPHeaders+Extensions.swift b/Sources/Extensions/HTTPHeaders+Extensions.swift index bf34e6a..91a30b9 100644 --- a/Sources/Extensions/HTTPHeaders+Extensions.swift +++ b/Sources/Extensions/HTTPHeaders+Extensions.swift @@ -6,11 +6,10 @@ // Copyright © 2023 Ben Shutt. All rights reserved. // -import Foundation import Alamofire +import Foundation public extension HTTPHeaders { - /// Append `headers` to this instance /// - Parameter headers: `HTTPHeader` mutating func append(_ headers: HTTPHeader...) { diff --git a/Sources/HTTPBody.swift b/Sources/HTTPBody.swift index 4a83749..b8b49a3 100644 --- a/Sources/HTTPBody.swift +++ b/Sources/HTTPBody.swift @@ -6,12 +6,11 @@ // Copyright © 2023 Ben Shutt. All rights reserved. // -import Foundation import Alamofire +import Foundation /// Defines a HTTP request body public struct HTTPBody { - /// `HTTPHeader` for the content type of the body public var contentType: HTTPHeader @@ -28,7 +27,6 @@ public struct HTTPBody { // MARK: - HTTPBody + JSON extension HTTPBody { - /// Make a JSON request body /// - Parameters: /// - model: Object that will be encoded into JSON data @@ -48,7 +46,6 @@ extension HTTPBody { // MARK: - HTTPBody + Data extension HTTPBody { - /// Make a `Data` request body /// - Parameter data: The data of the body /// - Returns: The `Data` request body diff --git a/Sources/URLRequestMaker.swift b/Sources/URLRequestMaker.swift index 66b5d18..19b7499 100644 --- a/Sources/URLRequestMaker.swift +++ b/Sources/URLRequestMaker.swift @@ -6,12 +6,11 @@ // Copyright © 2023 Ben Shutt. All rights reserved. // -import Foundation import Alamofire +import Foundation /// An entity that builds a `URLRequest`. public protocol URLRequestMaker: URLRequestConvertible { - /// The components of a URL. var urlComponents: URLComponents { get } diff --git a/Sources/Utilities/ResponseEventMonitor.swift b/Sources/Utilities/ResponseEventMonitor.swift index 6eabcf3..78739b4 100644 --- a/Sources/Utilities/ResponseEventMonitor.swift +++ b/Sources/Utilities/ResponseEventMonitor.swift @@ -6,20 +6,21 @@ // Copyright © 2023 Ben Shutt. All rights reserved. // -import Foundation import Alamofire +import Foundation /// `EventMonitor` that logs responses public struct ResponseEventMonitor: EventMonitor { - /// The `DispatchQueue` onto which Alamofire's root `CompositeEventMonitor` will dispatch events public var queue: DispatchQueue { .main } /// Public initializer - public init() {} + public init() { + // Defined or public access level + } public func request( - _ request: DataRequest, + _ _: DataRequest, didParseResponse response: DataResponse ) { debugPrint(response) diff --git a/Tests/HTTPBodyTests.swift b/Tests/HTTPBodyTests.swift index 72c7dad..108040c 100644 --- a/Tests/HTTPBodyTests.swift +++ b/Tests/HTTPBodyTests.swift @@ -6,9 +6,9 @@ // Copyright © 2025 Ben Shutt. All rights reserved. // +@testable import DataRequest import Foundation import Testing -@testable import DataRequest // MARK: - EncodableBody @@ -21,7 +21,6 @@ struct EncodableBody: Encodable { @Suite("Unit tests for HTTPBody") struct HTTPBodyTests { - // MARK: - Data @Test func dataHeader() throws { diff --git a/Tests/URLRequestMakerTests.swift b/Tests/URLRequestMakerTests.swift index f7fb2d3..15bcac7 100644 --- a/Tests/URLRequestMakerTests.swift +++ b/Tests/URLRequestMakerTests.swift @@ -6,10 +6,10 @@ // Copyright © 2025 Ben Shutt. All rights reserved. // -import Foundation import Alamofire -import Testing @testable import DataRequest +import Foundation +import Testing // MARK: - EmptyURLRequestMaker @@ -42,7 +42,6 @@ struct URLRequestMakerWithBody: URLRequestMaker { @Suite("Unit tests for URLRequestMaker") struct URLRequestMakerTests { - // MARK: - Defaults @Test func defaultMethod() throws { @@ -58,7 +57,7 @@ struct URLRequestMakerTests { #expect(Set(headers.keys) == [ "Accept-Encoding", "Accept-Language", - "User-Agent", + "User-Agent" ]) } diff --git a/Tests/Utilities/Atomic.swift b/Tests/Utilities/Atomic.swift index e45b3d6..a53ecc7 100644 --- a/Tests/Utilities/Atomic.swift +++ b/Tests/Utilities/Atomic.swift @@ -11,7 +11,6 @@ import Foundation /// Example of making a thread safe class using GCD @propertyWrapper final class Atomic: @unchecked Sendable { - /// Concurrent dispatch queue private let queue = DispatchQueue( label: "\(Atomic.self)", @@ -21,12 +20,6 @@ final class Atomic: @unchecked Sendable { /// A property that is thread safe and accessed only on the queue private var storedValue: Stored - /// Memberwise initializer - /// - Parameter wrappedValue: The value to store - init(wrappedValue: Stored) { - storedValue = wrappedValue - } - /// Get and set the stored value in a thread safe manner var wrappedValue: Stored { get { @@ -45,4 +38,10 @@ final class Atomic: @unchecked Sendable { } } } + + /// Memberwise initializer + /// - Parameter wrappedValue: The value to store + init(wrappedValue: Stored) { + storedValue = wrappedValue + } } diff --git a/Tests/WorldTime/GetWorldTime.swift b/Tests/WorldTime/GetWorldTime.swift index c2a42c6..3293610 100644 --- a/Tests/WorldTime/GetWorldTime.swift +++ b/Tests/WorldTime/GetWorldTime.swift @@ -6,13 +6,12 @@ // Copyright © 2023 Ben Shutt. All rights reserved. // -import Foundation import Alamofire import DataRequest +import Foundation /// Get the world time for a time zone struct GetWorldTime: DecodableRequest { - /// Decode response as `WorldTime` typealias ResponseBody = WorldTime diff --git a/Tests/WorldTimeTests.swift b/Tests/WorldTimeTests.swift index c16a816..f749083 100644 --- a/Tests/WorldTimeTests.swift +++ b/Tests/WorldTimeTests.swift @@ -6,8 +6,8 @@ // Copyright © 2023 Ben Shutt. All rights reserved. // -import Testing @testable import DataRequest +import Testing @Suite("Integration tests for the World Time API") struct WorldTimeTests { From 21caada558ab6e4b18b2bb2a55ac392c1ed70640 Mon Sep 17 00:00:00 2001 From: Ben Shutt Date: Fri, 3 Jan 2025 18:36:44 +0000 Subject: [PATCH 14/14] Add GitHub action `.github/workflows/swift.yml` which builds and tests the swift package --- .github/workflows/swift.yml | 19 ++++++++ Package.resolved | 2 +- Package.swift | 49 ++++++++++++++------ README.md | 4 ++ Sources/Utilities/ResponseEventMonitor.swift | 2 +- Tests/WorldTimeTests.swift | 4 +- 6 files changed, 61 insertions(+), 19 deletions(-) create mode 100644 .github/workflows/swift.yml diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml new file mode 100644 index 0000000..3e3e129 --- /dev/null +++ b/.github/workflows/swift.yml @@ -0,0 +1,19 @@ +# This workflow will build a Swift project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift + +name: Swift +on: + push: + branches: ["main"] + pull_request: + branches: ["*"] +jobs: + build: + runs-on: ubuntu-latest # For docker container + container: swift:latest + steps: + - uses: actions/checkout@v4 + - name: Build + run: swift build -v --configuration release + - name: Test + run: swift test -v --configuration release \ No newline at end of file diff --git a/Package.resolved b/Package.resolved index 8336368..320d754 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "25b484c8cb9974d7fa135e264aa55275ab78d92febdf49b1c153087ed853a610", + "originHash" : "3976824d324d5540b0007a3daf5c747340f900e41c18e9222b21091a712f5800", "pins" : [ { "identity" : "alamofire", diff --git a/Package.swift b/Package.swift index 9f349db..7768534 100644 --- a/Package.swift +++ b/Package.swift @@ -3,6 +3,14 @@ import PackageDescription +#if os(macOS) +let dependencies: [Dependency] = [.alamofire, .swiftLint] +let plugins: [Plugin] = [.swiftLint] +#else +let dependencies: [Dependency] = [.alamofire] +let plugins: [Plugin] = [] +#endif + let name = "DataRequest" let package = Package( name: name, @@ -16,36 +24,47 @@ let package = Package( targets: [name] ) ], - dependencies: [ - .package( - url: "https://github.com/Alamofire/Alamofire.git", - .upToNextMajor(from: "5.8.0") - ), - .package( - url: "https://github.com/SimplyDanny/SwiftLintPlugins", - .upToNextMajor(from: "0.0.0") - ) - ], + dependencies: dependencies, targets: [ .target( name: name, dependencies: ["Alamofire"], path: "Sources", - plugins: [.swiftlint] + plugins: plugins ), .testTarget( name: "\(name)Tests", dependencies: [.byName(name: name)], path: "Tests", - plugins: [.swiftlint] + plugins: plugins ) ] ) -// MARK: - Plugins +// MARK: - Dependency + +typealias Dependency = Package.Dependency +extension Dependency { + static var alamofire: Dependency { + .package( + url: "https://github.com/Alamofire/Alamofire.git", + .upToNextMajor(from: "5.8.0") + ) + } + + static var swiftLint: Dependency { + .package( + url: "https://github.com/SimplyDanny/SwiftLintPlugins", + .upToNextMajor(from: "0.0.0") + ) + } +} + +// MARK: - Plugin -private extension PackageDescription.Target.PluginUsage { - static var swiftlint: Self { +typealias Plugin = PackageDescription.Target.PluginUsage +extension Plugin { + static var swiftLint: Plugin { .plugin( name: "SwiftLintBuildToolPlugin", package: "SwiftLintPlugins" diff --git a/README.md b/README.md index e7debfb..2f308d8 100644 --- a/README.md +++ b/README.md @@ -75,3 +75,7 @@ extension Session { ``` This can be returned in the `session` property of the `DecodableRequest`. + +## GitHub Actions + +The `.github/workflows/swift.yml` GitHub action checks that the Swift package builds and the tests past using a swift docker container. diff --git a/Sources/Utilities/ResponseEventMonitor.swift b/Sources/Utilities/ResponseEventMonitor.swift index 78739b4..844729c 100644 --- a/Sources/Utilities/ResponseEventMonitor.swift +++ b/Sources/Utilities/ResponseEventMonitor.swift @@ -16,7 +16,7 @@ public struct ResponseEventMonitor: EventMonitor { /// Public initializer public init() { - // Defined or public access level + // Defined for public access level } public func request( diff --git a/Tests/WorldTimeTests.swift b/Tests/WorldTimeTests.swift index f749083..aefdd72 100644 --- a/Tests/WorldTimeTests.swift +++ b/Tests/WorldTimeTests.swift @@ -9,11 +9,11 @@ @testable import DataRequest import Testing -@Suite("Integration tests for the World Time API") +@Suite("Integration tests for the World Time API", .disabled()) struct WorldTimeTests { private let timeZone = "Europe/London" - @Test func test() async throws { + @Test func worldTimeAPI() async throws { let worldTime = try await GetWorldTime(timeZone: timeZone).request() #expect(worldTime.timezone == timeZone) }