Issue #195
Miami
- https://github.com/onmyway133/Miami
- Future based builders
- Should not wrap system API
URLSession
offer tons of thing that it’s hard to use with wrappers like Alamofire
Concerns
Parameter encoding is confusing
-https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#parameter-encoding
Query and body builder
- Understanding HTML Form Encoding: URL Encoded and Multipart Forms
- https://stackoverflow.com/questions/14551194/how-are-parameters-sent-in-an-http-post-request
- https://stackoverflow.com/questions/1617058/ok-to-skip-slash-before-query-string
- https://stackoverflow.com/questions/3508338/what-is-the-boundary-in-multipart-form-data
- How to construct URL with URLComponents and appendPathComponent in Swift
HTTP
Lazy execution
- Promise https://github.com/onmyway133/Then
- Signal and Future https://github.com/onmyway133/archives/tree/master/Signal
Catch error
Implementation
Use Promise to handle chain and error
https://github.com/onmyway133/Miami/blob/master/Sources/Shared/Future/Future.swift
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
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
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
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
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)
}
}