go-asyncapi lee documentos AsyncAPI 3 en Go y te da servidores, canales, operaciones y mensajes como structs normales y corrientes, con cada $ref seguido de un archivo a otro, cada trait fusionado y todo en el orden en que lo lista el documento, no en el que les dé la gana hoy a los maps de Go. No genera código y no juzga cómo llevas tu vida. Lo escribí porque todas las opciones en Go que encontré o seguían atascadas en AsyncAPI 2 o eran un puto generador entero que quería adueñarse de la estructura de mi proyecto, y yo solo quería la parte de carga, sin nada más. El generador vive aparte en gen-asyncapi, la misma separación que oapixconstgen en el lado de OpenAPI.
Instálalo, carga una spec
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 rechaza cualquier cosa cuyo campo asyncapi no empiece por 3., y luego carga servidores, canales, operaciones y componentes en ese orden, porque las operaciones apuntan a canales y los canales tienen que existir antes. Operation.Channel es un *Channel de verdad, no un string que buscas más tarde. Action es send o receive, y cualquier otra cosa es un commerr.ErrInvalidValue. server.URL() rellena cada {placeholder} con su valor por defecto, así que dejas de destrozar strings a mano.
Lo que de verdad me importa: los mensajes se identifican por el sitio donde vive su definición. Un mensaje referenciado desde tres canales y dos operaciones es un solo *Message, no cinco copias que te toca deduplicar como un cavernícola. Los traits se fusionan en el orden de la lista, al estilo JSON Merge Patch, sin pisar nunca lo que el mensaje ya define, y un $ref dentro del payload de un trait se resuelve respecto al archivo del propio trait, no al tuyo.
Recorrer el esquema de un payload
Los esquemas se quedan en crudo, como árboles *asyncapi.Object que conservan el orden de las claves: Keys []string más Values map[string]any. Get, String y Object son nil-safe, así que si encadenas hasta una clave que no existe te llega nil en vez de un panic. Las refs dentro de un esquema pasan por el Resolver del documento:
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 devuelve el valor más su Location, o sea, un archivo y un JSON pointer. Location es un struct comparable, así que dos refs que caen en el mismo esquema te dan la misma clave de map, y emitir cada tipo exactamente una vez deja de ser un puto desmadre. Esa sola propiedad es lo que hace que montar un generador encima no sea una locura. Los payloads en formato AsyncAPI, JSON Schema u OpenAPI se cargan como esquemas; con Avro, Protobuf o cualquier otra cosa, Payload se queda en nil y UnsupportedFormat te dice qué se ha saltado.
No va a descargar tus refs remotas
Cualquier $ref con :// dentro es un error que te dice que descargues el documento y lo referencies por su ruta. No pienso dejar que un cargador de specs haga llamadas de red a tus espaldas. Las refs locales y a archivos relativos se resuelven sin problema, cada archivo se carga una sola vez y se cachea por ruta absoluta, una cadena de refs tiene 64 saltos antes de que se la declare ciclo, y un puntero a la nada es commerr.ErrNotFound. Esos sentinels vienen de ctxerrors, así que errors.Is(err, commerr.ErrNotFound) pilla una ref rota por mucho contexto que le hayan envuelto alrededor. Si tu spec saca la mitad de sus esquemas de alguna URL, bájate esa mierda en local primero.