バリデーションをメソッドチェーンで書く方法

アプリ開発で、ユーザ入力が伴う画面では必ずと言っていいほど入力内容のバリデーションが必要です。ところが、もしかしてそのバリデーションのコード、読みにくすぎ？

例えばこんな要件があるとします：ユーザのメンバー ID を入力する画面があります。この ID は 10 桁以内の数字です。さてこれのバリデーションを書きましょうか：

```swift
func isIDValid(_ id: String) -> Bool {
    
    guard !id.isEmpty else {
        return false
    }
    
    guard Int(id) != nil else {
        return false
    }
    
    guard id.count < 10 else {
        return false
    }
    
    return true
    
}

validate("123") // true
validate("abc") // false
```

うーん読めなくはないですが、`guard` 文多いですね。今回のような単純な要件ならまあ読めなくはないですが…
あと書くのだるい…

もちろん、どうせ `Bool` 返してるだけなので、いっそのことこんな感じで書くのもいいですね：

```swift
func validate(_ id: String) -> Bool {
    
    return !id.isEmpty
        && Int(id) != nil
        && id.count <= 10
    
}

validate("123")
validate("abc")
```

これならだいぶ書きやすくなります。ただやっぱ補完効きにくいのでまだ改善の余地はあるし、何よりそもそもの話、バリデーション結果を `Bool` で返すのはなんかな…ですよね。

と言うわけで、これをこんな風に書きたいのです：

```swift
func validate(_ id: String) -> ValidationStatus<InvalidID> {

    IDValidator.validate(id) { $0
        .isNotEmpty()
        .isInt()
        .lessThan10Digits()
    }

}


validate("123") // .valid
validate("abc") // .invalid(.notInt)
```

これではメソッドチェーンが使えるのでだいぶ書きやすいし、何よりなんのバリデーションか一目瞭然ですね！

では、これをどうやって作っているのかと言うと、簡単です。まずバリデーションの型を作ります：

```swift
protocol InvalidStatus: Equatable {
    
}

enum ValidationStatus<Invalid: InvalidStatus> {
    case valid
    case invalid(Invalid)
}
```

バリデーション状態は `.valid` と `.invalid` のみにし、具体的な `.invalid` 状態はさらに `Invalid` で定義してもらいます。これのメリットは、大きなスコープで見た時は有効と無効だけで分けられるのでいろんな柔軟な対応がしやすい[^enum]し、具体的になぜ無効なのかが知りたいときのその無効状態を `Invalid: InvalidStatus` から取り出せます。

[^enum]: 参考：[それをEnumるなんて とんでもない！ - Swift Enum antipattern](https://speakerdeck.com/takasek/sorewoenumrunante-tondemonai-swift-enum-antipattern)

次に、このバリデーションを実際にかける型を作ります。とは言え、どっちかと言うとこれはラッパーオブジェクトに近いですかね：

```swift
struct ValidationContainer<Target, Invalid: InvalidStatus> {
    
    private let target: Target
    private let invalid: Invalid?
    
    private func finish() -> ValidationStatus<Invalid> {
        
        if let invalid = invalid {
            return .invalid(invalid)
            
        } else {
            return .valid
        }
        
    }
    
    static func validate(_ target: Target, with validation: (Self) -> Self) -> ValidationStatus<Invalid> {
        
        let container = Self.init(target: target, invalid: nil)
        let result = validation(container).finish()
        
        return result
        
    }
    
    func guarantee(_ condition: (Target) -> Bool, otherwise invalidStatus: Invalid) -> Self {
        
        // If the container already has an invalid status, skip the condition check.
        guard invalid == nil else {
            return self
        }
        
        if condition(target) == true {
            return self
            
        } else {
            return ValidationContainer(target: target, invalid: invalidStatus)
        }
        
    }
    
}
```

このコンテナは何かと言うと、現在のバリデーション対象と無効状態を保存して、そして次のバリデーションに回すためのものです。まずは無効状態なしで作って、そして次々と `guarantee` を回して、もしどこか無効状態が途中であったらそれをそのまま返して、最後まで問題なかったら `finish()` で `.valid` を返せばいいです。

あれ？ `validate` メソッドに `finish` は登場したけど `guarantee` 登場してなくない？と思うかもしれませんが、`guarantee` は実際の利用する側に公開したメソッドで、それは `validate` の `validation: (Self) -> Self` と言うクロージャ内で利用してもらう予定だからです。

ちなみに `struct` にも関わらず `Self` が書けるのは、Swift 5.1 の新機能です[^self]。

[^self]: 参考：[Expanding Swift Self to class members and value types](https://github.com/apple/swift-evolution/blob/master/proposals/0068-universal-self.md)

さて、ここまでで下準備ができました。ではどう利用すればいいでしょうか？まずは `InvalidStatus` を作ります。今回は ID のバリデーションなので、`InvalidID` を作りましょう：

```swift
enum InvalidID: InvalidStatus {
    case empty
    case notInt
    case tooLong(maxCount: Int)
}
```

見ての通り、ID が空か、数字じゃないか、もしくは文字数が多すぎるの 3 パターンの無効判定があります。そしてその次、それぞれの判定を `guarantee` を使って組み込みます：

```swift
extension ValidationContainer where Target == String, Invalid == InvalidID {
    
    func isNotEmpty() -> Self {
        
        return guarantee({ !$0.isEmpty }, otherwise: .empty)
        
    }
    
    func isInt() -> Self {
        
        return guarantee({ Int($0) != nil }, otherwise: .notInt)
        
    }
    
    func lessThan10Digits() -> Self {
        
        let maxDigits = 10
        return guarantee({ $0.count <= maxDigits }, otherwise: .tooLong(maxCount: maxDigits))
        
    }
    
}
```

これで `文字列は空文字列じゃない`、`文字列は数字である` と `文字列は最大 10 文字以内` のバリデーションが書けました。最後は `typealias` を定義してこれらを繋げば、読みやすいバリデーションが書けますね！

```swift
typealias IDValidator = ValidationContainer<String, InvalidID>

func validate(_ id: String) -> ValidationStatus<InvalidID> {

    IDValidator.validate(id) { $0
        .isNotEmpty()
        .isInt()
        .lessThan10Digits()
    }

}


validate("123") // .valid
validate("abc") // .invalid(.notInt)
```

めでたしめでたし。

---

p.s. この書き方なんか見覚えあるぞ？と思ってくれた方もしいらっしゃったら、ありがとうございます！！そうですあの [NotAutoLayout](https://github.com/el-hoshino/NotAutoLayout) でも使ってた[クロージャ×メソッドチェーンの組み方](https://speakerdeck.com/lovee/modannainisiyaraiza)です :laughing: