go-asyncapi leest AsyncAPI 3-documenten in Go en geeft je servers, channels, operations en messages terug als gewone structs, met elke $ref gevolgd over bestanden heen, elke trait erin gemerged en alles in de volgorde waarin het document het opsomt, in plaats van de volgorde waar de maps van Go vandaag zin in hebben. Het genereert geen code en het oordeelt niet over je levenskeuzes. Ik heb het geschreven omdat elke Go-optie die ik vond óf vastzat op AsyncAPI 2 óf een complete fucking generator was die de baas wilde spelen over mijn projectindeling, terwijl ik alleen het laadgedeelte wilde, los. De generator staat apart in gen-asyncapi, dezelfde splitsing als oapixconstgen aan de OpenAPI-kant.
Installeren, spec laden
go get github.com/psyb0t/go-asyncapidocument, 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 weigert alles waarvan het asyncapi-veld niet met 3. begint, en laadt dan servers, channels, operations en components in die volgorde, omdat operations naar channels wijzen en de channels eerst moeten bestaan. Operation.Channel is een echte *Channel, geen string die je later nog moet opzoeken. Action is send of receive, en al het andere is een commerr.ErrInvalidValue. server.URL() vult elke {placeholder} met de standaardwaarde, dus je hoeft niet meer met de hand strings te verminken.
Het deel waar het mij echt om gaat: messages worden bijgehouden op basis van waar hun definitie staat. Eén message waarnaar drie channels en twee operations verwijzen is één *Message, niet vijf kopieën die je als een holbewoner mag dedupliceren. Traits worden in lijstvolgorde gemerged, op de manier van JSON Merge Patch, en overschrijven nooit wat de message zelf al zet, en een $ref in de payload van een trait wordt opgelost ten opzichte van het bestand van die trait, niet het jouwe.
Door een payload-schema lopen
Schema’s blijven rauw, als *asyncapi.Object-bomen die de volgorde van de keys bewaren: Keys []string plus Values map[string]any. Get, String en Object zijn nil-safe, dus als je doorketent naar een key die er niet is, krijg je nil in plaats van een panic. Refs binnen een schema gaan via de Resolver van het 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 geeft de waarde terug plus de bijbehorende Location, oftewel een bestand en een JSON pointer. Location is een vergelijkbare struct, dus twee refs die op hetzelfde schema uitkomen geven je dezelfde map-key, en elk type precies één keer genereren is ineens geen klotezooi meer. Precies die ene eigenschap maakt een generator daarbovenop te doen. Payloads in AsyncAPI-, JSON Schema- of OpenAPI-formaat worden als schema’s geladen; bij Avro, Protobuf of wat dan ook blijft Payload nil en vertelt UnsupportedFormat je wat er is overgeslagen.
Je remote refs haalt het niet op
Elke $ref met :// erin is een fout die je vertelt het document te downloaden en ernaar te verwijzen via het pad. Een spec-loader die achter je rug netwerkcalls doet, komt er bij mij niet in. Lokale refs en refs naar relatieve bestanden worden gewoon opgelost, elk bestand wordt één keer geladen en gecachet op absoluut pad, een ref-keten krijgt 64 hops voordat hij als cyclus geldt, en een pointer naar het niets is commerr.ErrNotFound. Die sentinels komen uit ctxerrors, dus errors.Is(err, commerr.ErrNotFound) vangt een kapotte ref, hoeveel context er ook omheen gewikkeld is. Als je spec de helft van de schema’s van een of andere URL trekt, haal die troep dan eerst lokaal binnen.