Utilisation de Lookslike pour tester les formes de données en Go

Chez Elastic, nous avons mis au point une bibliothèque open source de tests et de validation de schémas en Go que je souhaiterais vous présenter. Son nom ? Lookslike. Lookslike vous permet d’établir des correspondances avec la forme de vos structures de données Golang comme JSON Schema, mais avec davantage de puissance et plus dans le style Go. Cette bibliothèque offre de nombreuses possibilités qui n’étaient pas proposées jusque-là.

Pour vous donner un exemple de sa puissance :

// Cette bibliothèque nous permet de vérifier si une structure de données est similaire, ou identique, à un schéma donné.
// Par exemple, nous pourrions faire un test pour déterminer si un animal est un chien ou un chat, à l’aide du code ci-dessous.

// Un chien nommé rover
rover := map[string]interface{}{"name": "rover", "barks": "often", "fur_length": "long"}
// Un chat nommé pounce
pounce := map[string]interface{}{"name": "pounce", "meows": "often", "fur_length": "short"}

// Ici, nous définissons un validateur.
// Ici, nous déterminons le chien comme étant un élément ayant :
// 1. une clé "name" (nom) qui correspond à une chaîne non vide quelconque, utilisant la définition `IsNonEmptyString` intégrée
// 2. une clé "fur_length" (longueur_fourrure) qui a pour valeur "long" (longue) ou "short" (courte)
// 3. une clé "barks" (aboie) qui a pour valeur "often" (souvent) ou "rarely" (rarement)
// 4. nous déterminons aussi qu’il s’agit de critères de correspondance stricts, c’est-à-dire que les éventuelles clés qui seraient présentes
//    autres que celles répertoriées seront considérées comme des erreurs
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())

L’exécution du code ci-dessus donnera le résultat suivant :

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]

Nous constatons ici que "rover", le chien, a généré une correspondance parfaite, contrairement à "pounce", le chat, qui est associé à deux erreurs. Quelles sont ces erreurs ? La première est qu’il n’y a pas de clé barks (aboie) définie. La deuxième est qu’il y a une autre clé, meows (miaule), qui n’était pas prévue.

Étant donné que Lookslike est en général utilisé pour réaliser des tests, nous disposons d’un module d’aide testslike.Test qui génère des sorties de tests bien agencées. Pour cela, changez juste les dernières lignes de l’exemple ci-dessus par ce qui suit :

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

Composition

L’un des concepts clés de Lookslike est la possibilité de combiner des validateurs. Supposons que nous souhaitions des validateurs distincts pour le chien et le chat, mais que nous ne voulons pas redéfinir des champs communs comme name (nom) et fur_length (longueur_fourrure) pour chacun d’eux. Voilà ce que ça donne.

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"},
}

// Nous constatons que les deux animaux ont la propriété "fur_length" (longueur_fourrure), mais que seul le chat miaule (clé "meows") et que seul le chien aboie (clé "barks").
// Nous pouvons encoder tous ces critères avec concision dans Lookslike avec lookslike.Compose.
// Comme nous pouvons le voir également, les clés "meows" (miaule) et "barks" (aboie) contiennent toutes deux les mêmes choix de valeur.
// Nous commencerons par créer un type IsDef composé avec la composition IsAny. Cette composition crée un type IsDef qui est
// un opérateur 'or' logique de ses arguments 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)
}

// Sortie :
// rover est un chien
// lucky est un chien
// pounce est un chat
// peanut est un chat

Pourquoi nous avons créé Lookslike

L’idée de Lookslike est née du projet Heartbeat chez Elastic. Heartbeat est l’agent sur lequel s’appuie notre solution Uptime. Il émet des requêtes ping sur les points de terminaison, puis indique s’ils sont actifs ou inactifs. Heartbeat génère des documents Elasticsearch, représentés en tant que types map[string]interface{} dans notre base de codes Golang. C’est le fait de tester lesdits documents de sortie qui a créé la nécessité d’établir cette bibliothèque, même si elle est désormais utilisée ailleurs dans la base de codes Beats.

Nous avons été confrontés à certaines problématiques, parmi lesquelles :

  • Certains champs contenaient des données avec lesquelles il fallait établir une correspondance imprécise, p. ex. monitor.duration qui mesurait la durée d’une exécution. Et d’une exécution à l’autre, la durée n’était pas la même. Nous souhaitions un moyen d’avoir une correspondance de données flexible.
  • Quel que soit le test, de nombreux champs étaient partagés avec d’autres tests, et les variations étaient limitées. Nous souhaitions pouvoir réduire la duplication de code en composant différentes définitions de champ.
  • Nous voulions obtenir des sorties de test appropriées, présentant les échecs de champ individuels comme des erreurs individuelles, d’où le module d’aide testslike.

Au vu de ces problématiques, nous avons pris les décisions suivantes au niveau de la conception :

  • Nous souhaitions que le schéma soit flexible, et nous voulions que les développeurs puissent créer de nouveaux critères de correspondance avec facilité.
  • Nous voulions qu’il soit possible de composer et d’imbriquer les schémas, de sorte que, si vous imbriquiez un document dans un autre, il soit possible de combiner simplement les schémas sans avoir à dupliquer une partie du code.
  • Nous avions besoin d’un bon module d’aide de test, pour que les échecs de test soient faciles à lire.

Types de clés

L’architecture de Lookslike tourne autour de deux principaux types : Validator et IsDef. Validator est le résultat de la compilation d’un schéma particulier. C’est une fonction qui prend une structure de données arbitraire et renvoie un résultat. IsDef est le type qui permet d’établir une correspondance avec un champ individuel. Vous vous demandez peut-être pourquoi nous faisons la distinction entre ces deux types. Et cette question est pertinente, car nous serons probablement amenés à fusionner ces types dans le futur. Concrètement, si nous faisons la distinction, c’est principalement parce que IsDef dispose d’arguments supplémentaires par rapport à la localisation dans la structure de documents, qui lui permettent d’effectuer des validations plus pointues d’après le contexte. Les fonctions Validator n’ont pas d’informations supplémentaires, mais elles sont plus conviviales à exécuter (elles prennent juste en compte interface{} et font la validation).

Pour obtenir des exemples de types IsDefs personnalisés, consultez tout simplement les fichiers source. Vous pouvez ajouter un nouveau type IsDef à votre propre source pour l’étendre.

Exemples dans le champ

Nous nous servons très régulièrement de Lookslike dans Beats. Besoin d’un exemple ? Ne cherchez plus, consultez cette recherche github.

Nous avons besoin de votre aide !

Si vous souhaitez utiliser Lookslike, envoyez une requête d’extraction sur le référentiel ! Nous pouvons utiliser un ensemble particulier de types IsDef plus complet.

En savoir plus

Nous n’avons pas ménagé notre peine pour vous aider avec Lookslike. Vous pouvez lire les documents concernant Lookslike sur godoc.org.