go-asyncapi: AsyncAPI-3-Specs in Go laden

go-asyncapi liest AsyncAPI-3-Dokumente in Go ein und gibt dir Server, Channels, Operations und Messages als schlichte Structs zurück. Jeder $ref wird über Dateigrenzen hinweg aufgelöst, jeder Trait eingemergt, und alles kommt in der Reihenfolge, in der das Dokument es auflistet, statt in der, auf die Gos Maps heute gerade Bock haben. Es generiert keinen Code und bewertet auch nicht deine Lebensentscheidungen. Ich hab es geschrieben, weil jede Go-Option, die ich gefunden habe, entweder auf AsyncAPI 2 festhing oder ein kompletter verdammter Generator war, der über mein Projektlayout bestimmen wollte, und ich wollte nur den Ladeteil, für sich allein. Der Generator lebt separat in gen-asyncapi, dieselbe Aufteilung wie bei oapixconstgen auf der OpenAPI-Seite.


Installieren, Spec laden

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 lehnt alles ab, dessen asyncapi-Feld nicht mit 3. anfängt, und lädt dann Server, Channels, Operations und Components in genau dieser Reihenfolge, weil Operations auf Channels zeigen und die Channels zuerst existieren müssen. Operation.Channel ist ein echter *Channel, kein String, den du später nachschlägst. Action ist send oder receive, alles andere ist ein commerr.ErrInvalidValue. server.URL() füllt jeden {placeholder} mit seinem Default, also hörst du auf, Strings von Hand zu verstümmeln.

Der Teil, der mir wirklich wichtig ist: Messages werden danach erfasst, wo ihre Definition liegt. Eine Message, die von drei Channels und zwei Operations referenziert wird, ist ein einziger *Message-Pointer, nicht fünf Kopien, die du wie ein Höhlenmensch deduplizieren darfst. Traits werden in Listenreihenfolge gemergt, im Stil von JSON Merge Patch, und überschreiben nie, was die Message schon setzt, und ein $ref im Payload eines Traits wird relativ zur Datei des Traits aufgelöst, nicht zu deiner.


Ein Payload-Schema durchlaufen

Schemas bleiben roh, als *asyncapi.Object-Bäume, die die Reihenfolge der Keys behalten: Keys []string plus Values map[string]any. Get, String und Object sind nil-safe, wenn du also bis zu einem fehlenden Key verkettest, kriegst du nil statt einer Panic. Refs innerhalb eines Schemas laufen über den Resolver des Dokuments:

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 gibt den Wert plus seine Location zurück, also eine Datei und einen JSON Pointer. Location ist ein vergleichbarer Struct, also liefern dir zwei Refs, die auf demselben Schema landen, denselben Map-Key, und jeden Typ genau einmal zu emittieren ist plötzlich kein Scheißchaos mehr. Genau diese eine Eigenschaft macht einen Generator obendrauf überhaupt erst vernünftig machbar. Payloads im Format AsyncAPI, JSON Schema oder OpenAPI werden als Schemas geladen; bei Avro, Protobuf oder sonst was bleibt Payload nil, und UnsupportedFormat sagt dir, was übersprungen wurde.


Remote-Refs lädt das Ding nicht

Jeder $ref mit :// drin ist ein Fehler, der dir sagt, du sollst das Dokument runterladen und über den Pfad referenzieren. Ein Spec-Loader, der hinter deinem Rücken Netzwerkaufrufe macht, kommt mir nicht ins Haus. Lokale Refs und Refs auf relative Dateien werden problemlos aufgelöst, jede Datei wird einmal geladen und nach absolutem Pfad gecacht, eine Ref-Kette bekommt 64 Hops, bevor sie als Zyklus gilt, und ein Pointer ins Nichts ist commerr.ErrNotFound. Diese Sentinels kommen aus ctxerrors, also fängt errors.Is(err, commerr.ErrNotFound) eine kaputte Ref, egal wie viel Kontext drumherum gewickelt wurde. Wenn deine Spec die Hälfte ihrer Schemas von irgendeiner URL zieht, hol dir den Scheiß vorher lokal.