Lookslikeでデータの形式を検証

Elasticが開発したオープンソースの新しいGoテスティング/スキーマ検証ライブラリをご紹介します。その名前はLookslike。これにより、Goのデータ構造の形式と照合させることができます。JSONスキーマと同様ですが、それよりもさらに強力かつGoらしい方法で実行できます。既存のGoテスティングライブラリでは不可能なことが多数実行できます。

早速、その強力な機能の例を紹介しましょう。

// このライブラリでは、あるデータ構造が特定のスキーマに類似または一致しているかをチェックできます。
// たとえば、下記のコードを使用して、ペットが犬か猫かを検証できます。

// 「rover」は犬の名前です。
rover := map[string]interface{}{"name": "rover", "barks": "often", "fur_length": "long"}
// 「pounce」は猫の名前です。
pounce := map[string]interface{}{"name": "pounce", "meows": "often", "fur_length": "short"}

// ここでバリデーターを定義します。
// 次のキーを持つものとして犬を定義します:
// 1. 組み込みの`IsNonEmptyString`定義を使用した、空でない任意の文字列の「name」キー
// 2. 「fur_length」キー。値は「long」または「short」のいずれか
// 3. 「barks」キー。値は「often」または「rarely」のいずれか
// 4. これをstrict matcher(厳密な一致)として定義。つまり、リストしたキー以外のものが存在する場合は、
//    エラーとして認識される
dogValidator := lookslike.Strict(lookslike.MustCompile(map[string]interface{}{
	"name":       isdef.IsNonEmptyString,
	"fur_length": isdef.IsAny(isdef.IsEqual("long"), isdef.IsEqual("short")),
	"barks": isdef.IsAny(isdef.IsEqual("often"), isdef.IsEqual("rarely")),
}))

result := dogValidator(rover)
fmt.Printf("Checked rover, validation status %t, errors: %v\n", result.Valid, result.Errors())
result = dogValidator(pounce)
fmt.Printf("Checked pounce, validation status %t, errors: %v\n", result.Valid, result.Errors())

上記のコードを実行すると、以下の内容が出力されます。

Checked rover, validation status true, errors: []
Checked pounce, validation status false, errors: [@Path 'barks': expected this key to be present @Path 'meows': unexpected field encountered during strict validation]

ここで一致したのは、予測どおり犬の「rover」であり、猫の「pounce」は一致せず、エラーが2つ出力されます。そのエラーのうちの1つはbarksキーが定義されていないこと、もう1つは予測されていない余分なキーのmeowsがあることです。

通常、Lookslikeはテスティングとして使用されるため、テスト出力を適切なフォーマットとして成形するtestslike.Testヘルパーを用意しています。上記例の最後の行を下記に変更するだけです。

testslike.Test(t, dogValidator, rover)
testslike.Test(t, dogValidator, pounce)

構成

