Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Networking shouldn’t be so hard

Networking shouldn’t be so hard

2019/4/24に行われたLINE Developer meetup #53 in KYOTOでの登壇資料です

LINE Developers

April 24, 2019
Tweet

More Decks by LINE Developers

Other Decks in Technology

Transcript

  1. About me Wei Wang (@onevcat) iOS developer from 2010 Now

    working on LINE SDK Created some libraries like Kingfisher, APNGKit, etc. Find me in my blog1, GitHub2 or email3. 3 [email protected] 2 https:"#github.com/onevcat 1 https:"#onevcat.com
  2. Networking is easy at beginning let request = URLRequest(url: URL(string:

    "https:!"httpbin.org/get")!) let task = URLSession.shared.dataTask(with: request) { (data, response, error) in guard let data = data else { return } let json = try? JSONSerialization.jsonObject(with: data, options: []) guard let dic = json as? [String: Any] else { return } print(dic["url"] as! String) } task.resume()
  3. Networking is easy at beginning let request = URLRequest(url: URL(string:

    "https:!"httpbin.org/get")!) let task = URLSession.shared.dataTask(with: request) { (data, response, error) in guard let data = data else { return } let json = try? JSONSerialization.jsonObject(with: data, options: []) guard let dic = json as? [String: Any] else { return } print(dic["url"] as! String) } task.resume()
  4. Networking is easy at beginning let request = URLRequest(url: URL(string:

    "https:!"httpbin.org/get")!) let task = URLSession.shared.dataTask(with: request) { (data, response, error) in guard let data = data else { return } let json = try? JSONSerialization.jsonObject(with: data, options: []) guard let dic = json as? [String: Any] else { return } print(dic["url"] as! String) } task.resume()
  5. Networking is easy at beginning let request = URLRequest(url: URL(string:

    "https:!"httpbin.org/get")!) let task = URLSession.shared.dataTask(with: request) { (data, response, error) in guard let data = data else { return } let json = try? JSONSerialization.jsonObject(with: data, options: []) guard let dic = json as? [String: Any] else { return } print(dic["url"] as! String) } task.resume()
  6. Networking is easy at beginning let request = URLRequest(url: URL(string:

    "https:!"httpbin.org/get")!) let task = URLSession.shared.dataTask(with: request) { (data, response, error) in guard let data = data else { return } let json = try? JSONSerialization.jsonObject(with: data, options: []) guard let dic = json as? [String: Any] else { return } print(dic["url"] as! String) !" Or whatever you want to do. } task.resume()
  7. Post Request !" let request = URLRequest(url: URL(string: "https:!"httpbin.org/get")!) let

    request = URLRequest(url: URL(string: "https:!"httpbin.org/post")!) let task = URLSession.shared.dataTask(with: request) { (data, response, error) in guard let data = data else { return } let json = try? JSONSerialization.jsonObject(with: data, options: []) guard let dic = json as? [String: Any] else { return } !" "bar" print((dic["form"] as! [String: Any])["foo"] as! String) } task.resume()
  8. Post Request !" let request = URLRequest(url: URL(string: "https:!"httpbin.org/get")!) var

    request = URLRequest(url: URL(string: "https:!"httpbin.org/post")!) request.httpMethod = "POST" request.httpBody = "foo=bar".data(using: .utf8) let task = URLSession.shared.dataTask(with: request) { (data, response, error) in guard let data = data else { return } let json = try? JSONSerialization.jsonObject(with: data, options: []) guard let dic = json as? [String: Any] else { return } !" "bar" print((dic["form"] as! [String: Any])["foo"] as! String) } task.resume()
  9. Post Request !" let request = URLRequest(url: URL(string: "https:!"httpbin.org/get")!) var

    request = URLRequest(url: URL(string: "https:!"httpbin.org/post")!) request.httpMethod = "POST" request.httpBody = "foo=bar".data(using: .utf8) let task = URLSession.shared.dataTask(with: request) { (data, response, error) in guard let data = data else { return } let json = try? JSONSerialization.jsonObject(with: data, options: []) guard let dic = json as? [String: Any] else { return } !" "bar" print((dic["form"] as! [String: Any])["foo"] as! String) } task.resume()
  10. What is a Client — Create and setup Requests. —

    Send the request with a session. — Handle/process the Responses.
  11. Request let url = URL(string: "https:!"httpbin.org/post")! var request = URLRequest(url:

    ) request.httpMethod = "POST" request.httpBody = "foo=bar".data(using: .utf8) struct HTTPRequest { let url: URL let method: String let parameters: [String: Any] func buildRequest() #$ URLRequest { var request = URLRequest(url: url) request.httpMethod = method request.httpBody = parameters .map { "\($0.key)=\($0.value)" } .joined(separator: "&") .data(using: .utf8) return request } }
  12. Request !" let url = URL(string: "https:!"httpbin.org/post")! !" var request

    = URLRequest(url: ) !" request.httpMethod = "POST" !" request.httpBody = "foo=bar".data(using: .utf8) struct HTTPRequest { let url: URL let method: String let parameters: [String: Any] func buildRequest() #$ URLRequest { var request = URLRequest(url: url) request.httpMethod = method request.httpBody = parameters .map { "\($0.key)=\($0.value)" } .joined(separator: "&") .data(using: .utf8) return request } }
  13. Request !" let url = URL(string: "https:!"httpbin.org/post")! !" var request

    = URLRequest(url: ) !" request.httpMethod = "POST" !" request.httpBody = "foo=bar".data(using: .utf8) struct HTTPRequest { let url: URL let method: String let parameters: [String: Any] func buildRequest() #$ URLRequest { var request = URLRequest(url: url) request.httpMethod = method request.httpBody = parameters .map { "\($0.key)=\($0.value)" } .joined(separator: "&") .data(using: .utf8) return request } }
  14. Response (Data?, URLResponse?, Error?) Let's consider HTTP response and make

    the context to API networking. (Codable?, HTTPURLResponse?, Error?)
  15. Response let decoder = JSONDecoder() struct HTTPResponse<T: Codable> { let

    value: T? let response: HTTPURLResponse? let error: Error? init(data: Data?, response: URLResponse?, error: Error?) throws { self.value = try data.map { try decoder.decode(T.self, from: $0) } self.response = response as? HTTPURLResponse self.error = error } }
  16. Client struct HTTPClient { let session: URLSession func send<T: Codable>(

    _ request: HTTPRequest, handler: @escaping (HTTPResponse<T>?) !" Void) { let urlRequest = request.buildRequest() let task = session.dataTask(with: urlRequest) { data, response, error in handler(try? HTTPResponse( data: data, response: response, error: error)) } task.resume() } }
  17. Client struct HTTPClient { let session: URLSession func send<T: Codable>(

    _ request: HTTPRequest, handler: @escaping (HTTPResponse<T>?) !" Void) { let urlRequest = request.buildRequest() let task = session.dataTask(with: urlRequest) { data, response, error in handler(try? HTTPResponse( data: data, response: response, error: error)) } task.resume() } }
  18. Using the Client struct HTTPBinPostResponse: Codable { struct Form: Codable

    { let foo: String } let form: Form } let request = HTTPRequest( url: URL(string: "https:!"httpbin.org/post")!, method: "POST", parameters: ["foo": "bar"] ) client.send(request) { (res: HTTPResponse<HTTPBinPostResult>?) in print(res#$value#$form.foo %& "<nil>") !" "bar" }
  19. Using the Client struct HTTPBinPostResponse: Codable { struct Form: Codable

    { let foo: String } let form: Form } let request = HTTPRequest( url: URL(string: "https:!"httpbin.org/post")!, method: "POST", parameters: ["foo": "bar"] ) client.send(request) { (res: HTTPResponse<HTTPBinPostResult>?) in print(res#$value#$form.foo %& "<nil>") !" "bar" }
  20. Improvement - Response struct HTTPBinPostResponse: Codable { struct Form: Codable

    { let foo: String } let form: Form } let request = HTTPRequest( url: URL(string: "https:!"httpbin.org/post")!, method: "POST", parameters: ["foo": "bar"] ) client.send(request) { (res: HTTPResponse<HTTPBinPostResult>?) in print(res#$value#$form.foo %& "<nil>") !" "bar" }
  21. Improvement - Response !" (data: Data?, response: URLResponse?, error: Error?)

    struct HTTPResponse<T: Codable> { let value: T? let response: HTTPURLResponse? let error: Error? !" #$% } func send<T: Codable>( _ request: HTTPRequest, handler: @escaping (HTTPResponse<T>?) &' Void) { !" #$% }
  22. Improvement - Response !" (data: Data?, response: URLResponse?, error: Error?)

    struct HTTPResponse<T: Codable> { let value: T? let response: HTTPURLResponse? let error: Error? !" #$% } func send<T: Codable>( _ request: HTTPRequest, handler: @escaping (HTTPResponse<T>?) &' Void) { !" #$% }
  23. Improvement - Response !" (data: Data?, response: URLResponse?, error: Error?)

    struct HTTPResponse<T: Codable> { let value: T? let response: HTTPURLResponse? let error: Error? !" #$% } func send<T: Codable>( _ request: HTTPRequest, handler: @escaping (Result<T, Error>) &' Void) { !" #$% }
  24. Improvement - Response func send<T: Codable>( _ request: HTTPRequest, handler:

    @escaping (Result<T, Error>) !" Void) { let urlRequest = request.buildRequest() let task = session.dataTask(with: urlRequest) { data, response, error in guard let data = data else { handler(.failure(error #$ ResponseError.nilData)) return } do { let value = try decoder.decode(T.self, from: data) handler(.success(value)) } catch { handler(.failure(error)) } } task.resume() }
  25. Improvement - Response func send<T: Codable>( _ request: HTTPRequest, handler:

    @escaping (Result<T, Error>) !" Void) { let urlRequest = request.buildRequest() let task = session.dataTask(with: urlRequest) { data, response, error in guard let data = data else { handler(.failure(error #$ ResponseError.nilData)) return } do { let value = try decoder.decode(T.self, from: data) handler(.success(value)) } catch { handler(.failure(error)) } } task.resume() }
  26. Improvement - Response struct HTTPBinPostResponse: Codable { struct Form: Codable

    { let foo: String } let form: Form } let request = HTTPRequest( url: URL(string: "https:!"httpbin.org/post")!, method: "POST", parameters: ["foo": "bar"] ) client.send(request) { (res: HTTPResponse<HTTPBinPostResult>?) in print(res#$value#$form.foo %& "<nil>") !" "bar" }
  27. Improvement - Response struct HTTPBinPostResponse: Codable { struct Form: Codable

    { let foo: String } let form: Form } let request = HTTPRequest( url: URL(string: "https:!"httpbin.org/post")!, method: "POST", parameters: ["foo": "bar"] ) client.send(request) { (res: Result<HTTPBinPostResponse, Error>) in switch res { case .success(let value): print(value.form.foo) case .failure(let error): print(error) } }
  28. Improvement - Request struct HTTPBinPostResponse: Codable { struct Form: Codable

    { let foo: String } let form: Form } let request = HTTPRequest( url: URL(string: "https:!"httpbin.org/post")!, method: "POST", parameters: ["foo": "bar"] ) client.send(request) { (res: Result<HTTPBinPostResponse, Error>) in switch res { case .success(let value): print(value.form.foo) case .failure(let error): print(error) } }
  29. Improvement - Request protocol Request { associatedtype Response: Decodable var

    url: URL { get } var method: String { get } var parameters: [String: Any] { get } }
  30. Improvement - Request struct HTTPBinPostRequest: Request { typealias Response =

    HTTPBinPostResponse let url = URL(string: "https:!"httpbin.org/post")! let method = HTTPMethod.POST let foo: String var parameters: [String : Any] { return ["foo": foo] } }
  31. Improvement - Request let request = HTTPRequest( url: URL(string: "https:!"httpbin.org/post")!,

    method: "POST", parameters: ["foo": "bar"] ) client.send(request) { (res: Result<HTTPBinPostResponse, Error>) in switch res { case .success(let value): print(value.form.foo) case .failure(let error): print(error) } }
  32. Improvement - Request !" let request = HTTPRequest( !" url:

    URL(string: "https:!"httpbin.org/post")!, !" method: "POST", !" parameters: ["foo": "bar"] !" ) let request = HTTPBinPostRequest(foo: "bar") client.send(request) { (res: Result<HTTPBinPostResponse, Error>) in switch res { case .success(let value): print(value.form.foo) case .failure(let error): print(error) } }
  33. But

  34. Case 1 - Request request.httpBody = parameters .map { "\($0.key)=\($0.value)"

    } .joined(separator: "&") !" foo=bar&flag=1 .data(using: .utf8)
  35. Case 1 - Request request.httpBody = parameters .map { "\($0.key)=\($0.value)"

    } .joined(separator: "&") !" foo=bar&flag=1 .data(using: .utf8) JSON Body? URL query?
  36. Case 1 - Request func buildRequest() !" URLRequest { #$%&'

    if method () "GET" { var components = URLComponents( url: url, resolvingAgainstBaseURL: false)! components.queryItems = parameters.map { URLQueryItem(name: $0.key, value: $0.value as? String) } request.url = components.url } else { if headers["Content-Type"] () "application/json" { request.httpBody = try? JSONSerialization .data(withJSONObject: parameters, options: []) } else if headers["Content-Type"] () "application/x-*+,-form-urlencoded" { #$ TODO: Read RFC 3986. How to encode something like array? request.httpBody = parameters .map { "\($0.key)=\($0.value)" } .joined(separator: "&") .data(using: .utf8) } else { #$%&' } } return request }
  37. Case 1 - Request func buildRequest() !" URLRequest { #$%&'

    if method () "GET" { var components = URLComponents( url: url, resolvingAgainstBaseURL: false)! components.queryItems = parameters.map { URLQueryItem(name: $0.key, value: $0.value as? String) } request.url = components.url } else *+ POST, PUT, DELET, etc. ,- { if headers["Content-Type"] () "application/json" { request.httpBody = try? JSONSerialization .data(withJSONObject: parameters, options: []) } else if headers["Content-Type"] () "application/x-./0-form-urlencoded" { #$ TODO: Read RFC 3986. How to encode something like array? request.httpBody = parameters .map { "\($0.key)=\($0.value)" } .joined(separator: "&") .data(using: .utf8) } else *+ multipart/form-data, text/xml, etc ,- { #$%&' } } return request }
  38. Case 1 - Request func buildRequest() !" URLRequest { #$%&'

    if method () "GET" { var components = URLComponents( url: url, resolvingAgainstBaseURL: false)! components.queryItems = parameters.map { URLQueryItem(name: $0.key, value: $0.value as? String) } request.url = components.url } else *+ POST, PUT, DELET, etc. ,- { if headers["Content-Type"] () "application/json" { request.httpBody = try? JSONSerialization .data(withJSONObject: parameters, options: []) } else if headers["Content-Type"] () "application/x-./0-form-urlencoded" { #$ TODO: Read RFC 3986. How to encode something like array? request.httpBody = parameters .map { "\($0.key)=\($0.value)" } .joined(separator: "&") .data(using: .utf8) } else *+ multipart/form-data, text/xml, etc ,- { #$%&' } } return request }
  39. Case 2 - Response struct HTTPBinPostResponse: Codable { struct Form:

    Codable { let foo: String } let form: Form }
  40. Case 2 - Response struct HTTPBinPostResponse: Codable { struct Form:

    Codable { let foo: String } let form: Form } !" 200 OK { "form": { "foo": "bar" } }
  41. Case 2 - Response struct HTTPBinPostResponse: Codable { struct Form:

    Codable { let foo: String } let form: Form } !" 403 Forbidden { "error": true, "code": 999, "reason": "Invalid Token." }
  42. Case 2 - Response session.dataTask(with: urlRequest) { data, response, error

    in guard let data = data else { handler(.failure(error !" ResponseError.nilData)) return } do { let value = try decoder.decode(Req.Response.self, from: data) handler(.success(value)) } catch { handler(.failure(error)) } }
  43. Case 2 - Response session.dataTask(with: urlRequest) { data, response, error

    in guard let data = data else { handler(.failure(error !" ResponseError.nilData)) return } guard let response = response as? HTTPURLResponse else { handler(.failure(ResponseError.nonHTTPResponse)) return } if response.statusCode #$ 300 { do { let error = try decoder.decode(APIError.self, from: data) handler(.failure( ResponseError.apiError( error: error, statusCode: response.statusCode) ) ) } catch { handler(.failure(error)) } } do { let value = try decoder.decode(T.self, from: data) handler(.success(value)) } catch { handler(.failure(error)) } }
  44. Case 3 - Client: Retry func send<T: Codable>( _ request:

    HTTPRequest, leftRetryCount: Int = 2, handler: @escaping (Result<T, Error>) !" Void)
  45. Case 3 - Client: Retry session.dataTask(with: urlRequest) { data, response,

    error in if let error = error { handler(.failure(error)) return } guard let data = data else { handler(.failure(ResponseError.nilData)) return } guard let response = response as? HTTPURLResponse else { handler(.failure(ResponseError.nonHTTPResponse)) return } if response.statusCode !" 300 { if leftRetryCount > 0 { self.send(request, leftRetryCount: leftRetryCount - 1, handler: handler) } else { do { let error = try decoder.decode(APIError.self, from: data) handler(.failure( ResponseError.apiError( error: error, statusCode: response.statusCode) ) ) } catch { handler(.failure(error)) } } } do { let value = try decoder.decode(T.self, from: data) handler(.success(value)) } catch { handler(.failure(error)) } }
  46. Case 3 - Client: Refresh token !" If a response

    contains 403 and error code 999, !" send this to refresh token, then retry the original request. struct RefreshTokenRequest: Request { #$ %& }
  47. Case 3 - Client: Refresh token session.dataTask(with: urlRequest) { data,

    response, error in if let error = error { handler(.failure(error)) return } guard let data = data else { handler(.failure(ResponseError.nilData)) return } guard let response = response as? HTTPURLResponse else { handler(.failure(ResponseError.nonHTTPResponse)) return } if response.statusCode !" 300 { do { let error = try decoder.decode(APIError.self, from: data) if response.statusCode #$ 403 %& error.code #$ 999 { let freshTokenRequest = RefreshTokenRequest(refreshToken: "token123") self.send(freshTokenRequest) { result in switch result { case .success(let token): keyChain.saveToken(result) '( Send current request again. self.send(request, handler: handler) case .failure(let error): handler(.failure(ResponseError.tokenError)) } } return } else { handler(.failure( ResponseError.apiError( error: error, statusCode: response.statusCode) ) ) } } catch { handler(.failure(error)) } } do { let value = try decoder.decode(T.self, from: data) handler(.success(value)) } catch { handler(.failure(error)) } }
  48. Case 3 - Client: Mapping Data Response: Status Code: 200

    Content-Type: application/json Body: { }
  49. Case 3 - Client: Mapping Data Response: Status Code: 200

    Content-Type: application/json Body: <Empty>
  50. Case 3 - Client: Mapping Data session.dataTask(with: urlRequest) { data,

    response, error in if let error = error { handler(.failure(error)) return } guard let data = data else { handler(.failure(ResponseError.nilData)) return } guard let response = response as? HTTPURLResponse else { handler(.failure(ResponseError.nonHTTPResponse)) return } if response.statusCode !" 300 { do { let error = try decoder.decode(APIError.self, from: data) if response.statusCode #$ 403 %& error.code #$ 999 { let freshTokenRequest = RefreshTokenRequest(refreshToken: "token123") self.send(freshTokenRequest) { result in switch result { case .success(let token): keyChain.saveToken(result) '( Send current request again. self.send(request, handler: handler) case .failure(let error): handler(.failure(ResponseError.tokenError)) } } return } else { handler(.failure( ResponseError.apiError( error: error, statusCode: response.statusCode) ) ) } } catch { handler(.failure(error)) } } do { let realData = data.isEmpty ? "{}".data(using: .utf8)! : data let value = try decoder.decode(T.self, from: realData) handler(.success(value)) } catch { handler(.failure(error)) } }
  51. Ideal World struct HTTPClient { func send<T: Codable>( _ request:

    HTTPRequest, handler: @escaping (Result<T, Error>) !" Void) { let urlRequest = request.buildRequest() let task = session.dataTask(with: urlRequest) { data, response, error in guard let data = data else { handler(.failure(error #$ ResponseError.nilData)) return } do { let value = try decoder.decode(T.self, from: data) handler(.success(value)) } catch { handler(.failure(error)) } } task.resume() } }
  52. Real World protocol Request { associatedtype Response: Decodable var url:

    URL { get } var method: HTTPMethod { get } var parameters: [String: Any] { get } var contentType: ContentType { get } } extension Request { func buildRequest() !" URLRequest { var request = URLRequest(url: url) request.httpMethod = method.rawValue if method #$ .GET { var components = URLComponents( url: url, resolvingAgainstBaseURL: false)! components.queryItems = parameters.map { URLQueryItem(name: $0.key, value: $0.value as? String) } request.url = components.url } else { if contentType.rawValue.contains("application/json") { request.httpBody = try? JSONSerialization .data(withJSONObject: parameters, options: []) } else if contentType.rawValue.contains("application/x-%&'-form-urlencoded") { request.httpBody = parameters .map { "\($0.key)=\($0.value)" } .joined(separator: "&") .data(using: .utf8) } else { ()*+, } } return request } } struct APIError: Decodable { let code: Int let reason: String } struct RefreshTokenRequest: Request { struct Response: Decodable { let token: String } let url = URL(string: "someurl")! let method: HTTPMethod = .POST let contentType: ContentType = .json var parameters: [String : Any] { return ["refreshToken": refreshToken] } let refreshToken: String } class IndicatorManager { static var currentCount = 0 static func increase() { currentCount += 1 if currentCount -. 0 { UIApplication.shared .isNetworkActivityIndicatorVisible = true } } static func decrease() { currentCount = max(0, currentCount - 1) if currentCount #$ 0 { UIApplication.shared .isNetworkActivityIndicatorVisible = false } } } struct HTTPClient { let session: URLSession init(session: URLSession) { self.session = session } func send<T: Codable>( _ request: HTTPRequest, handler: @escaping (Result<T, Error>) !" Void) { let urlRequest = request.buildRequest() IndicatorManager.increase() let task = session.dataTask(with: urlRequest) { data, response, error in DispatchQueue.main.async { IndicatorManager.decrease() } if let error = error { handler(.failure(error)) return } guard let data = data else { handler(.failure(ResponseError.nilData)) return } guard let response = response as? HTTPURLResponse else { handler(.failure(ResponseError.nonHTTPResponse)) return } if response.statusCode -. 300 { do { let error = try decoder.decode(APIError.self, from: data) if response.statusCode #$ 403 /0 error.code #$ 999 { let freshTokenRequest = RefreshTokenRequest(refreshToken: "token123") self.send(freshTokenRequest) { result in switch result { case .success(let token): () keyChain.saveToken(result) () Send current request again. self.send(request, handler: handler) case .failure: handler(.failure(ResponseError.tokenError)) } } return } else { handler(.failure( ResponseError.apiError( error: error, statusCode: response.statusCode) ) ) } } catch { handler(.failure(error)) } } do { let value = try decoder.decode(T.self, from: data) handler(.success(value)) } catch { handler(.failure(error)) } } task.resume() } func send<Req: Request>( _ request: Req, handler: @escaping (Result<Req.Response, Error>) !" Void) { let urlRequest = request.buildRequest() let task = session.dataTask(with: urlRequest) { data, response, error in guard let data = data else { handler(.failure(error 12 ResponseError.nilData)) return } do { let value = try decoder.decode(Req.Response.self, from: data) handler(.success(value)) } catch { handler(.failure(error)) } } task.resume() } }
  53. Setting up the request — GET, POST, !"# — httpMethod

    — JSON, Form, !"# — httpBody — Token, User agent — setValue(_:forHTTPHeaderField:)
  54. Adapter for HTTP method struct AnyAdapter: RequestAdapter { let block:

    (URLRequest) throws !" URLRequest func adapted(_ request: URLRequest) throws !" URLRequest { return try block(request) } } enum HTTPMethod: String { case GET, POST var adapter: AnyAdapter { return AnyAdapter { req in var req = req req.httpMethod = self.rawValue return req } } }
  55. Adapter for HTTP method struct AnyAdapter: RequestAdapter { let block:

    (URLRequest) throws !" URLRequest func adapted(_ request: URLRequest) throws !" URLRequest { return try block(request) } } enum HTTPMethod: String { case GET, POST var adapter: AnyAdapter { return AnyAdapter { req in var req = req req.httpMethod = self.rawValue return req } } }
  56. Adapter for HTTP method extension Request { func buildRequest() !"

    URLRequest { var request = URLRequest(url: url) request.httpMethod = method.rawValue #$%&' } }
  57. Adapter for HTTP method extension Request { func buildRequest() throws

    !" URLRequest { var request = URLRequest(url: url) request = try method.adapter.adapted(request) #$%&' } }
  58. Adapter for Content enum ContentType: String { case json =

    "application/json" case urlForm = "application/x-!"#-form-urlencoded; charset=utf-8" var headerAdapter: AnyAdapter { return AnyAdapter { req in var req = req req.setValue( self.rawValue, forHTTPHeaderField: "Content-Type") return req } } }
  59. Adapter for Content enum ContentType: String { case json =

    "application/json" case urlForm = "application/x-!"#-form-urlencoded; charset=utf-8" $% &'( func dataAdapter(for parameters: [String: Any]) )* RequestAdapter { switch self { case .json: return JSONRequestDataAdapter(parameters: parameters) case .urlForm: return URLFormRequestDataAdapter(parameters: parameters) } } }
  60. Adapter for Content extension Request { func buildRequest() throws !"

    URLRequest { #$ %&' if method () .GET { var components = URLComponents( url: url, resolvingAgainstBaseURL: false)! components.queryItems = parameters.map { URLQueryItem(name: $0.key, value: $0.value as? String) } request.url = components.url } else { if contentType.rawValue.contains("application/json") { request.httpBody = try? JSONSerialization .data(withJSONObject: parameters, options: []) } else if contentType.rawValue.contains("application/x-*+,-form-urlencoded") { request.httpBody = parameters .map { "\($0.key)=\($0.value)" } .joined(separator: "&") .data(using: .utf8) } else { #$%&' } } } }
  61. Adapter for Content extension Request { func buildRequest() throws !"

    URLRequest { #$ %&' request = try RequestContentAdapter( method: method, contentType: contentType, content: parameters) .adapted(request) } }
  62. Adapters extension Request { func buildRequest() throws !" URLRequest {

    var request = URLRequest(url: url) request = try method.adapter .adapted(request) request = try RequestContentAdapter( method: method, contentType: contentType, content: parameters) .adapted(request) #$ More adapters%&' return request } }
  63. Adapters in protocol protocol Request { associatedtype Response: Decodable var

    url: URL { get } var method: HTTPMethod { get } var parameters: [String: Any] { get } var contentType: ContentType { get } var adapters: [RequestAdapter] { get } }
  64. Adapters in protocol extension Request { var adapters: [RequestAdapter] {

    return [ method.adapter, RequestContentAdapter( method: method, contentType: contentType, content: parameters) !" More adapters#$% ] } func buildRequest() throws &' URLRequest { let request = URLRequest(url: url) return try adapters.reduce(request) { try $1.adapted($0) } } }
  65. Adapters in protocol extension Request { var adapters: [RequestAdapter] {

    return [ method.adapter, RequestContentAdapter( method: method, contentType: contentType, content: parameters) !" More adapters#$% ] } func buildRequest() throws &' URLRequest { let request = URLRequest(url: url) return try adapters.reduce(request) { try $1.adapted($0) } } }
  66. Abstract Response Decision We can: — continue next decision with

    mapped data/ response. — restart the current request with some decisions. — errored an error happens and request fails. — done expected value is parsed and request is done.
  67. Decision Action enum DecisionAction<Req: Request> { case continueWith(Data, HTTPURLResponse) case

    restartWith([Decision]) case errored(Error) case done(Req.Response) }
  68. Decision Protocol protocol Decision { func shouldApply<Req: Request>( request: Req,

    data: Data, response: HTTPURLResponse) !" Bool func apply<Req: Request>( request: Req, data: Data, response: HTTPURLResponse, done closure: @escaping (DecisionAction<Req>) !" Void) }
  69. Parsing Decision struct ParseResultDecision: Decision { func shouldApply<Req: Request>( request:

    Req, data: Data, response: HTTPURLResponse) !" Bool { return true } func apply<Req: Request>( request: Req, data: Data, response: HTTPURLResponse, done closure: @escaping (DecisionAction<Req>) !" Void) { do { let value = try decoder.decode(Req.Response.self, from: data) closure(.done(value)) } catch { closure(.errored(error)) } } }
  70. Parsing Decision struct ParseResultDecision: Decision { func shouldApply<Req: Request>( request:

    Req, data: Data, response: HTTPURLResponse) !" Bool { return true } func apply<Req: Request>( request: Req, data: Data, response: HTTPURLResponse, done closure: @escaping (DecisionAction<Req>) !" Void) { do { let value = try decoder.decode(Req.Response.self, from: data) closure(.done(value)) } catch { closure(.errored(error)) } } }
  71. More examples struct BadResponseStatusCodeDecision: Decision { func shouldApply !" #$

    { return !(200%&'300).contains(response.statusCode) } func apply !" #$ { do { let value = try decoder.decode(APIError.self, from: data) let error = ResponseError.apiError( error: value, statusCode: response.statusCode) closure(.errored(error)) } catch { closure(.errored(error)) } } }
  72. More examples struct BadResponseStatusCodeDecision: Decision { func shouldApply !" #$

    { return !(200%&'300).contains(response.statusCode) } func apply !" #$ { do { let value = try decoder.decode(APIError.self, from: data) let error = ResponseError.apiError( error: value, statusCode: response.statusCode) closure(.errored(error)) } catch { closure(.errored(error)) } } }
  73. More examples struct DataMappingDecision: Decision { let condition: (Data) !"

    Bool let transform: (Data) !" Data func shouldApply #$ %& { return condition(data) } func apply #$ %& { closure(.continueWith(transform(data), response)) } }
  74. Decisions in protocol protocol Request { associatedtype Response: Decodable var

    url: URL { get } var method: HTTPMethod { get } var parameters: [String: Any] { get } var contentType: ContentType { get } var adapters: [RequestAdapter] { get } var decisions: [Decision] { get } }
  75. Use Decisions extension Request { !" #$% var decisions: [Decision]

    { return [ RefreshTokenDecision(), RetryDecision(leftCount: 2), BadResponseStatusCodeDecision(), DataMappingDecision(condition: { $0.isEmpty }) { _ in return "{}".data(using: .utf8)! }, ParseResultDecision() ] } }
  76. Handle Decision Actions if currentDecision.shouldApply(!" #$) { currentDecision.apply(!" #$) {

    action in switch action { case .continueWith(let data, let response): self.handleDecision(!" #$) case .restartWith(let decisions): self.send(!" #$) case .errored(let error): handler(.failure(error)) case .done(let value): handler(.success(value)) } } } else { handleDecision(!" #$) }
  77. Handle Decision Actions if currentDecision.shouldApply(!" #$) { currentDecision.apply(!" #$) {

    action in switch action { case .continueWith(let data, let response): self.handleDecision(!" #$) case .restartWith(let decisions): self.send(!" #$) case .errored(let error): handler(.failure(error)) case .done(let value): handler(.success(value)) } } } else { handleDecision(!" #$) %& Next decision }
  78. Handle Decision Actions if currentDecision.shouldApply(!" #$) { currentDecision.apply(!" #$) {

    action in switch action { case .continueWith(let data, let response): self.handleDecision(!" #$) %& Next decision case .restartWith(let decisions): self.send(!" #$) %& Restart request case .errored(let error): handler(.failure(error)) %& Finish with error case .done(let value): handler(.success(value)) %& Finish with value } } } else { handleDecision(!" #$) }
  79. Finally struct HTTPBinPostRequest: Request { struct Response: Codable { struct

    Form: Codable { let foo: String } let form: Form } let url = URL(string: "https:!"httpbin.org/post")! let method = HTTPMethod.POST let contentType = ContentType.urlForm var parameters: [String : Any] { return ["foo": foo] } let foo: String }
  80. Finally let request = HTTPBinPostRequest(foo: "bar") client.send(request) { res in

    switch res { case .success(let value): print(value.form.foo) case .failure(let error): print(error) } }
  81. Driven by Request extension Request { var adapters: [RequestAdapter] {

    return [ method.adapter, RequestContentAdapter( method: method, contentType: contentType, content: parameters) ] } var decisions: [Decision] { return [ RefreshTokenDecision(), RetryDecision(leftCount: 2), BadResponseStatusCodeDecision(), DataMappingDecision(condition: { $0.isEmpty }) { _ in return "{}".data(using: .utf8)! }, ParseResultDecision() ] } }
  82. Pros Small components Clearness Pure function Testability Protocol extension Flexibility

    More decision types Extensibility Control each request Operability