Swift 3でバリデーションライブラリ作った

[Swift Advent Calendar 2016](http://qiita.com/advent-calendar/2016/swift) 22日目の[かっくん](http://qiita.com/fromkk)です。

今の会社に入社して1年1ヶ月が経過しました。
入社したての頃に[Objective-Cでバリデーションライブラリを作成](http://techblog.timers-inc.com/entry/tmvalidator)しました。
当時はSwiftはリリースされていましたが、自社のアプリにはまだ適応しておらず、個々人で自主的に勉強を進めていた様な状況でした。

あれからたった1年で、世間的にはSwift 3がリリースされOSSになり、自社のアプリも50%ぐらいがSwiftになりました。
時間の流れとは恐ろしいですね...

というわけで当時は全然Swiftを理解してなかった人が1年経って同じ目的のライブラリを作るとこうなったという記事を書こうかなと思います。

# Valy

<img src="https://github.com/fromkk/Valy/blob/master/valy.png?raw=true" width="320" height="auto">

今回Swift 3で[Valy](https://github.com/fromkk/Valy)というライブラリを作成しました。
決してバリィさんをパクったわけでは...(ｺﾞﾎｯｺﾞﾎｯ
（名前に関してはバリデーションを行いそうな事を想像出来る事、短くてタイプ数が少なくて済むというのを重要視していて、たまたま似たような名前のゆるキャラがいたので便乗した形に近いです...）

基本思想はTMValidatorとほぼ同じで、バリデーターを作成し、ルールを追加して、文字列をチェックメソッドに渡してやればチェックをしてくれるといった感じです。
じゃあ何故敢えてSwiftで書き直すのかというと、やはり過去に作ったものは時間が経つと粗が見え、当時はイケていたと思っていた設計も改めて見るとすっきりしていない箇所が目につく様になったからです。  
これを成長と呼ぶのか好みが変わったのかは自分では分かりませんがポジティブに成長だと捉えて、新しく作り直してみたいなと思ったのがきっかけです。

## TMValidatorのサンプルコード

```Objective-C
UITextField *name = [[UITextField alloc] init];
name.text = @"名前";

TMValidatorField *fieldName = [TMValidatorField fieldWithValue:name.text andLabel:@"name" andElement:name];
[[fieldName addRule:[TMValidatorRuleRequired rule]] addRule:[TMValidatorRuleMaxLength ruleWithLength:@10]];

TMValidator *validator = [TMValidator validator];
[[validator addField:fieldName];

[validator runWithSuccesses:^(NSArray *successes) {
    NSLog(@"success! %@", successes);
} andFailure:^(NSArray *errors) {
    NSLog(@"failure! %@", errors);
}];
```

## Valyのサンプルコード

```swift
let result = Valy.factory()
    .add(rule: ValyRule.required)
    .add(rule: ValyRule.maxLength(10))
    .run(with: value)
switch result {
case .success:
    print("success!")
case .failure(let rule):
    print("failed rule \(rule)")
}
```

比べてみると一目瞭然ですがまずSwiftになっただけでかなりすっきりした様な気がします。（文字量が減っただけというツッコミは受け付けてません）
内部の実装も大幅に違います。

## ルール

`TMValidator`ではルールは`TMValidatorRule`という基底クラスがあり、これを継承してルールを作るという事をやっていました。
また、エラーメッセージとエラーコード等をそれぞれのルール側で定義する必要がありかなり面倒でした。
`Valy`ではルールは`AnyValidatorRule`を実装していれば何でも良いです。
これにより独自のバリデータールールを作成する事が容易になりました。

```swift
enum CustomValidatorRule: AnyValidatorRule {
    case email
    func run(with value: String?) -> Bool {
        switch self {
        case .email:
            return doYourEmailValidation(value)
        }
    }
}

let result = Valy.factory().add(rule: CustomValidatorRule.email).run(with: value)
```

こんな感じで好き放題にルールを作る事が出来る様になりました。
そのお陰で`Valy`側では最低限のルールをデフォルトで用意するだけにしてあります。

用意したルールはこんな感じです。

```swift
public enum ValyRule: AnyValidatorRule {
    case required //必須
    case match(String) //完全一致
    case pattern(String) //正規表現
    case alphabet //半角英字
    case digit //半角数字
    case alnum //半角英数
    case number //数値
    case minLength(Int) //最低文字数
    case maxLength(Int) //最大文字数
    case exactLength(Int) //文字数一致
    case numericMin(Double) //最低数値
    case numericMax(Double) //最大数値
    case numericBetween(min: Double, max: Double) //数値範囲
}
```

宗教論争が起こりそうなメールアドレスやURLのバリデーションは結局独自カスタムしたくなったり、サービスによってバリデーションが違ったりするのでデフォルトで用意するのはやめました。

## エラーメッセージ

`TMValidator`ではルール側にエラーメッセージを用意して、エラーが発生した際にそのエラーを返す様な仕組みにしていました。

```objc
@interface TMValidatorRule : NSObject <TMValidatorRuleProtocol>

+ (instancetype _Nonnull)rule;

@property (nonatomic, readonly) TMValidatorErrorCode errorCode;
@property (nonatomic) NSString * _Nullable errorMessage;
@property (nonatomic) NSDictionary * _Nullable userInfo;

@end
```

ただ、社内で利用していると、後からエラーメッセージをカスタムしたいという要望があり、その為のバージョンアップを行ったりしました。
これを踏まえて`Valy`ではバリデーションに失敗した時は、最初に失敗したルールのみを返す様にしました。
エラーメッセージやエラーの表示方法は使う側に任せる様にしています。

```swift
var errorMessages: [String] = []
let nameValidation = Valy.factory(rules: [ValyRule.required, ValyRule.maxLength(10)])
switch nameValidation.run(with: name) {
case .failure(let rule):
    switch rule {
        case ValyRule.required:
        errorMessages.append("名前は必須入力です")
        case ValyRule.maxLength(let length):
        errorMessages.append("名前は\(length)文字以内で入力して下さい")
        default:
        break
    }
default:
break
}

if 0 != errorMessages.count {
    //display error!
    return
}
```

少しネストが深くなりましたが、エラーメッセージを作成する所を別の関数等で用意すれば、すっきり書く事は出来るかなと思っています。

## Swiftだから出来たこと

### `enum`でルール

`TMValidator`はルールも全てクラスでしたが、`Valy`は`enum`で作る事が出来る様になりました。
`Associated Value`を使う事で柔軟にルールを作る事が出来る様になりました。

```swift
enum CustomValidatorRule: AnyValidatorRule {
    case allowedCharacterSet(CharacterSet):
    func run(with value: String?) -> Bool {
        guard let value = value else {
            return true
        }

        switch self {
        case .allowdCharacterSet(let characterSet):
            return 0 == value.components(separatedBy: characterSet).joined().characters.count
        }
        
    }
}
```

### 型消去

最後に、今回個人的に初めて型消去の仕組みを参考にして取り入れてみました。

```swift
protocol AnyValidator {
    var rules: [AnyValidatorRule] { get }
    static func factory() -> AnyValidator
    static func factory(rules: [AnyValidatorRule]) -> AnyValidatable
    func add(rule: AnyValidatorRule) -> AnyValidatable
    func add(rules: [AnyValidatorRule]) -> AnyValidatable
}
protocol AnyValidatable: AnyValidator {
    func run(with value: String?) -> ValidatorResult
}
```

上記の様な`protocol`を用意してみました。
`factory()`単体で呼び出した場合、ルールが何もない事が明白なので、`run(with:)`を呼び出させたく無かったのです。
その為、`run(with:)`が定義されていない`AnyValidator`の`protocol`を返す様にしています。
一方、`factory(rules:)`や、`add(rule:)`の場合は既に`AnyValidatorRule`が渡されている事が期待出来るので`run(with:)`が定義されている`AnyValidatable`を返す様にしました。

`Valy`自体は `class Valy: AnyValidatable {}`の様に`AnyValidatable`を実装しているんですが、`Valy.factory().run(with: value)`としようとすると、 **Value of type 'AnyValidator' has no member 'run'**というエラーが出ます。
ちゃんと`Valy.factory().add(rule: ValyRule.required).run(with: value)`の様にルールを追加してやる必要があります。

自分としてもまだ勉強中な為、これで正しいか少し不安な所はありますが、プロトコルを上手く活用してライブラリを使う側にも正しく制約を加える事が出来ました🎉

# まとめ

1年前と同じ目的のライブラリを再度作ってみましたが、時代も変われば自分も変わって良い感じに変化が出せたんじゃないかと思います。
また、勉強会等で得た知識を新たに生かせたのが嬉しかったです。
いいなと思ったら[スター⭐](https://github.com/fromkk/Valy)、いいね👍下さい！