diff --git a/.gitignore b/.gitignore index 2b5f284..d40b76a 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,29 @@ DerivedData/ !default.perspectivev3 xcuserdata/ +## Other +*.moved-aside +*.xcuserstate +*.xcscmblueprint +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## Build generated +build/ +DerivedData/ + +## Various settings +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata/ + ## Other *.moved-aside *.xccheckout diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/Json.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/Json.xcscheme deleted file mode 100644 index 2765ce0..0000000 --- a/.swiftpm/xcode/xcshareddata/xcschemes/Json.xcscheme +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/Request-Package.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/Request-Package.xcscheme deleted file mode 100644 index 3aaeb57..0000000 --- a/.swiftpm/xcode/xcshareddata/xcschemes/Request-Package.xcscheme +++ /dev/null @@ -1,106 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/Request.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/Request.xcscheme deleted file mode 100644 index f3a3fe3..0000000 --- a/.swiftpm/xcode/xcshareddata/xcschemes/Request.xcscheme +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.travis.yml b/.travis.yml index c462ed9..51c1533 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,6 @@ language: - swift script: - swift package generate-xcodeproj - - xcodebuild clean test -project Request.xcodeproj -scheme "Request-Package" -destination "OS=13.0,name=iPhone XS" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO ONLY_ACTIVE_ARCH=NO -quiet -enableCodeCoverage YES -derivedDataPath .build/derivedData + - xcodebuild clean test -project Request.xcodeproj -scheme "Request-Package" -destination "OS=13.0,name=iPhone 11" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO ONLY_ACTIVE_ARCH=NO -quiet -enableCodeCoverage YES -derivedDataPath .build/derivedData after_success: - bash <(curl -s https://codecov.io/bash) -D .build/derivedData diff --git a/README.md b/README.md index bc451dd..103e3b9 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,14 @@ Body(["key": "value"]) Body("myBodyContent") Body(myJson) ``` +- `Timeout` + +Sets the timeout for a request or resource: +```swift +Timeout(60) +Timeout(60, for: .request) +Timeout(30, for: .resource) +``` - `RequestParam` Add a param directly @@ -196,6 +204,17 @@ RequestChain { } ``` +## Repeated Calls +`.update` is used to run additional calls after the initial one. You can pass it either a number or a custom `Publisher`. You can also chain together multiple `.update`s. The two `.update`s in the following example are equivalent, so the end result is that the `Request` will be called once immediately and twice every 10 seconds thereafter. +```swift +Request { + Url("https://jsonplaceholder.typicode.com/todo") +} +.update(every: 10) +.update(publisher: Timer.publish(every: 10, on: .main, in: .common).autoconnect()) +.call() +``` + ## Json `swift-request` includes support for `Json`. `Json` is used as the response type in the `onJson` callback on a `Request` object. diff --git a/Sources/Json/Json.swift b/Sources/Json/Json.swift index 14a8302..983b2cc 100644 --- a/Sources/Json/Json.swift +++ b/Sources/Json/Json.swift @@ -96,6 +96,7 @@ public struct Json { newSubs.remove(at: 0) var json = self[subs.first!] json[newSubs] = newValue + self[subs.first!] = json } } } diff --git a/Sources/Json/Literals.swift b/Sources/Json/Literals.swift index b1ddc5f..8a2e95b 100644 --- a/Sources/Json/Literals.swift +++ b/Sources/Json/Literals.swift @@ -45,6 +45,8 @@ extension Json: ExpressibleByArrayLiteral { extension Json: ExpressibleByDictionaryLiteral { public init(dictionaryLiteral elements: (String, Any)...) { - self.init { elements } + jsonData = Dictionary(elements, uniquingKeysWith: { (value1, value2) -> Any in + return value1 + }) } } diff --git a/Sources/Request/Helpers/Auth.swift b/Sources/Request/Helpers/Auth.swift index 7e942a9..dd7ae29 100644 --- a/Sources/Request/Helpers/Auth.swift +++ b/Sources/Request/Helpers/Auth.swift @@ -44,7 +44,7 @@ public struct Auth { extension Auth { /// Authenticates using `username` and `password` directly public static func basic(username: String, password: String) -> Auth { - return Auth(type: .basic, key: "\(username):\(password)") + return Auth(type: .basic, key: Data("\(username):\(password)".utf8).base64EncodedString()) } /// Authenticates using a `token` diff --git a/Sources/Request/Request/Request.swift b/Sources/Request/Request/Request.swift index 0b0b4b9..9d08146 100644 --- a/Sources/Request/Request/Request.swift +++ b/Sources/Request/Request/Request.swift @@ -30,6 +30,7 @@ import Combine /// - Precondition: The `Request` body must contain **exactly one** `Url` public typealias Request = AnyRequest +// TODO: Fix EXC_BAD_ACCESS instead of workaround with `struct` /// Tha base class of `Request` to be used with a `Codable` `ResponseType` when using the `onObject` callback /// /// *Example*: @@ -40,9 +41,9 @@ public typealias Request = AnyRequest /// .onObject { myCodableStructs in /// ... /// } -public class AnyRequest: ObservableObject, Identifiable where ResponseType: Decodable { - public var willChange = PassthroughSubject() - +public struct AnyRequest/*: ObservableObject, Identifiable*/ where ResponseType: Decodable { + public let combineIdentifier = CombineIdentifier() + private var params: CombinedParams private var onData: ((Data) -> Void)? @@ -50,15 +51,16 @@ public class AnyRequest: ObservableObject, Identifiable where Resp private var onJson: ((Json) -> Void)? private var onObject: ((ResponseType) -> Void)? private var onError: ((RequestError) -> Void)? + private var updatePublisher: AnyPublisher? - @Published public var response: Response = Response() + /*@Published*/ public var response: Response = Response() public init(@RequestBuilder builder: () -> RequestParam) { let params = builder() if !(params is CombinedParams) { self.params = CombinedParams(children: [params]) } else { - self.params = builder() as! CombinedParams + self.params = params as! CombinedParams } self.response = Response() } @@ -68,38 +70,56 @@ public class AnyRequest: ObservableObject, Identifiable where Resp self.response = Response() } + internal init(params: CombinedParams, + onData: ((Data) -> Void)?, + onString: ((String) -> Void)?, + onJson: ((Json) -> Void)?, + onObject: ((ResponseType) -> Void)?, + onError: ((RequestError) -> Void)?, + updatePublisher: AnyPublisher?) { + self.params = params + self.onData = onData + self.onString = onString + self.onJson = onJson + self.onObject = onObject + self.onError = onError + self.updatePublisher = updatePublisher + } + /// Sets the `onData` callback to be run whenever `Data` is retrieved public func onData(_ callback: @escaping (Data) -> Void) -> Self { - self.onData = callback - return self + Self.init(params: params, onData: callback, onString: onString, onJson: onJson, onObject: onObject, onError: onError, updatePublisher: updatePublisher) } /// Sets the `onString` callback to be run whenever a `String` is retrieved public func onString(_ callback: @escaping (String) -> Void) -> Self { - self.onString = callback - return self + Self.init(params: params, onData: onData, onString: callback, onJson: onJson, onObject: onObject, onError: onError, updatePublisher: updatePublisher) } /// Sets the `onData` callback to be run whenever `Json` is retrieved public func onJson(_ callback: @escaping (Json) -> Void) -> Self { - self.onJson = callback - return self + Self.init(params: params, onData: onData, onString: onString, onJson: callback, onObject: onObject, onError: onError, updatePublisher: updatePublisher) } /// Sets the `onObject` callback to be run whenever `Data` is retrieved public func onObject(_ callback: @escaping (ResponseType) -> Void) -> Self { - self.onObject = callback - return self + Self.init(params: params, onData: onData, onString: onString, onJson: onJson, onObject: callback, onError: onError, updatePublisher: updatePublisher) } /// Handle any `RequestError`s thrown by the `Request` public func onError(_ callback: @escaping (RequestError) -> Void) -> Self { - self.onError = callback - return self + Self.init(params: params, onData: onData, onString: onString, onJson: onJson, onObject: onObject, onError: callback, updatePublisher: updatePublisher) } /// Performs the `Request`, and calls the `onData`, `onString`, `onJson`, and `onError` callbacks when appropriate. public func call() { + performRequest() + if let updatePublisher = self.updatePublisher { + updatePublisher.subscribe(self) + } + } + + private func performRequest() { // Url guard var components = URLComponents(string: params.children!.filter({ $0.type == .url })[0].value as! String) else { fatalError("Missing Url in Request body") @@ -143,40 +163,95 @@ public class AnyRequest: ObservableObject, Identifiable where Resp request.httpBody = body[0].value as? Data } + // Configuration + let configuration = URLSessionConfiguration.default + let timeouts = params.children!.filter { $0.type == .timeout } + if timeouts.count > 0 { + for timeout in timeouts { + guard let (source, interval) = timeout.value as? (Timeout.Source, TimeInterval) else { + fatalError("Invalid Timeout \(timeout)") + } + if source.contains(.request) { + configuration.timeoutIntervalForRequest = interval + } + if source.contains(.resource) { + configuration.timeoutIntervalForResource = interval + } + } + } + + // PERFORM REQUEST - URLSession.shared.dataTask(with: request) { data, res, err in - if res != nil { - let statusCode = (res as! HTTPURLResponse).statusCode + URLSession(configuration: configuration).dataTask(with: request) { data, res, err in + if let res = res as? HTTPURLResponse { + let statusCode = res.statusCode if statusCode < 200 || statusCode >= 300 { - if self.onError != nil { - self.onError!(RequestError(statusCode: statusCode, error: data)) + if let onError = self.onError { + onError(RequestError(statusCode: statusCode, error: data)) return } } + } else if let err = err, let onError = self.onError { + onError(RequestError(statusCode: -1, error: err.localizedDescription.data(using: .utf8))) } - if data != nil { - if self.onData != nil { - self.onData!(data!) + if let data = data { + if let onData = self.onData { + onData(data) } - if self.onString != nil { - if let string = String(data: data!, encoding: .utf8) { - self.onString!(string) + if let onString = self.onString { + if let string = String(data: data, encoding: .utf8) { + onString(string) } } - if self.onJson != nil { - if let string = String(data: data!, encoding: .utf8) { + if let onJson = self.onJson { + if let string = String(data: data, encoding: .utf8) { if let json = try? Json(string) { - self.onJson!(json) + onJson(json) } } } - if self.onObject != nil { - if let decoded = try? JSONDecoder().decode(ResponseType.self, from: data!) { - self.onObject!(decoded) + if let onObject = self.onObject { + if let decoded = try? JSONDecoder().decode(ResponseType.self, from: data) { + onObject(decoded) } } self.response.data = data } }.resume() } + + /// Sets the `Request` to be performed additional times after the initial `call` + public func update(publisher: T) -> Self { + var newPublisher = publisher + .map {_ in Void()} + .assertNoFailure() + .eraseToAnyPublisher() + if let updatePublisher = self.updatePublisher { + newPublisher = newPublisher.merge(with: updatePublisher).eraseToAnyPublisher() + } + return Self.init(params: params, onData: onData, onString: onString, onJson: onJson, onObject: onObject, onError: onError, updatePublisher: newPublisher) + } + + /// Sets the `Request` to be repeated periodically after the initial `call` + public func update(every seconds: TimeInterval) -> Self { + self.update(publisher: Timer.publish(every: seconds, on: .main, in: .common).autoconnect()) + } +} + +extension AnyRequest : Subscriber { + public typealias Input = Void + public typealias Failure = Never + + public func receive(subscription: Subscription) { + subscription.request(.unlimited) + } + + public func receive(_ input: Void) -> Subscribers.Demand { + self.performRequest() + return .none + } + + public func receive(completion: Subscribers.Completion) { + return + } } diff --git a/Sources/Request/Request/RequestBuilder.swift b/Sources/Request/Request/RequestBuilder.swift index b91d26e..e258386 100644 --- a/Sources/Request/Request/RequestBuilder.swift +++ b/Sources/Request/Request/RequestBuilder.swift @@ -10,14 +10,27 @@ import Foundation @_functionBuilder public struct RequestBuilder { public static func buildBlock(_ params: RequestParam...) -> RequestParam { + + let resultParams = params.filter { $0.children == nil } + params.compactMap { $0.children }.joined() // Multiple Urls - if params.filter({ $0.type == .url }).count > 1 { + if resultParams.filter({ $0.type == .url }).count > 1 { fatalError("You cannot specify more than 1 `Url`") } // Missing Url - if params.filter({ $0.type == .url }).count < 1 { + if resultParams.filter({ $0.type == .url }).count < 1 { fatalError("You must have a `Url`") } - return CombinedParams(children: params) + return CombinedParams(children: resultParams) + } + + public static func buildBlock(_ param: RequestParam) -> RequestParam { + return param + } + + public static func buildIf(_ param: RequestParam?) -> RequestParam { + if let param = param { + return param + } + return CombinedParams(children: []) } } diff --git a/Sources/Request/Request/RequestParams/Body.swift b/Sources/Request/Request/RequestParams/Body.swift index f269fe2..4b15cb2 100644 --- a/Sources/Request/Request/RequestParams/Body.swift +++ b/Sources/Request/Request/RequestParams/Body.swift @@ -26,9 +26,7 @@ import Foundation /// } public struct Body: RequestParam { public var type: RequestParamType = .body - public var key: String? public var value: Any? - public var children: [RequestParam]? /// Creates the `Body` from key-value pairs public init(_ dict: [String:Any]) { diff --git a/Sources/Request/Request/RequestParams/Header.swift b/Sources/Request/Request/RequestParams/Header.swift index 0cd7ad8..cb59051 100644 --- a/Sources/Request/Request/RequestParams/Header.swift +++ b/Sources/Request/Request/RequestParams/Header.swift @@ -11,7 +11,6 @@ public struct HeaderParam: RequestParam { public var type: RequestParamType = .header public var key: String? public var value: Any? - public var children: [RequestParam]? = nil public init(key: String, value: String) { self.key = key @@ -23,7 +22,7 @@ public struct HeaderParam: RequestParam { public struct Header { /// Sets the value for any header public static func `Any`(key: String, value: String) -> HeaderParam { - return HeaderParam(key: key, value: value) + HeaderParam(key: key, value: value) } } diff --git a/Sources/Request/Request/RequestParams/Method.swift b/Sources/Request/Request/RequestParams/Method.swift index f6bc991..97eec73 100644 --- a/Sources/Request/Request/RequestParams/Method.swift +++ b/Sources/Request/Request/RequestParams/Method.swift @@ -23,9 +23,7 @@ public enum MethodType: String { /// Sets the method of the `Request` public struct Method: RequestParam { public var type: RequestParamType = .method - public var key: String? public var value: Any? - public var children: [RequestParam]? = nil public init(_ type: MethodType) { self.value = type diff --git a/Sources/Request/Request/RequestParams/QueryParam.swift b/Sources/Request/Request/RequestParams/QueryParam.swift index d7a893e..c0b0b2f 100644 --- a/Sources/Request/Request/RequestParams/QueryParam.swift +++ b/Sources/Request/Request/RequestParams/QueryParam.swift @@ -12,7 +12,6 @@ public struct QueryParam: RequestParam { public var type: RequestParamType = .query public var key: String? public var value: Any? - public var children: [RequestParam]? public init(_ key: String, value: String) { self.key = key @@ -25,7 +24,6 @@ public struct QueryParam: RequestParam { /// `[key:value, key2:value2]` becomes `?key=value&key2=value2` public struct Query: RequestParam { public var type: RequestParamType = .query - public var key: String? public var value: Any? public var children: [RequestParam]? = [] diff --git a/Sources/Request/Request/RequestParams/RequestParam.swift b/Sources/Request/Request/RequestParams/RequestParam.swift index 1d3d3c9..9c9efb8 100644 --- a/Sources/Request/Request/RequestParams/RequestParam.swift +++ b/Sources/Request/Request/RequestParams/RequestParam.swift @@ -15,6 +15,7 @@ public enum RequestParamType { case body case header case combined + case timeout } /// A parameter used to build the `Request` @@ -25,6 +26,12 @@ public protocol RequestParam { var children: [RequestParam]? { get } } +public extension RequestParam { + var key: String? { nil } + var value: Any? { nil } + var children: [RequestParam]? { nil } +} + /// A way to create a custom `RequestParam` /// - Important: You will most likely want to use one of the builtin `RequestParam`s, such as: `Url`, `Method`, `Header`, `Query`, or `Body`. public struct AnyParam: RequestParam { diff --git a/Sources/Request/Request/RequestParams/Timeout.swift b/Sources/Request/Request/RequestParams/Timeout.swift new file mode 100644 index 0000000..d2cc496 --- /dev/null +++ b/Sources/Request/Request/RequestParams/Timeout.swift @@ -0,0 +1,31 @@ +// +// File.swift +// +// +// Created by Carson Katri on 6/23/20. +// + +import Foundation + +/// Sets the `timeoutIntervalForRequest` and/or `timeoutIntervalForResource` of the `Request` +public struct Timeout: RequestParam { + public var type: RequestParamType = .timeout + public var value: Any? = nil + + public init(_ timeout: TimeInterval, for source: Source = .all) { + self.value = (source, timeout) + } + + public struct Source: OptionSet { + public let rawValue: Int + + public init(rawValue: Int) { + self.rawValue = rawValue + } + + public static let request = Self(rawValue: 1 << 0) + public static let resource = Self(rawValue: 1 << 1) + + public static let all: Self = [.request, .resource] + } +} diff --git a/Sources/Request/SwiftUI/RequestView/RequestView.swift b/Sources/Request/SwiftUI/RequestView/RequestView.swift index 7729d90..bf9876f 100644 --- a/Sources/Request/SwiftUI/RequestView/RequestView.swift +++ b/Sources/Request/SwiftUI/RequestView/RequestView.swift @@ -37,7 +37,7 @@ public struct RequestView : View where Content: View, Plac } public var body: some View { - if data == nil || oldReq == nil || oldReq?.id != request.id { + if data == nil/* || oldReq == nil || oldReq?.id != request.id*/ { let req = self.request.onData { data in self.oldReq = self.request self.data = data diff --git a/Tests/RequestTests/JsonTests.swift b/Tests/RequestTests/JsonTests.swift index 3306aa0..54b2131 100644 --- a/Tests/RequestTests/JsonTests.swift +++ b/Tests/RequestTests/JsonTests.swift @@ -54,17 +54,26 @@ class JsonTests: XCTestCase { } func testSubscripts() { - guard let json = try? Json(complexJson) else { + guard var json = try? Json(complexJson) else { XCTAssert(false) return } let subscripts: [JsonSubscript] = ["projects", 0, "codeCov"] - let _: [Any] = [ - json.firstName, - json.projects[0], - json["projects", 0, "stars"], - json[subscripts] - ] + + json[] = 0 + + XCTAssertEqual(json["isEmployed"].bool, true) + json["isEmployed"] = false + XCTAssertEqual(json["isEmployed"].bool, false) + + XCTAssertEqual(json["projects", 0, "stars"].int, 91) + json["projects", 0, "stars"] = 10 + XCTAssertEqual(json["projects", 0, "stars"].int, 10) + + XCTAssertEqual(json[subscripts].double, 0.98) + json[subscripts] = 0.49 + XCTAssertEqual(json[subscripts].double, 0.49) + } func testAccessors() { @@ -84,6 +93,7 @@ class JsonTests: XCTestCase { json.projects[1].codeCov.double, json.projects[1].codeCov.doubleOptional as Any, json.projects[1].passing.boolOptional as Any, + json.projects[1].passing.bool as Any, json.value, ] XCTAssert(true) @@ -95,8 +105,14 @@ class JsonTests: XCTestCase { return } json.firstName = "Cameron" + json.projects[0].stars = 100 json.likes = ["Hello", "World"] + json.projects[1] = ["name" : "hello", "description" : "world"] XCTAssertEqual(json["firstName"].string, "Cameron") + XCTAssertEqual(json["projects"][0].stars.int, 100) + XCTAssertEqual(json["likes"][0].string, "Hello") + XCTAssertEqual(json["likes"][1].string, "World") + XCTAssertEqual(json["projects"][1]["name"].string, "hello") } func testStringify() { @@ -119,6 +135,7 @@ class JsonTests: XCTestCase { ("measureParse", testMeasureParse), ("subscripts", testSubscripts), ("accessors", testAccessors), + ("set", testSet), ("stringify", testStringify), ("measureStringify", testMeasureStringify), ] diff --git a/Tests/RequestTests/RequestTests.swift b/Tests/RequestTests/RequestTests.swift index b044b86..b96738c 100644 --- a/Tests/RequestTests/RequestTests.swift +++ b/Tests/RequestTests/RequestTests.swift @@ -1,6 +1,4 @@ import XCTest -import SwiftUI -import Combine import Json @testable import Request @@ -31,16 +29,31 @@ final class RequestTests: XCTestCase { Url("https://jsonplaceholder.typicode.com/todos") }) } - + + func testRequestWithCondition() { + let condition = true + performRequest(Request { + if condition { + Url(protocol: .https, url: "jsonplaceholder.typicode.com/todos") + } + if !condition { + Url("invalidurl") + } + }) + } + func testPost() { + // Workaround for 'ambiguous reference' error. + let method = Method(.post) performRequest(Request { Url("https://jsonplaceholder.typicode.com/todos") - Method(.post) + method Body([ "title": "My Post", "completed": true, "userId": 3, ]) + Body("{\"userId\" : 3,\"title\" : \"My Post\",\"completed\" : true}") }) } @@ -48,10 +61,11 @@ final class RequestTests: XCTestCase { performRequest(Request { Url("https://jsonplaceholder.typicode.com/todos") Method(.get) - Query(["userId":"1"]) + Query(["userId":"1", "password": "2"]) + Query([QueryParam("key", value: "value"), QueryParam("key2", value: "value2")]) }) } - + func testComplexRequest() { performRequest(Request { Url("https://jsonplaceholder.typicode.com/todos") @@ -60,6 +74,26 @@ final class RequestTests: XCTestCase { Header.CacheControl(.noCache) }) } + + func testHeaders() { + performRequest(Request { + Url("https://jsonplaceholder.typicode.com/todos") + Header.Any(key: "Custom-Header", value: "value123") + Header.Accept(.json) + Header.Accept("text/html") + Header.Authorization(.basic(username: "carsonkatri", password: "password123")) + Header.Authorization(.bearer("authorizationToken")) + Header.CacheControl(.maxAge(1000)) + Header.CacheControl(.maxStale(1000)) + Header.CacheControl(.minFresh(1000)) + Header.ContentLength(0) + Header.ContentType(.xml) + Header.Host("jsonplaceholder.typicode.com") + Header.Origin("www.example.com") + Header.Referer("redirectfrom.example.com") + Header.UserAgent(.firefoxMac) + }) + } func testObject() { struct Todo: Decodable { @@ -73,7 +107,7 @@ final class RequestTests: XCTestCase { var response: [Todo]? = nil var error: Data? = nil - _ = AnyRequest<[Todo]> { + AnyRequest<[Todo]> { Url("https://jsonplaceholder.typicode.com/todos") } .onError { err in @@ -92,10 +126,65 @@ final class RequestTests: XCTestCase { XCTAssert(true) } } - + + func testString() { + let expectation = self.expectation(description: #function) + var response: String? = nil + var error: Data? = nil + + Request { + Url("https://jsonplaceholder.typicode.com/todos") + } + .onError { err in + error = err.error + expectation.fulfill() + } + .onString { result in + response = result + expectation.fulfill() + } + .call() + waitForExpectations(timeout: 10000) + if error != nil { + XCTAssert(false) + } else if response != nil { + XCTAssert(true) + } + } + + func testJson() { + let expectation = self.expectation(description: #function) + var response: Json? = nil + var error: Data? = nil + + Request { + Url("https://jsonplaceholder.typicode.com/todos") + } + .onError { err in + error = err.error + expectation.fulfill() + } + .onJson { result in + response = result + expectation.fulfill() + } + .call() + waitForExpectations(timeout: 10000) + if error != nil { + XCTAssert(false) + } else if let response = response, response.count > 0 { + XCTAssert(true) + } + } + func testRequestGroup() { let expectation = self.expectation(description: #function) var loaded: Int = 0 + var datas: Int = 0 + var strings: Int = 0 + var jsons: Int = 0 + var errors: Int = 0 + let numberOfResponses = 10 RequestGroup { Request { Url("https://jsonplaceholder.typicode.com/todos") @@ -106,18 +195,51 @@ final class RequestTests: XCTestCase { Request { Url("https://jsonplaceholder.typicode.com/todos/1") } + Request { + Url("invalidURL") + } } .onData { (index, data) in if data != nil { loaded += 1 + datas += 1 + } + if loaded >= numberOfResponses { + expectation.fulfill() + } + } + .onString { (index, string) in + if string != nil { + loaded += 1 + strings += 1 + } + if loaded >= numberOfResponses { + expectation.fulfill() + } + } + .onJson { (index, json) in + if json != nil { + loaded += 1 + jsons += 1 } - if loaded >= 3 { + if loaded >= numberOfResponses { expectation.fulfill() } } + .onError({ (index, error) in + loaded += 1 + errors += 1 + if loaded >= numberOfResponses { + expectation.fulfill() + } + }) .call() waitForExpectations(timeout: 10000) - XCTAssertEqual(loaded, 3) + XCTAssertEqual(loaded, numberOfResponses) + XCTAssertEqual(datas, 3) + XCTAssertEqual(strings, 3) + XCTAssertEqual(jsons, 3) + XCTAssertEqual(errors, 1) } func testRequestChain() { @@ -126,6 +248,7 @@ final class RequestTests: XCTestCase { RequestChain { Request.chained { (data, err) in Url("https://jsonplaceholder.typicode.com/todos") + Method(.get) } Request.chained { (data, err) in let json = try? Json(data[0]!) @@ -141,6 +264,27 @@ final class RequestTests: XCTestCase { waitForExpectations(timeout: 10000) XCTAssert(success) } + + func testRequestChainErrors() { + let expectation = self.expectation(description: #function) + var success = false + RequestChain { + Request.chained { (data, err) in + Url("invalidurl") + } + Request.chained { (data, err) in + Url("https://jsonplaceholder.typicode.com/thispagedoesnotexist") + } + } + .call { (data, errors) in + if errors.count == 2 { + success = true + } + expectation.fulfill() + } + waitForExpectations(timeout: 10000) + XCTAssert(success) + } func testAnyRequest() { let expectation = self.expectation(description: #function) @@ -167,15 +311,75 @@ final class RequestTests: XCTestCase { waitForExpectations(timeout: 10000) XCTAssert(success) } + + func testError() { + let expectation = self.expectation(description: #function) + var success = false + + Request { + Url("https://jsonplaceholder.typicode./todos") + } + .onError { err in + print(err) + success = true + expectation.fulfill() + } + .call() + waitForExpectations(timeout: 10000) + XCTAssert(success) + } + + func testUpdate() { + let expectation = self.expectation(description: #function) + var numResponses = 0 + + Request { + Url("https://jsonplaceholder.typicode.com/todos") + } + .update(every: 1) + .onData { data in + numResponses += 1 + if numResponses >= 3 { + expectation.fulfill() + } + } + .call() + waitForExpectations(timeout: 10000) + } + + func testTimeout() { + let expectation = self.expectation(description: #function) + + Request { + Url("http://10.255.255.1") + Timeout(1, for: .all) + } + .onError { error in + if let err = error.error, let msg = String(data: err, encoding: .utf8) { + if msg == "The request timed out." { + expectation.fulfill() + } + } + } + .call() + + waitForExpectations(timeout: 2000) + } static var allTests = [ ("simpleRequest", testSimpleRequest), ("post", testPost), ("query", testQuery), ("complexRequest", testComplexRequest), + ("headers", testHeaders), ("onObject", testObject), + ("onString", testString), + ("onJson", testJson), ("requestGroup", testRequestGroup), ("requestChain", testRequestChain), + ("requestChainErrors", testRequestChainErrors), ("anyRequest", testAnyRequest), + ("testError", testError), + ("testUpdate", testUpdate), ] }