How to build a networking in Swift

Issue #195

Miami

Concerns

Parameter encoding is confusing

-https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#parameter-encoding

Query and body builder

HTTP

Lazy execution

Catch error

Implementation

Use Promise to handle chain and error

https://github.com/onmyway133/Miami/blob/master/Sources/Shared/Future/Future.swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
import Foundation

public class Token {
private let lock = NSRecursiveLock()
private var _isCancelled = false
private var callbacks: [() -> Void] = []

public init() {}

public func isCancelled() -> Bool {
return lock.whenLock {
return _isCancelled
}
}

public func cancel() {
lock.whenLock {
guard self._isCancelled == false else {
return
}

self._isCancelled = true
self.callbacks.forEach { $0() }
self.callbacks.removeAll()
}
}

public func onCancel(_ callback: @escaping () -> Void) {
lock.whenLock {
self.callbacks.append(callback)
}
}
}

public class Resolver<T> {
public let queue: DispatchQueue
public let token: Token
private var callback: (Result<T, Error>) -> Void

public init(queue: DispatchQueue, token: Token, callback: @escaping (Result<T, Error>) -> Void) {
self.queue = queue
self.token = token
self.callback = callback
}

public func complete(value: T) {
self.handle(result: .success(value))
}

public func fail(error: Error) {
self.handle(result: .failure(error))
}

public func handle(result: Result<T, Error>) {
queue.async {
self.callback(result)
self.callback = { _ in }
}
}
}

public class Future<T> {
public let work: (Resolver<T>) -> Void

public init(work: @escaping (Resolver<T>) -> Void) {
self.work = work
}

public static func fail(error: Error) -> Future<T> {
return Future<T>.result(.failure(error))
}

public static func complete(value: T) -> Future<T> {
return .result(.success(value))
}

public static func result(_ result: Result<T, Error>) -> Future<T> {
return Future<T>(work: { resolver in
switch result {
case .success(let value):
resolver.complete(value: value)
case .failure(let error):
resolver.fail(error: error)
}
})
}

public func run(queue: DispatchQueue = .serial(), token: Token = Token(), completion: @escaping (Result<T, Error>) -> Void) {
queue.async {
if (token.isCancelled()) {
completion(.failure(NetworkError.cancelled))
return
}

let resolver = Resolver<T>(queue: queue, token: token, callback: completion)
self.work(resolver)
}
}

public func map<U>(transform: @escaping (T) -> U) -> Future<U> {
return Future<U>(work: { resolver in
self.run(queue: resolver.queue, token: resolver.token, completion: { result in
resolver.handle(result: result.map(transform))
})
})
}

public func flatMap<U>(transform: @escaping (T) -> Future<U>) -> Future<U> {
return Future<U>(work: { resolver in
self.run(queue: resolver.queue, token: resolver.token, completion: { result in
switch result {
case .success(let value):
let future = transform(value)
future.run(queue: resolver.queue, token: resolver.token, completion: { newResult in
resolver.handle(result: newResult)
})
case .failure(let error):
resolver.fail(error: error)
}
})
})
}

public func catchError(transform: @escaping (Error) -> Future<T>) -> Future<T> {
return Future<T>(work: { resolver in
self.run(queue: resolver.queue, token: resolver.token, completion: { result in
switch result {
case .success(let value):
resolver.complete(value: value)
case .failure(let error):
let future = transform(error)
future.run(queue: resolver.queue, token: resolver.token, completion: { newResult in
resolver.handle(result: newResult)
})
}
})
})
}

public func delay(seconds: TimeInterval) -> Future<T> {
return Future<T>(work: { resolver in
resolver.queue.asyncAfter(deadline: DispatchTime.now() + seconds, execute: {
self.run(queue: resolver.queue, token: resolver.token, completion: { result in
resolver.handle(result: result)
})
})
})
}

public func log(closure: @escaping (Result<T, Error>) -> Void) -> Future<T> {
return Future<T>(work: { resolver in
self.run(queue: resolver.queue, token: resolver.token, completion: { result in
closure(result)
resolver.handle(result: result)
})
})
}

public static func sequence(futures: [Future<T>]) -> Future<Sequence<T>> {
var index = 0
var values = [T]()

func runNext(resolver: Resolver<Sequence<T>>) {
guard index < futures.count else {
let sequence = Sequence(values: values)
resolver.complete(value: sequence)
return
}

let future = futures[index]
index += 1

future.run(queue: resolver.queue, token: resolver.token, completion: { result in
switch result {
case .success(let value):
values.append(value)
runNext(resolver: resolver)
case .failure(let error):
resolver.fail(error: error)
}
})
}

return Future<Sequence<T>>(work: runNext)
}
}

