API 指的是應用程式之間溝通的介面,而本篇主題其實是前端呼叫後端 Web API 的設計,前端使用 Web API 可以拿到遠端的資料,也可依照不同用戶提供他們不同的內容,因此如何設計前端 Web API 層的邏輯變得很重要,需要兼具共用性和擴充性。
本篇所提及的 Web API 設計版本,其實是用來 紀錄自己曾經使用過的設計方式 ,未來也可能會再增加,所以內容有大量自身使用過的感想,不代表絕對的好壞,依照專案需求或架構會有更適合的設計方式 。
Web API 設計的邏輯 在實作 Web API 層的邏輯前,可以把它分成三段流程
🔵 Part1 組合 Request 流程
如何組成 Request ?
發送前需要設定好各項參數,可能包含有下列項目
URL : 請求的網址,包含 scheme、host、 port、path、query … 等等
ex. https://www.ios.com/api/user?id=9999&type=1
HTTP method : 請求的方法,通常能反應出此 API 的目的
ex. get、post、put、patch、delete ….
HTTP header : 請求的附加資料,有很多種已經定好的規範、也可以自訂資料
ex. Content-Type: application/x-www-form-urlencoded
ex. Content-Type: application/json
ex. Authorization: Bearer …. => 身份驗證用
ex. Accept-Language: zh-TW => 指定語言用
ex. Tracing: …. => 客製化追蹤
Body: 依照不同的 Content-Type 會塞不同的 Body
ex. “action=getmemberdata&id=111”.data(using: .utf8)
ex. “{"action": "getlatestposts","fetchcount": 20 }”.data(using: .utf8)
🔴 Part2 發送 API 流程
使用什麼發送 API ?
通常使用 URLSession 或是使用 Alamofire
選擇發送 Task 類型,一般 API 快速交握請求會使用 dataTask,此外 uploadTask、downloadTask 是用於長時間上傳或下載且可在背景執行。
如何設定網路 Config ?
如何接收回應 ?
一種是使用 callback closure 的方式觸發
另一種就是 Swift 5.5 URLSession 開始支援 async await 的方式,寫起來更直觀更方便
記得要注意 API 回來的 thread 是在哪裡 ?
URLSession 的 dataTask callback 預設是 background thread
URLSession 的 async await 是回發送前的 thread
Alamofire callback 預設是 main thread
🟢 Part3 處理 Response 流程
收到回應該如何處理 ?
URLSession 回應通常會收到三個物件 Data、URLResponse、Error 皆為 optional
dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void)
通常先處理 Error ,如果有值就是網路層就遇到問題
其次可以判斷 URLResponse 內的 statusCode ,如果符合規範的 API 設計可以從 HTTP statusCode 判斷狀態
(response as? HTTPURLResponse)?.statusCode
2XX 成功回應
3XX 重新轉向的回應
4XX 用戶端的錯誤 => 400 無效請求、401 授權失敗 …
5XX 伺服器端錯誤 => 500 伺服器端內部未知錯誤 …
最後要將 Data 轉換成預期的物件,有可能是後台定義好的回應物件或是錯誤物件
如果發生錯誤,是否需要重試 ?
如果發生用戶沒有網路,如何通知用戶 ?
如果發生授權失敗或過期,是否需要自動重刷 token 或是通知畫面 ?
上面三個部分也可能會加入 Debug Log 或是事件追蹤,當頁面的不同、用戶操作的不同、站台的不同也會進行不同的設定。 下面來試試看進行 Web API 的邏輯封裝和重複利用,每個版本都會示範 Get、Post+UrlEncoded、Post+Json,如果需要其他 method 可以自行新增。
[Version1]:使用物件和方法封裝
特性
使用單一或多個 Manager 封裝呼叫 API 的方法和處理方式,也是實作難度最低的
至少能確保呼叫 API 邏輯或三方依賴都可以鎖在 Manager 內部
每一道方法都是呼叫一道 API 自行實作 Part1、Part2、Part3,確保職責的單一性,理論上互相不干擾
ex. requestHttpBinGet
、requestHttpBinPostUrlEncoded
、requestHttpBinPostJson
封裝共用的邏輯,像是解析回應物件、組裝 Header 等等
ex. handleResponse
、addHeaderContentTypeJson
、addHeaderContentTypeURLEncoded
實作範例 APIManager APIManager+HttpBinGet APIManager+HttpBinPostUrlEncoded APIManager+HttpBinPostJson URLRequest+AddHeader APIManager.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 class APIManager { static let shared = APIManager () private init () {} func handleResponse <T : Decodable >(data : Data ?, response : URLResponse ?, error : Error ?, completion : @escaping ((Result <T ,APIManagerError >) -> Void )) { if let error = error{ completion(.failure(.requestError(error: error))) return } if let statusCode = (response as? HTTPURLResponse )? .statusCode{ print (statusCode) } guard let data = data else { completion(.failure(.nilData)) return } guard let response = try? JSONDecoder ().decode(T .self , from: data) else { completion(.failure(.decodeError)) return } completion(.success(response)) } } extension APIManager { enum APIManagerError : Error { case requestError(error: Error ) case nilData case decodeError } }
APIManager+HttpBinGet.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 extension APIManager { struct HttpBinGetResponse : Codable { struct Args : Codable { let value: String } let args: Args let origin: String let url: String } func requestHttpBinGet (value : String , completion : @escaping ((Result <HttpBinGetResponse ,APIManagerError >) -> Void )) { guard let url = URL (string: "https://httpbin.org/get?value=\(value) " ) else { return } var request = URLRequest (url: url) request.method = .get request.addHeaderAuthToken() let dataTask = URLSession .shared.dataTask(with: request) { data, response, error in self .handleResponse(data: data, response: response, error: error, completion: completion) } dataTask.resume() }
APIManager+HttpBinPostUrlEncoded.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 extension APIManager { struct HttpBinPostUrlEncodedResponse : Codable { struct Form : Codable { let value: String } let form: Form let origin: String let url: String } func requestHttpBinPostUrlEncoded (value : String , completion : @escaping ((Result <HttpBinPostUrlEncodedResponse ,APIManagerError >) -> Void )) { guard let url = URL (string: "https://httpbin.org/post" ) else { return } var request = URLRequest (url: url) request.method = .post request.addHeaderAuthToken() request.addHeaderContentTypeURLEncoded() request.httpBody = "value=\(value) " .data(using: .utf8) let dataTask = URLSession .shared.dataTask(with: request) { data, response, error in self .handleResponse(data: data, response: response, error: error, completion: completion) } dataTask.resume() } }
APIManager+HttpBinPostJson.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 extension APIManager { struct HttpBinPostJsonResponse : Codable { struct JsonData : Codable { let value: String } let json: JsonData let origin: String let url: String } func requestHttpBinPostJson (value : String , completion : @escaping ((Result <HttpBinPostJsonResponse ,APIManagerError >) -> Void )) { guard let url = URL (string: "https://httpbin.org/post" ) else { return } var request = URLRequest (url: url) request.method = .post request.addHeaderAuthToken() request.addHeaderContentTypeJson() request.httpBody = """ { "value": "\(value) " } """ .data(using: .utf8) let dataTask = URLSession .shared.dataTask(with: request) { data, response, error in self .handleResponse(data: data, response: response, error: error, completion: completion) } dataTask.resume() } }
URLRequest+AddHeader.swift 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 extension URLRequest { @discardableResult mutating func addHeaderAuthToken () -> URLRequest { self .addValue("Bearer ......" , forHTTPHeaderField: "Authorization" ) return self } @discardableResult mutating func addHeaderContentTypeJson () -> URLRequest { self .addValue("application/json" , forHTTPHeaderField: "Content-Type" ) return self } @discardableResult mutating func addHeaderContentTypeURLEncoded () -> URLRequest { self .addValue("application/x-www-form-urlencoded" , forHTTPHeaderField: "Content-Type" ) return self } }
使用方式 1 2 3 4 5 6 7 8 9 APIManager .shared.requestHttpBinGet(value: "xxxx1" ) { result in print (result) } APIManager .shared.requestHttpBinPostUrlEncoded(value: "xxxx2" ) { result in print (result) } APIManager .shared.requestHttpBinPostJson(value: "xxxx3" ) { result in print (result) }
Demo
🤔【 問題 】 實作每道 API 時都需要自行呼叫 Part1、Part2、Part3 不夠方便,很多時候都是相同流程卻要重複寫,希望有預設的流程,只需要定義請求的欄位和回應的物件就好。
[Version2]:使用繼承和反射欄位
特性
使用 NetworkManager 封裝各種類型的請求方式
使用父類別 ( BaseRequest ) 和子類別繼承的特性
定義請求通用的欄位 ex. Domain, Path, Method …. 等等,子類別可以自行修改
定義 send 方法實作 Part1、Part2、Part3,並呼叫 NetworkManager,子類別可以直接呼叫
使用”反射欄位”來組合參數
ex. get query, post body …. 等等
實作範例 NetworkManager BaseRequest HttpBinBase HttpBinGet HttpBinPostUrlEncoded HttpBinPostJson 完整版 NetworkManager.swift
NetworkManager.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 class NetworkManager { static let shared = NetworkManager () private init () {} private func convertParameters (parameters : [String : Any ]) -> [String : String ] { var temp: [String : String ] = [:] for one in parameters{ switch one.value { case is String , is Int , is Double , is CustomStringConvertible : temp[one.key] = "\(one.value) " default : break } } return temp } private func handleResponse <T : Decodable >(data : Data ?, response : URLResponse ?, error : Error ?, completion : @escaping ((Result <T ,NetworkManagerError >) -> Void )) { } } extension NetworkManager { enum NetworkManagerError : Error { case requestError(error: Error ) case nilData case decodeError } } extension NetworkManager { func sendGetRequest <T : Decodable >(... ) -> URLSessionDataTask ?{ } } extension NetworkManager { func sendPostUrlEncodedRequest <T : Decodable >(... ) -> URLSessionDataTask ?{ } func sendPostJSONRequest <T : Decodable >(... ) -> Void )) -> URLSessionDataTask ?{ } }
BaseRequest.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 class BaseRequest { var domain: String { return "" } var path: String { return "" } var method: HttpMethod { return .get } var contentType: ContentType { return .none } var timeoutInterval: TimeInterval { return 60 } @discardableResult func send <T : Decodable >(completion : @escaping ((Result <T ,NetworkManager .NetworkManagerError >) -> Void )) -> URLSessionDataTask ? { let urlString = domain + path switch method { case .get: return NetworkManager .shared.sendGetRequest(urlString: urlString, queryItems: getParameters(), timeoutInterval: timeoutInterval, completion: completion) case .post where contentType == .urlencoded: return NetworkManager .shared.sendPostUrlEncodedRequest(urlString: urlString, urlEncodedParas: getParameters(), timeoutInterval: timeoutInterval, completion: completion) case .post where contentType == .json: return NetworkManager .shared.sendPostJSONRequest(urlString: urlString, parameters: getParameters(), timeoutInterval: timeoutInterval, completion: completion) default : return nil } } private func getParameters () -> [String : Any ] { return getParameters(mirror: Mirror (reflecting: self )) } private func getParameters (mirror : Mirror ) -> [String : Any ] { var parameters: [String : Any ] = [:] if mirror.superclassMirror != nil { parameters = parameters.merging(getParameters(mirror: mirror.superclassMirror! )){ $1 } } for attr in mirror.children { if let propertyName = attr.label { parameters[propertyName] = attr.value } } return parameters } } extension BaseRequest { enum ContentType { case none case urlencoded case json } enum HttpMethod { case get case post } }
HttpBinBaseRequest.swift 1 2 3 4 5 6 7 class HttpBinBaseRequest : BaseRequest { override var domain: String { return "https://httpbin.org/" } }
HttpBinGetRequest+Response.swift 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class HttpBinGetRequest : HttpBinBaseRequest { override var path: String { return "get" } var value: String = "abc" func setData (value : String ) -> Self { self .value = value return self } } struct HttpBinGetResponse : Codable { struct Args : Codable { let value: String } let args: Args let origin: String let url: String }
HttpBinPostUrlEncodedRequest+Response.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 class HttpBinPostUrlEncodedRequest : HttpBinBaseRequest { override var path: String { return "post" } override var method: HttpMethod { return .post } override var contentType: ContentType { return .urlencoded } var value: String = "我是資料" func setData (value : String ) -> Self { self .value = value return self } } struct HttpBinPostJsonResponse : Codable { struct JsonData : Codable { let value: String } let json: JsonData let origin: String let url: String }
HttpBinPostJsonRequest+Response.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 class HttpBinPostUrlEncodedRequest : HttpBinBaseRequest { override var path: String { return "post" } override var method: HttpMethod { return .post } override var contentType: ContentType { return .urlencoded } var value: String = "我是資料" func setData (value : String ) -> Self { self .value = value return self } } struct HttpBinPostJsonResponse : Codable { struct JsonData : Codable { let value: String } let json: JsonData let origin: String let url: String }
使用方式 1 2 3 4 5 6 7 8 9 10 11 12 HttpBinGetRequest ().setData(value: "wwww1" ).send { (result: Result <HttpBinGetResponse ,NetworkManager .NetworkManagerError >) in print (result) } HttpBinPostUrlEncodedRequest ().setData(value: "wwww2" ).send { (result: Result <HttpBinPostUrlEncodedResponse ,NetworkManager .NetworkManagerError >) in print (result) } HttpBinPostJsonRequest ().setData(value: "wwww3" ).send { (result: Result <HttpBinPostJsonResponse ,NetworkManager .NetworkManagerError >) in print (result) }
Demo
🤔【 問題 】 雖然建立 API 物件變得非常快速看似美好,但面對各種不同站台的需求,還有當初面對多達 40 幾隻 App 使用這套系統,NetworkManager 因應不同參數或預設值不斷地擴大請求方法,有時還需要不同設定值的 URLSession,BaseRequest 也開始新增不同子類別和子流程(有無身份驗證/有無特定Header/有無Log/不同處理回應的方式…),有些特殊的 API 還會需要同時塞 query 加上 body,甚至需要新增各種 Bool 開關或是 Delegate 的通知接口。 第一次體驗到如此深刻的”繼承税”…😭😭😭
繼承税這個詞是我從「 The Pragmatic Programmer 20週年紀念版 」看到的,書本引用一句話 “您只想要一根香蕉,但是您得到的卻是一隻拿著香蕉的大猩猩和整個叢林 - Joe Armstrong” 。
繼承就是一種耦合,父類別和子類別之間耦合、子類別新方法呼叫到父類別也是耦合,時間一久往往會迭代出多個類別的依賴關係,像是一個網狀一樣,修改需要小心影響上下功能;如果想要避免就是不要使用繼承,改用下列方式,詳情可以參考該書。
interface/protocol
delegation
mixin/trait
[Version3]:使用部件化設計 此版本是從上面提到 onevcat 大神分享的作法 fork 做修改,並且因為我遇到的需求做調整,所以不會是最完美最適合你的,但是部件化的概念很適合給大家來進行調整。
特性
Part1 組合 Request:
使用 Adapter 組裝一個一個的部件,可以是新增一個 Header、也可以是新增 Token
每經過一個部件會回傳修改後的 URLRequest,會把所有 Adapter 都執行過一遍後回傳最終的 URLRequest
Part2 發送 API:
使用 protocol ApiComponentSendable 先預設定義一種發送流程,並且可抽換發送主體(Native/Alamofire/Mock都可)
Part3 處理 Response:
使用 Decision 一個一個決策決定該做什麼,例如接續決策、重頭開始、發生錯誤、結束決策
如果是”接續決策”就往下一個決策判斷
如果到最後還是沒有”結束決策”或是”發生錯誤”,就代表決策設定有問題
在定義 Request 中使用 RequestBaseSetting 方便抽換基底設定(正式或測試機)
在處理 Response 中使用 DecisionRecord 可以在 Console 清楚顯示決策流程
之前接過特殊站台當回應成功時,statusCode = 200 但 Data 為空的,所以範例有個 NilDataDecision、NilDataResponse 可使用
實作範例 下面只列出重要的物件,完整範例可以參考下面 Demo 網址!
Request ApiComponentSendable Adapter Decision GetRequest HttpBinService HttpBinService+HttpBinGet Request 定義 Api 的 基礎設定(domain)、HTTPMethod、轉接器、決策路徑。
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 protocol RequestBaseSetting { var domain: String { get } } protocol Request { associatedtype Response : Decodable var setting: RequestBaseSetting { get set } var method: HTTPMethod { get } var adapters: [RequestAdapter ] { get } var decisions: [Decision ] { get } } extension Request { func buildRequest () throws -> URLRequest { guard let url = URL (string: setting.domain) else { throw ApiComponentError .requestAdapterFailure(error: nil ) } let request = URLRequest (url: url) return try adapters.reduce(request) { try $1 .adapted($0 ) } } }
ApiComponentSendable 定義發送流程,包含 Part1 + Part2 + Part3,每個部分都是被組裝而成。完整版 ApiComponentSendable.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 protocol ApiComponentSendable { func sendRequest (request : URLRequest , queue : DispatchQueue , handler : @escaping (Data ?, URLResponse ?, Error ?) -> Void ) } extension ApiComponentSendable { func send <Req : Request >( request : Req , queue : DispatchQueue , handler : @escaping (Swift .Result <Req .Response , Error >) -> Void ) { send(request: request, decisions: request.decisions, queue: queue, handler: handler) } func send <Req : Request >( request : Req , decisions : [Decision ], queue : DispatchQueue , handler : @escaping (Swift .Result <Req .Response , Error >) -> Void ) { request.buildRequest sendRequest(request: urlRequest, queue: queue){ (data, response, error) in self .handleDecision( request: request, queue: queue, data: data, response: httpResponse, nowDecisions: decisions, decisionRecords: [], handler: handler ) } } func handleDecision <Req : Request >( request : Req , queue : DispatchQueue , data : Data ?, response : HTTPURLResponse , nowDecisions : [Decision ], decisionRecords : [DecisionRecord <Req >], handler : @escaping (Swift .Result <Req .Response , Error >) -> Void ) { var nowDecisions = nowDecisions let current = nowDecisions.removeFirst() current.shouldApply ... current.apply(request: request, data: data, response: response) { action in handleDecision send(... )發送Request (使用特定決策) handler(.failure(error)) handler(.success(response)) } } } }
Adapter 就是用來組裝 Request 的各種部件,可以組合 Header 或是 body 都可以。
Adapter 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 protocol RequestAdapter { func adapted (_ request : URLRequest ) throws -> URLRequest } struct HTTPMethodAdapter : RequestAdapter { let method: HTTPMethod func adapted (_ request : URLRequest ) throws -> URLRequest { var request = request request.httpMethod = method.rawValue return request } } struct ContentTypeAdapter : RequestAdapter { let contentType: ContentType func adapted (_ request : URLRequest ) throws -> URLRequest { var request = request request.setValue(contentType.rawValue, forHTTPHeaderField: "Content-Type" ) return request } }
Decision 有兩種方法 shouldApply 和 apply,當 shouldApply 為 true 的時候才會執行 apply。 apply 記得要執行任一種 DecisionAction 才會順利執行。
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 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 ) } enum DecisionAction <Req : Request > { case continueWith(Data ?, HTTPURLResponse ) case restartWith([Decision ]) case errored(Error ) case done(Req .Response ) } struct BadStatusCodeDecision : Decision { func shouldApply <Req : Request >(request : Req , data : Data ?, response : HTTPURLResponse ) -> Bool { return ! (200 ..< 300 ).contains(response.statusCode) } func apply <Req : Request >( request : Req , data : Data ?, response : HTTPURLResponse , done closure : @escaping (DecisionAction <Req >) -> Void ) { closure(.errored(ApiComponentError .badStatusCode(data: data, statusCode: response.statusCode))) } } 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 ) { guard let data = data else { closure(.errored(ApiComponentError .nilData)) return } do { let value = try JSONDecoder ().decode(Req .Response .self , from: data) closure(.done(value)) } catch { closure(.errored(ApiComponentError .decodeFailure(data: data, error: error))) } } }
GetRequest conform Request,並且實作預設的 method、adapters、decisions。
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 protocol GetRequest : Request { var path: String { get } var queryParams: [String : String ]? { get } } extension GetRequest { var method: HTTPMethod { return .get } var adapters: [RequestAdapter ] { return [ PathAdapter (path: path), HTTPMethodAdapter (method: method), QueryParamsAdapter (queryParams: queryParams) ] } var decisions: [Decision ] { return [ BadStatusCodeDecision (), ParseResultDecision () ] } }
HttpBinService 用來放該站台的 Request、Response、預設的設定值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 struct HttpBinService { static var defaultSetting: HttpBinServiceSetting = .init (status: .production) } struct HttpBinServiceSetting : RequestBaseSetting { enum Status : String { case debug = "https://httpbin.debug.org" case production = "https://httpbin.org" } var status: Status var domain: String { return status.rawValue } }
HttpBinGetRequest conform GetRequest,只要定義所需欄位就行!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 extension HttpBinService { struct HttpBinGetRequest : GetRequest { typealias Response = HttpBinGetResponse var setting: RequestBaseSetting = defaultSetting var path: String { return "/get" } var queryParams: [String : String ]? { return ["foo" : foo] } let foo: String } struct HttpBinGetResponse : Codable { struct Args : Codable { let foo: String } let args: Args } }
使用方式 基本用法 Mock測試 切換正式或測試環境 完全客製化Request 1 2 3 4 let request1 = HttpBinService .HttpBinGetRequest (foo: "123" )ApiNativeClient (session: .shared).send(request: request1, queue: .main) { result in print (result) }
1 2 3 4 🌐 [HttpBinGetRequest][GET]: https://httpbin.org/get?foo=123 ⏰ [HttpBinGetRequest][ReceiveTime]: 0.416s - https://httpbin.org/get?foo=123 📦 [HttpBinGetRequest][StatusCode = 200][ReceiveData]: ... 🚥 [HttpBinGetRequest][Decisions]: {BadStatusCode❕} -> {ParseResult❗️}[Success!!!🙆🙆🙆]
1 2 3 4 5 6 7 8 9 var request = HttpBinService .HttpBinGetRequest (foo: "0000" )let dataString = """ {"args":{"foo":"0000"}} """ let apiMockClient = ApiMockClient (data: dataString.data(using: .utf8), statusCode: 200 )apiMockClient.send(request: request, queue: .main) { result in print (result) }
1 2 3 4 5 6 7 8 9 10 11 12 HttpBinService .defaultSetting.status = .debuglet request = HttpBinService .HttpBinGetRequest (foo: "123" )ApiNativeClient (session: .shared).send(request: request, queue: .main) { result in print (result) } var request = HttpBinService .HttpBinGetRequest (foo: "123" )request.setting = HttpBinServiceSetting .init (status: .debug) ApiNativeClient (session: .shared).send(request: request, queue: .main) { result in print (result) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 extension HttpBinService { struct HttpBinCustomRequest : Request { typealias Response = HttpBinCustomResponse var setting: RequestBaseSetting = defaultSetting var method: HTTPMethod { return .get } var adapters: [RequestAdapter ] { return [ ] } var decisions: [Decision ] { return [ ] } } struct HttpBinCustomResponse : Codable { } }
Demo