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 ?
    • 怎麼處理網路層的邏輯封包、網路交握都被底層封裝了,只能設定一些通用屬性、Cookie、Cache 等等,可參考 URLSessionConfiguration
    • 值得注意的是 URLSession 對相同 host 並發 Request 是有上限的,如果有特殊需求需要調整,可參考 httpMaximumConnectionsPerHost
    • 取得 task 進度,需要實作 URLSessionTaskDelegate
    • 取得狀態 task.state
    • 強制取消 task.cancel (polling/long-polling可能會用到)
  • 如何接收回應 ?
    • 一種是使用 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. requestHttpBinGetrequestHttpBinPostUrlEncodedrequestHttpBinPostJson
  • 封裝共用的邏輯,像是解析回應物件、組裝 Header 等等
    • ex. handleResponseaddHeaderContentTypeJsonaddHeaderContentTypeURLEncoded

實作範例

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
// API 管理者
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))
}
}

// MARK: - 錯誤物件
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)){

// TODO: Part1 組合 Request,使用方法或擴充封裝類似的邏輯
guard let url = URL(string: "https://httpbin.org/get?value=\(value)") else { return }
var request = URLRequest(url: url)
request.method = .get
request.addHeaderAuthToken()

// TODO: Part2 發動 API,如果有需求也可抽換
let dataTask = URLSession.shared.dataTask(with: request) {
data, response, error in

// TODO: Part3 處理 Response,使用方法或擴充封裝類似的邏輯
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)){

// TODO: Part1 組合 Request,使用方法或擴充封裝類似的邏輯
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)

// TODO: Part2 發動 API,如果有需求也可抽換
let dataTask = URLSession.shared.dataTask(with: request) {
data, response, error in

// TODO: Part3 處理 Response,使用方法或擴充封裝類似的邏輯
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)){

// TODO: Part1 組合 Request,使用方法或擴充封裝類似的邏輯
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)

// TODO: Part2 發動 API,如果有需求也可抽換
let dataTask = URLSession.shared.dataTask(with: request) {
data, response, error in

// TODO: Part3 處理 Response,使用方法或擴充封裝類似的邏輯
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.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
/// Network 管理者
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)){
// ...
}
}

// MARK: - 錯誤物件
extension NetworkManager{

/// 錯誤物件
enum NetworkManagerError: Error{
case requestError(error: Error)
case nilData
case decodeError
}
}

// MARK: - Get 相關請求
extension NetworkManager{

/// 發送 Get 請求
func sendGetRequest<T: Decodable>(...) -> URLSessionDataTask?{
// ...
}
}

// MARK: - Post 相關請求
extension NetworkManager{

/// 發送 Post UrlEncoded 請求
func sendPostUrlEncodedRequest<T: Decodable>(...) -> URLSessionDataTask?{
// ...
}

/// 發送 Post JSON 請求
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{

/// Domain
var domain: String {
return ""
}

/// Path
var path: String {
return ""
}

/// 請求方法
var method: HttpMethod {
return .get
}

/// 內容格式
var contentType: ContentType {
return .none
}

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

/// 取得參數(使用Mirror所有父類別的參數,並轉換為Dictionary)
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{

/// Domain
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

幸運的是正好看到 onevcat 大神在 iPlayground 2019 分享了 部件化網路程式設計 / hackmd共筆 / ComponentNetworking Demo,基本上可以達到每一道 API 抽換各種流程並且共用邏輯,甚至也可以進行單元測試或 Mock 資料;其中用部件並且”組合”出功能的概念頁也讓我想到 SwiftLee 這篇 Composition vs. Inheritance


[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 定義 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
/// Request基礎設定
protocol RequestBaseSetting {

/// Domain
var domain: String { get }
}

/// 基底Request
protocol Request {

/// 基底Response
associatedtype Response: Decodable

/// Request基礎設定
var setting: RequestBaseSetting { get set }

/// HTTPMethod
var method: HTTPMethod { get }

/// 請求轉接器(發送前處理)
var adapters: [RequestAdapter] { get }

/// 決策路徑(接收回應後處理)
var decisions: [Decision] { get }
}

/// 基底Request
extension Request {

/// 建立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 {

/// 發送Request(使用初始決策)
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)
}

/// 發送Request(使用特定決策)
func send<Req: Request>(
request: Req,
decisions: [Decision],
queue: DispatchQueue,
handler: @escaping (Swift.Result<Req.Response, Error>) -> Void)
{
// Todo: 建立Request
request.buildRequest

// Todo: 發送 Request
sendRequest(request: urlRequest, queue: queue){
(data, response, error) in

// Todo: - 預設處理 data, response, error

// Todo: - 處理決策
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)
{
// Todo: - 判斷是否應用決策
var nowDecisions = nowDecisions
let current = nowDecisions.removeFirst()
current.shouldApply ...

// Todo: - 執行決策
current.apply(request: request, data: data, response: response) { action in

// 判斷 action
// Todo: 1 繼續執行
handleDecision
// Todo: 2 重新開始
send(...)發送Request(使用特定決策)
// Todo: 3 發生錯誤
handler(.failure(error))
// Todo: 4 完成決策
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
}

/// HTTPMethod 的 轉接器
struct HTTPMethodAdapter: RequestAdapter {
let method: HTTPMethod
func adapted(_ request: URLRequest) throws -> URLRequest {
var request = request
request.httpMethod = method.rawValue
return request
}
}

/// ContentType 的 轉接器
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)
}

/// 決策動作
/// - continueWith: 接續決策
/// - restartWith: 重頭開始
/// - errored: 發生錯誤
/// - done: 結束決策
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
/// 基底Request - Get
protocol GetRequest: Request {

/// Path
var path: String { get }

/// Query Params
var queryParams: [String: String]? { get }
}

/// 基底Request - 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
/// HttpBinService ( https://httpbin.org/#/HTTP_Methods )
struct HttpBinService{

/// 預設設定值
static var defaultSetting: HttpBinServiceSetting = .init(status: .production)
}

/// HttpBinServiceSetting
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
}
}

使用方式

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
// Case: 進行 Mock 的測試
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
// Case: 整個站台切換正式或測試環境
HttpBinService.defaultSetting.status = .debug
let request = HttpBinService.HttpBinGetRequest(foo: "123")
ApiNativeClient(session: .shared).send(request: request, queue: .main) {
result in print(result)
}
// Case: 只切換一道 Api 正式或測試環境
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 // TODO: - Method
}
var adapters: [RequestAdapter] {
return [
// TODO: - 請求轉接器
]
}
var decisions: [Decision] {
return [
// TODO: - 決策路徑
]
}
}
struct HttpBinCustomResponse: Codable {
// TODO: - 回應物件
}
}

Demo