Skip to content

Commit

Permalink
feature: add timeout to SabyNetwork
Browse files Browse the repository at this point in the history
  • Loading branch information
0xWOF committed Nov 15, 2023
1 parent 16ade61 commit f3154b0
Show file tree
Hide file tree
Showing 7 changed files with 129 additions and 22 deletions.
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ var package = Package(
path: "Source/TestFake"),
.target(
name: "SabyNetwork",
dependencies: ["SabyConcurrency", "SabyJSON", "SabySafe"],
dependencies: ["SabyConcurrency", "SabyJSON", "SabySafe", "SabyTime"],
path: "Source/Network"),
.target(
name: "SabyNumeric",
Expand Down
22 changes: 20 additions & 2 deletions Source/Network/Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import Foundation

import SabyTime
import SabyConcurrency

public protocol Client<Request, Response> {
Expand All @@ -18,6 +19,7 @@ public protocol Client<Request, Response> {
method: ClientMethod,
header: ClientHeader,
body: Request,
timeout: Interval?,
optionBlock: (inout URLRequest) -> Void
) -> Promise<ClientResult<Response>, Error>
}
Expand All @@ -27,19 +29,35 @@ extension Client {
_ url: URL,
method: ClientMethod = .get,
header: ClientHeader = [:],
timeout: Interval? = nil,
optionBlock: (inout URLRequest) -> Void = { _ in }
) -> Promise<ClientResult<Response>, Error> where RequestValue? == Request {
request(url: url, method: method, header: header, body: nil, optionBlock: optionBlock)
request(
url: url,
method: method,
header: header,
body: nil,
timeout: timeout,
optionBlock: optionBlock
)
}

public func request(
_ url: URL,
method: ClientMethod = .get,
header: ClientHeader = [:],
body: Request,
timeout: Interval? = nil,
optionBlock: (inout URLRequest) -> Void = { _ in }
) -> Promise<ClientResult<Response>, Error> {
request(url: url, method: method, header: header, body: body, optionBlock: optionBlock)
request(
url: url,
method: method,
header: header,
body: body,
timeout: timeout,
optionBlock: optionBlock
)
}
}

Expand Down
20 changes: 20 additions & 0 deletions Source/Network/Implement/Client/DataClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Foundation

import SabyConcurrency
import SabyJSON
import SabyTime