Lookslikeの重要な概念は、バリデーターを組み合わせられることです。たとえば、nameや`fur_lengthなどの共通のフィールドを再定義することなく、猫のバリデーターと犬のバリデーターを分ける場合を見てみましょう。次のようなケースを例として説明します。

pets := []map[string]interface{}{
	{"name": "rover", "barks": "often", "fur_length": "long"},
	{"name": "lucky", "barks": "rarely", "fur_length": "short"},
	{"name": "pounce", "meows": "often", "fur_length": "short"},
	{"name": "peanut", "meows": "rarely", "fur_length": "long"},
}

// すべてのペットに「fur_length」プロパティがありますが、猫のみに「meow」、犬のみに「bark」があります。
// lookslike.Composeを使用することで、これを簡潔にエンコードできます。
// また、「meows」と「barks」には同じ値が列挙されていることが分かります。
// IsAny構成を使用して、IsDef構成を作成することから開始しましょう。これにより、
// IsDef引数の論理的な「or」となる新しいIsDefが作成されます。

isFrequency := isdef.IsAny(isdef.IsEqual("often"), isdef.IsEqual("rarely"))

petValidator := MustCompile(map[string]interface{}{
	"name":       isdef.IsNonEmptyString,
	"fur_length": isdef.IsAny(isdef.IsEqual("long"), isdef.IsEqual("short")),
})
dogValidator := Compose(
	petValidator,
	MustCompile(map[string]interface{}{"barks": isFrequency}),
)
catValidator := Compose(
	petValidator,
	MustCompile(map[string]interface{}{"meows": isFrequency}),
)

for _, pet := range pets {
	var petType string
	if dogValidator(pet).Valid {
		petType = "dog"
	} else if catValidator(pet).Valid {
		petType = "cat"
	}
	fmt.Printf("%s is a %s\n", pet["name"], petType)
}

// 出力:
// roverは犬
// luckyは犬
// pounceは猫
// peanutは猫

Lookslikeを構築した理由

Lookslikeは、ElasticのHeartbeatプロジェクトから生まれました。Heartbeatは、Elasticのアップタイムソリューションを支えるエージェントであり、エンドポイントにpingを送信し、それらが稼働しているかどうかをレポートします。Heartbeatの最終的な出力はElasticsearchドキュメントであり、Goのコードベースでmap[string]interface{}型として表されます。これらの出力ドキュメントのテストのために、このライブラリが必要になりました(ただし、現在はBeatsコードベースの他の部分で使用されています)。

Elasticが直面した課題は次のようなものでした。

  • 実行時間を示すmonitor.durationなど、あいまいに一致する必要のあるデータを持つフィールドがありました。これは実行によって異なります。データを大まかに一致させる方法が必要でした。
  • どのテストでも、多くのフィールドが他のテストと同様であり、ほんのいくつかが異なっているだけでした。異なるフィールド定義を構成してコードの重複を削減できる必要がありました。
  • 各フィールドの失敗を個別のエラーとして表示する見やすいテスト出力が必要でした。そのためにテストヘルパーのtestslikeが必要でした。

これらの課題に対応するために、設計に関して次のような決定を行いました。

  • 柔軟なスキーマにすること。また、開発者にとって新しいマッチャーを簡単に作成できるようにすること。
  • すべてのスキーマを構成可能およびネスト可能にすること。つまり、ドキュメントを別のドキュメントにネストする場合、コード群を重複させることなく、スキーマを組み合わせるだけで済むようにすること。
  • テストの失敗が簡単に見て分かるようにするために、優れたテストヘルパーを作成すること。

主要な型

Lookslikeのアーキテクチャーは、ValidatorIsDefの2つの主要な型を中心に展開します。Validatorは、特定のスキーマをコンパイルした結果であり、任意のデータ構造を受け取り、結果を返す関数です。IsDefは、各フィールドの一致の照合に使用される型です。なぜこれらを区別しているのか不思議に思う方もいらっしゃるでしょう。実際、これらの型は今後マージする可能性があります。しかし、IsDefがドキュメント構造の中の場所に関する追加の引数を取得する主な理由は、そのコンテキストに基づいて高度な検証を実行できるようにするためです。Validator関数は追加のコンテキストを受領しませんが、ユーザーにとってより実行しやすくなっています(interface{}のみを取得し、それを検証します)。

カスタムのIsDefsを記述する例として、こちらのソースファイルをご覧ください。ご自身のソースに新しいIsDefを追加して拡張することができます。

フィールドでの例

LookslikeはBeatsで広範囲に使用できます。その多数の使用例については、こちらのgithub検索でご確認いただけます。

ご協力をお願いします

Lookslikeにご興味をお持ちの場合は、こちらのリポジトリでプルリクエストを送信してください。特に、より包括的なIsDefのセットが利用できるようになる可能性があります。

詳細のご確認

Elasticでは、Lookslikeに関する優れたドキュメントを作成できるよう懸命に取り組んでいます。Lookslikeのドキュメントはgodoc.orgで参照できます。