go-asyncapi: charger des specs AsyncAPI 3 en Go

go-asyncapi lit des documents AsyncAPI 3 en Go et te rend les serveurs, les canaux, les opérations et les messages sous forme de structs tout bêtes, avec chaque $ref suivi d’un fichier à l’autre, chaque trait fusionné, et tout dans l’ordre où le document le liste au lieu de l’ordre qui plaît aux maps de Go ce jour-là. Il ne génère pas de code et il ne juge pas tes choix de vie. Je l’ai écrit parce que toutes les options Go que j’ai trouvées étaient soit coincées sur AsyncAPI 2, soit un putain de générateur complet qui voulait régenter l’arborescence de mon projet, alors que je voulais juste la partie chargement, et rien d’autre. Le générateur vit à part dans gen-asyncapi, même découpage qu’avec oapixconstgen côté OpenAPI.


Installe-le, charge une spec

go get github.com/psyb0t/go-asyncapi
document, err := asyncapi.Load("asyncapi.yaml")
if err != nil {
	return err
}
for _, server := range document.Servers {
	fmt.Println(server.Name, server.URL())
}
for _, operation := range document.Operations {
	for _, message := range operation.Messages {
		fmt.Println(operation.Action, operation.Channel.Address, message.Name)
	}
}

Load refuse tout document dont le champ asyncapi ne commence pas par 3., puis charge les serveurs, les canaux, les opérations et les composants dans cet ordre, parce que les opérations pointent vers des canaux et que les canaux doivent exister d’abord. Operation.Channel est un vrai *Channel, pas une string que tu vas chercher plus tard. Action vaut send ou receive, et n’importe quoi d’autre donne un commerr.ErrInvalidValue. server.URL() remplit chaque {placeholder} avec sa valeur par défaut, donc tu arrêtes de charcuter des strings à la main.

Le truc qui compte vraiment pour moi: les messages sont identifiés par l’endroit où se trouve leur définition. Un message référencé depuis trois canaux et deux opérations, c’est un seul *Message, pas cinq copies à dédoublonner comme un sauvage. Les traits fusionnent dans l’ordre de la liste, façon JSON Merge Patch, sans jamais écraser ce que le message définit déjà, et un $ref dans le payload d’un trait se résout par rapport au fichier du trait, pas au tien.


Parcourir le schéma d’un payload

Les schémas restent bruts, sous forme d’arbres *asyncapi.Object qui gardent l’ordre des clés: Keys []string plus Values map[string]any. Get, String et Object sont nil-safe, donc si tu enchaînes jusqu’à une clé absente tu récupères nil au lieu d’un panic. Les refs à l’intérieur d’un schéma passent par le Resolver du document:

message := document.Messages[0]
schema, at, err := document.Resolver.Deref(message.Payload, message.PayloadAt)
if err != nil {
	return err
}
object, ok := schema.(*asyncapi.Object) // false for a true/false schema
if !ok {
	return nil
}
properties := object.Object("properties")
if properties == nil {
	return nil
}
for _, name := range properties.Keys {
	fmt.Println(name)
}

Deref renvoie la valeur plus sa Location, c’est-à-dire un fichier et un JSON pointer. Location est un struct comparable, donc deux refs qui tombent sur le même schéma te donnent la même clé de map, et émettre chaque type exactement une fois n’est plus un bordel sans nom. C’est cette propriété-là, à elle seule, qui permet de poser un générateur par-dessus sans devenir fou. Les payloads au format AsyncAPI, JSON Schema ou OpenAPI se chargent comme schémas; avec Avro, Protobuf ou n’importe quoi d’autre, Payload reste à nil et UnsupportedFormat te dit ce qui a été ignoré.


Il n’ira pas chercher tes refs distantes

Tout $ref qui contient :// est une erreur qui te dit de télécharger le document et de le référencer par son chemin. Pas question qu’un loader de specs fasse des appels réseau dans ton dos. Les refs locales et vers des fichiers relatifs se résolvent sans souci, chaque fichier est chargé une seule fois et mis en cache par chemin absolu, une chaîne de refs a droit à 64 sauts avant d’être déclarée cycle, et un pointeur vers le vide, c’est commerr.ErrNotFound. Ces sentinelles viennent de ctxerrors, donc errors.Is(err, commerr.ErrNotFound) attrape une ref cassée quelle que soit la quantité de contexte enroulée autour. Si ta spec va chercher la moitié de ses schémas sur une URL quelconque, rapatrie cette merde en local d’abord.