public final class DataClient: Client {
public typealias Request = Data?
Expand Down Expand Up @@ -37,6 +38,7 @@ extension DataClient {
method: ClientMethod = .get,
header: ClientHeader = [:],
body: Data? = nil,
timeout: Interval? = nil,
optionBlock: (inout URLRequest) -> Void = { _ in }
) -> Promise<ClientResult<Data?>, Error> {
let pending = Promise<ClientResult<Data?>, Error>.pending()
Expand Down Expand Up @@ -73,13 +75,31 @@ extension DataClient {
pending.onCancel {
task.cancel()
}
if let timeout {
let item = DispatchWorkItem {
pending.reject(DataClientError.timeout)
task.cancel()
}
DispatchQueue.global().asyncAfter(
deadline: .now() + timeout.dispatchTime,
execute: item
)
pending.promise.finally {
item.cancel()
}
pending.onCancel {
item.cancel()
}
}

task.resume()

return pending.promise
}
}

public enum DataClientError: Error {
case timeout
case statusCodeNotFound
case statusCodeNot2XX(codeNot2XX: Int, body: Data?)
}
9 changes: 8 additions & 1 deletion Source/Network/Implement/Client/JSONClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Foundation
import SabyConcurrency
import SabySafe
import SabyJSON
import SabyTime

public final class JSONClient: Client {
let client: DataClient
Expand Down Expand Up @@ -42,6 +43,7 @@ extension JSONClient {
method: ClientMethod = .get,
header: ClientHeader = [:],
body: JSON? = nil,
timeout: Interval? = nil,
optionBlock: (inout URLRequest) -> Void = { _ in }
) -> Promise<ClientResult<JSON>, Error> {
guard let body = try? self.encoder.encode(body) else {
Expand All @@ -53,6 +55,7 @@ extension JSONClient {
method: method,
header: header,
body: body,
timeout: timeout,
optionBlock: optionBlock
)
.then { code2XX, data -> ClientResult<JSON> in
Expand All @@ -64,7 +67,10 @@ extension JSONClient {
return (code2XX, body)
}
.catch { error in
if case DataClientError.statusCodeNotFound = error {
if case DataClientError.timeout = error {
throw JSONClientError.timeout
}
else if case DataClientError.statusCodeNotFound = error {
throw JSONClientError.statusCodeNotFound
}
else if case DataClientError.statusCodeNot2XX(let codeNot2XX, let data) = error {
Expand All @@ -78,6 +84,7 @@ extension JSONClient {
}

public enum JSONClientError: Error {
case timeout
case statusCodeNotFound
case statusCodeNot2XX(codeNot2XX: Int, body: JSON)
case bodyIsNotEncodable
Expand Down
38 changes: 20 additions & 18 deletions Source/TestMock/MockURLProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import Foundation

import SabyJSON
import SabyConcurrency

public protocol URLResultStorage {
static var results: [URLResult] { get }
Expand Down Expand Up @@ -43,15 +44,16 @@ public final class MockURLProtocol<Storage: URLResultStorage>: URLProtocol {
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
}

if let data = result.data {
client?.urlProtocol(self, didLoad: data)
}

if let error = result.error {
client?.urlProtocol(self, didFailWithError: error)
}
else {
client?.urlProtocolDidFinishLoading(self)

result.data.then { data in
guard let data else { return }
self.client?.urlProtocol(self, didLoad: data)
}
.then {
self.client?.urlProtocolDidFinishLoading(self)
}
}

Expand All @@ -67,10 +69,14 @@ extension MockURLProtocol {
public struct URLResult {
let url: URL?
let response: URLResponse?
let data: Data?
let data: Promise<Data?, Never>
let error: Error?

public init(url: URL, code: Int, data: Data?) {
self.init(url: url, code: code, data: .resolved(data))
}

public init(url: URL, code: Int, data: Promise<Data?, Never>) {
let response = HTTPURLResponse(url: url, statusCode: code, httpVersion: nil, headerFields: nil)

self.url = url
Expand All @@ -80,26 +86,22 @@ public struct URLResult {
}

public init(url: URL, code: Int, json: JSON) {
self.init(url: url, code: code, json: .resolved(json))
}

public init(url: URL, code: Int, json: Promise<JSON, Never>) {
let response = HTTPURLResponse(url: url, statusCode: code, httpVersion: nil, headerFields: nil)
let (data, error) = { () -> (Data?, Error?) in
do {
return (try json.datafy(), nil)
}
catch {
return (nil, error)
}
}()

self.url = url
self.response = response
self.data = data
self.error = error
self.data = json.then { try? $0.datafy() }
self.error = nil
}

public init(url: URL, error: Error) {
self.url = url
self.response = nil
self.data = nil
self.data = .resolved(nil)
self.error = error
}
}
30 changes: 30 additions & 0 deletions Test/Network/DataClientTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import XCTest

import SabyTestMock
import SabyTestExpect
import SabyConcurrency

final class DataClientTest: XCTestCase {
func test__init() {
Expand Down Expand Up @@ -88,4 +89,33 @@ final class DataClientTest: XCTestCase {

Expect.promise(response, state: .rejected(Expect.SampleError.one), timeout: .seconds(2))
}

func test__request_timeout() {
final class MockURLResultStorage: URLResultStorage {
static var results: [URLResult] = [
URLResult(
url: URL(string: "https://mock.api.ab180.co/request")!,
code: 200,
data: Promise.delay(.milliseconds(2000)).then { Data() }
)
]
}
let client = DataClient() {
$0.protocolClasses = [MockURLProtocol<MockURLResultStorage>.self]
}

let response = client.request(
URL(string: "https://mock.api.ab180.co/request")!,
timeout: .millisecond(50)
)

response.then { data in
print(data)
}
.catch { error in
print(error)
}

Expect.promise(response, state: .rejected(DataClientError.timeout), timeout: .seconds(2))
}
}
30 changes: 30 additions & 0 deletions Test/Network/JSONClientTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import XCTest
import SabyJSON
import SabyTestMock
import SabyTestExpect
import SabyConcurrency

final class JSONClientTest: XCTestCase {
func test__init() {
Expand Down Expand Up @@ -182,4 +183,33 @@ final class JSONClientTest: XCTestCase {
timeout: .seconds(2)
)
}

func test__request_timeout() {
final class MockURLResultStorage: URLResultStorage {
static var results: [URLResult] = [
URLResult(
url: URL(string: "https://mock.api.ab180.co/request")!,
code: 200,
data: Promise.delay(.milliseconds(2000)).then { try! JSON.from([:]).datafy() }
)
]
}
let client = JSONClient() {
$0.protocolClasses = [MockURLProtocol<MockURLResultStorage>.self]
}

let response = client.request(
URL(string: "https://mock.api.ab180.co/request")!,
timeout: .millisecond(50)
)

response.then { data in
print(data)
}
.catch { error in
print(error)
}

Expect.promise(response, state: .rejected(JSONClientError.timeout), timeout: .seconds(2))
}
}

0 comments on commit f3154b0

Please sign in to comment.