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

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)
    }
}