Go のパスワードバリデーションの正規表現で肯定的先読みができない場合の対処法

# 概要
Go のパスワードバリデーションを実装中に正規表現で肯定的先読みがサポートされておらず目的のバリデーション正規表現が1行ではかけませんでした。

ここでは、Go のパスワードバリデーションの実装サンプルとして解決方法の１つを残しておきます。

# 動作環境
- Go v1.16
- [go-playground/validator v10.6.1](https://github.com/go-playground/validator)
- [gin v1.7.2](https://github.com/gin-gonic/gin) 


# やりたいこと
パスワードバリデーションで**「大文字小文字英字」「数字」「特殊記号」を少なくとも１つずつ含んだ**パスワードかつ利用可能文字も**「大文字小文字英字」「数字」「特殊記号」**のみ許可するバリデーションを Go と [go-playground/validator](https://github.com/go-playground/validator) を利用して実装したい。

# javascript の場合
肯定的先読みがサポートされている js などでは例えば、**少なくとも1文字以上大文字の英字**が含まれている正規表現は下記のようにかけます。

```
(?=.*?[A-Z])
```

これを応用すれば、**「大文字小文字英字」「数字」「特殊記号」を少なくとも１つずつ含むかつ利用可能文字も「大文字小文字英字」「数字」「特殊記号」**の正規表現は下記のように記述できます。

```js
'^(?=.*?[a-z])(?=.*?[A-Z])(?=.*?\\d)(?=.*[-^$*.@])[a-zA-Z\\d-^$*.@]$'
```

それぞれの正規表現の細かい解説はこちらの記事で詳しくまとめてくださっている方がいましたので、参照してみてください。
https://qiita.com/momotaro98/items/460c6cac14473765ec14


# Go の場合
しかし Go だと肯定的先読みがサポートされていないので上記の書き方を Go でも同じように記述するとエラーがでてしまいます。

```go
reg = `^(?=.*?[a-z])(?=.*?[A-Z])(?=.*?\\d)(?=.*[-^$*.@])[a-zA-Z\\d-^$*.@]$`
regexp.MustCompile(reg).Match([]byte(str))
// error parsing regexp: invalid or unsupported Perl syntax: `(?=`
```

# 解決方法
go-playground/validator のバリデーションライブラリを利用して 1行の正規表現で書かずに正規表現を分割したカスタマイズバリデーションの組み合わせで実装しました。

下記が実装コードです。
※ validator ライブラリの使い方と、カスタムバリデーションの登録部分の実装は省略しています

```go:models.go
type User struct {
	Password string `json:"password" jaFieldName:"パスワード" validate:"required,custom-low,custom-upp,custom-num,custom-symbol"`
}
```

```go:validator.go
// 小文字が含まれるかどうか
func includeLowercase(fl validator.FieldLevel) bool {
	return checkRegexp("[a-z]", fl.Field().String())
}

// 大文字が含まれるかどうか
func includeUppercase(fl validator.FieldLevel) bool {
	return checkRegexp("[A-Z]", fl.Field().String())
}

// 数値が含まれるかどうか
func includeNumeric(fl validator.FieldLevel) bool {
	return checkRegexp("[0-9]", fl.Field().String())
}

// 特殊記号が含まれるかどうか
func includeSymbol(fl validator.FieldLevel) bool {
	availableChar := checkRegexp(`^[0-9a-zA-Z\-^$*.@]+$`, fl.Field().String())
	checkIsSymbol := checkRegexp(`[\-^$*.@]`, fl.Field().String())

	return availableChar && checkIsSymbol
}

// 正規表現共通関数
func checkRegexp(reg, str string) bool {
	r := regexp.MustCompile(reg).Match([]byte(str))
	return r
}
```
## 解説
実現したい正規表現を一行で書かずにカスタマイズバリデーションとして細かく分割します。
ここでは

- 小文字が含まれるかどうか
- 大文字が含まれるかどうか
- 数値が含まれるかどうか
- 特殊記号が含まれるかどうか

の 4つに分割しました。

上から3つはコードそのままだと思うので最後だけ解説します。

👇 ここでは利用可能文字を指定してます。

```go
availableChar := checkRegexp(`^[0-9a-zA-Z\-^$*.@]+$`, fl.Field().String())
```
先程の上から 3 つのバリデーションはあくまでも **含まれるか** しかチェックしていないので、小文字、大文字、数値が少なくとも１つ以上入ってれば**それ以外の文字列は何でもOKになってしまいます**。日本語などのマルチバイト文字さえ許可してしまいます。
それを防ぐために利用可能文字の正規表現をバリデーションに追加しています。

次に

👇 ここでは特殊記号が少なくとも１つ以上含まれているかどうかのチェックを行っています。

```go
checkIsSymbol := checkRegexp(`[\-^$*.@]`, fl.Field().String())
```

最後に 2つの論理演算をして特殊記号チェックのバリデーション結果を返します。

```go
return availableChar && checkIsSymbol
```

## メリット
分割したことによるメリットはバリデーションエラー時のメッセージを分けたカスタムバリデーションごとに表示することができます。
もし正規表現を1行で記述した場合は、数字が含まれていないのか特殊記号が含まれていないのか判別が付きません。

分割することによって 1つずつバリデーションを評価するため、詳細なエラーメッセージをユーザに表示することができます。

# 最後に

正規表現は 1行で複雑に実装しようとすると解読が大変になるので後から修正するのが怖くなります。なるべくシンプルに分割して実装することでテストもしやすくなるので良いかなと思いました。

Go では使用上肯定的先読みがサポートされていないので、もし同じように実装で困ったら分割してみることをおすすめします。

またバリデーションの実装には **[go-playground/validator](https://github.com/go-playground/validator)**ライブラリが非常に便利だったので参考にしてみてくださいm(_ _)m

# 参考
- [パスワード向け正規表現 /^(?=.*?[a-z])(?=.*?\d)[a-z\d]{8,100}$/i を解読する](https://qiita.com/momotaro98/items/460c6cac14473765ec14)
- [Override existing validator - Adding regex for password validation](https://github.com/go-playground/validator/issues/251)