will and way

ただの自分用メモを人に伝える形式で書くことでわかりやすくまとめてるはずのブログ

Alamofireでパラメータをenumで扱えるようにする

github.com

Alamofireといえば、言わずと知れたSwift界のHTTPクライアント。名前の由来はテキサスの花の名前らしいすね。
今回はAlamofireのリクエストパラメータをenumで扱うという話。

大前提。Stringは脆い!

Stringはすべての表現を兼ね備える万能な型です。
色は"#FFFFFF"という値で扱うこともできるし、URLも"http://blog.matsuokah.jp/“と扱うことができます。

故に、APIレスポンスの型にStringが採用される変数も多いですよね。
(数値とBool以外Stringで受け取ってクライアントでパースするのが一般的なような)

しかしながらStringを型として用いる場合、その変数には「特定の値が入っている」ことを前提として扱わなければならないので、コードの堅牢性を下げうる型とも言えます。

let color: String = "http://blog.matsuokah.jp/"と入力できてしまうが故にエラーはランタイムで発生することでしょう。コンパイラ言語のありがたみもありません。

今回はこれをAlamofireのパラメータに当てはめて考え、キー値をenumで扱えるようにしました

Alamofireのパラメータは [String: Any]という形式

AlamofireではAlamofire.Parametersというエイリアスが[String:Any]に参照しており、
Dictionaryをセットするだけでリクエストに応じて柔軟にパラメータを設定してくれる形式になっています。

dictionaryを代入するポイントはAlamofire.requestparamters:なので、ラップする箇所や各所のAPIサービス周辺でパラメータを用意する形式がシンプルなやり方可と思います。

APIClientにAPIのエンドポイント、パラメータ、返却する方をジェネリクスで決めてDataStoreやModelからコールするイメージです

struct SearchAPIService {        
    let apiClient: APIClient

    // APIをコールするServiceのメソッドの一例
    func searchUser(userName: String) -> Observable<SearchResult> {
        var paramter =  [String: Any]()        
        parameter["user_name"] = userName
        return apiClient.get(endpoint: endpoint, parameter: parameter)
    }
}

Parameterをenum化する

import Foundation

import Alamofire

/// Dictionaryから新たなDictionaryを作る
// MARK: - Dictionary
extension Dictionary {
    // MARK: Public Methods        
    public func associate<NKey, NValue>(transformer: @escaping ((Key, Value) -> (NKey, NValue))) -> Dictionary<NKey, NValue> {
        var dic: [NKey: NValue] = [:]
        forEach {
            let pair = transformer($0.key, $0.value)
            dic[pair.0] = pair.1
        }
        return dic
    }
}

// MARK: - ParameterKey
public protocol ParameterKey: Hashable {
    var key: String { get }
}

// MARK: - RequestParameter
public protocol RequestParameter {
    associatedtype T : ParameterKey
    
    // MARK: Internal Properties
    var parameters: Alamofire.Parameters { get }
    
    // MARK: Private Properties
    var parameter: [Self.T: Any] { get set }
    
    // MARK: Internal Methods
    mutating func setParameter(_ key: Self.T, _ value: Any)
}

// MARK: - APIParameter
public extension RequestParameter {
    public var parameters: Alamofire.Parameters {
        if parameter.isEmpty {
            return [:]
        }
        
        return parameter.associate { (param, any) in
            return (param.key, any)
        }
    }

    public mutating func setParameter(_ key: Self.T, _ value: Any) {
        parameter[key] = value
    }
}
// MARK: - SearchParameterKeys
public enum SearchParameterKeys: ParameterKey {
    case userName
    
    // MARK: Internal Properties
    public var key: String {
        switch self {
        case .userName:
            return "user_name"
        }
    }
}

// MARK: - SearchParameter
public struct SearchParameter: RequestParameter {
    public typealias T = SearchParameterKeys
    public var parameter: [SearchParameterKeys : Any] = [:]
}
struct SearchAPIService {        
    let apiClient: APIClient

    // APIをコールするServiceのメソッドの一例
    func searchUser(userName: String) -> Observable<SearchResult> {
        var paramter =  SearchParamter()        
        parameter.set(.userName, userName)
        return apiClient.get(endpoint: endpoint, parameter: parameter.parameters)
    }
}

こうすることで、SearchAPIに対して設定できるパラメータはSearchParameterのみ。SearchParameterでセットできるキー値はSearchParameterKeysのみとなりました。
また、qというキーに対して今はenum値qを用いてますがqueryのような、デスクリプティブな表現もできるようになります。

Pros & Cons

Pros

  • キーに設定できるパラメータが絞られるようになった
  • APIServiceのインタフェースにParameterを用いることができるようになったので、引数が少なく、拡張性をもたせることができる
  • キーenumはキー文字列へのaliasなのでより、説明的な表現ができる

Cons

  • キー値の共通がこのままだとできないので、API毎に似たような定義が増える

まとめ

Consに関してはプロトコルで共通部分を抜き出すなどすればなんとかできそうだと思っています。
また、主観ですがAPIに対して特定のキー値があることのほうが多いので問題に当たるケースは少ないかなと楽観してます。

今回載せたコードは下記のリポジトリに載せてます。(テストは動きません無)

github.com