extension NSLocking {
@inline(__always)
func whenLock<T>(_ closure: () throws -> T) rethrows -> T {
lock()
defer { unlock() }
return try closure()
}

@inline(__always)
func whenLock(_ closure: () throws -> Void) rethrows {
lock()
defer { unlock() }
try closure()
}
}

Query builder to build query

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import Foundation

public protocol QueryBuilder {
func build() -> [URLQueryItem]
}

public class DefaultQueryBuilder: QueryBuilder {
public let parameters: JSONDictionary

public init(parameters: JSONDictionary = [:]) {
self.parameters = parameters
}

public func build() -> [URLQueryItem] {
var components = URLComponents()

let parser = ParameterParser()
let pairs = parser
.parse(parameters: parameters)
.map({ $0 })
.sorted(by: <)

components.queryItems = pairs.map({ key, value in
URLQueryItem(name: key, value: value)
})

return components.queryItems ?? []
}

public func build(queryItems: [URLQueryItem]) -> String {
var components = URLComponents()
components.queryItems = queryItems.map({
return URLQueryItem(name: escape($0.name), value: escape($0.value ?? ""))
})

return components.query ?? ""
}

public func escape(_ string: String) -> String {
return string.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? ""
}
}

Body builder to build body

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import Foundation

public class JsonBodyBuilder: BodyBuilder {
public let parameters: JSONDictionary

public init(parameters: JSONDictionary) {
self.parameters = parameters
}

public func build() -> ForBody? {
guard let data = try? JSONSerialization.data(
withJSONObject: parameters,
options: JSONSerialization.WritingOptions()
) else {
return nil
}

return ForBody(body: data, headers: [
Header.contentType.rawValue: "application/json"
])
}
}

Make request with networking

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import Foundation

public class Networking {
public let session: URLSession
public let mockManager = MockManager()

public var before: (URLRequest) -> URLRequest = { $0 }
public var catchError: (Error) -> Future<Response> = { error in Future.fail(error: error) }
public var validate: (Response) -> Future<Response> = { Future.complete(value: $0) }
public var logResponse: (Result<Response, Error>) -> Void = { _ in }

public init(session: URLSession = .shared) {
self.session = session
}

public func make(options: Options, baseUrl: URL) -> Future<Response> {
let builder = UrlRequestBuilder()
do {
let request = try builder.build(options: options, baseUrl: baseUrl)
return make(request: request)
} catch {
return Future<Response>.fail(error: error)
}
}

public func make(request: URLRequest) -> Future<Response> {
if let mock = mockManager.findMock(request: request) {
return mock.future.map(transform: { Response(data: $0, urlResponse: URLResponse()) })
}

let future = Future<Response>(work: { resolver in
let task = self.session.dataTask(with: request, completionHandler: { data, response, error in
if let data = data, let urlResponse = response {
resolver.complete(value: Response(data: data, urlResponse: urlResponse))
} else if let error = error {
resolver.fail(error: NetworkError.urlSession(error, response))
} else {
resolver.fail(error: NetworkError.unknownError)
}
})

resolver.token.onCancel {
task.cancel()
}

task.resume()
})

return future
.catchError(transform: self.catchError)
.flatMap(transform: self.validate)
.log(closure: self.logResponse)
}
}

Mock a request

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import Foundation

public class Mock {
public let options: Options
public let future: Future<Data>

public init(options: Options, future: Future<Data>) {
self.options = options
self.future = future
}

public static func on(options: Options, data: Data) -> Mock {
return Mock(options: options, future: Future.complete(value: data))
}

public static func on(options: Options, error: Error) -> Mock {
return Mock(options: options, future: Future.fail(error: error))
}

public static func on(options: Options, file: String, fileExtension: String, bundle: Bundle = Bundle.main) -> Mock {
guard
let url = bundle.url(forResource: file, withExtension: fileExtension),
let data = try? Data(contentsOf: url)
else {
return .on(options: options, error: NetworkError.invalidMock)
}

return .on(options: options, data: data)
}
}

Comments