Skip to content

AsyncAPI Spec Generation

See also: render/asyncapi/v3 on pkg.go.dev · api/events on pkg.go.dev

Runnable demos: examples/event-driven · examples/api-events

go-codex generates AsyncAPI 3.0 (and 2.6) documents from the same codec definitions that drive runtime decode/encode — no separate YAML authoring, no drift.

The api/events builder generates a complete AsyncAPI 3.0 document from all registered channels:

import (
    "github.com/DaniDeer/go-codex/api/events"
    "github.com/DaniDeer/go-codex/validate"
)

b := events.NewBuilder(
    events.Info{Title: "User Events", Version: "1.0.0"},
    events.WithTopicConstraints(validate.MQTTPublishTopic),
)
b.AddServer("production", events.Server{
    URL:      "broker.example.com",
    Protocol: "mqtt",
})

// Register a static topic channel
userCreated, _ := events.NewChannel[UserCreatedEvent]("user/created", userCreatedCodec,
    events.Subscribe{
        OperationID: "receiveUserCreated",
        Summary:     "A user was created",
        SchemaName:  "UserCreatedEvent",  // → $ref in spec
    },
).Register(b)

// Register a template topic channel with parameter
sensorUUIDCodec := codex.String().Refine(validate.UUID)
sensorMeasurement, _ := events.NewChannel[Measurement]("sensors/{sensorID}/measurements",
    measurementCodec,
    events.Subscribe{
        OperationID: "receiveSensorMeasurement",
        Summary:     "Sensor measurement received",
        SchemaName:  "Measurement",
    },
    events.TopicParam{
        Name:        "sensorID",
        Description: "UUID of the sensor publishing the measurement.",
    }.WithCodec(sensorUUIDCodec),  // codec validates + flows schema into spec parameters:
).Register(b)

// Register both directions on same channel
events.NewChannel[UserEvent]("user/events", codec,
    events.Subscribe{OperationID: "receiveUserEvent", Summary: "Receive user events"},
    events.Publish{OperationID: "sendUserEvent",    Summary: "Send user events"},
).Register(b)

// Generate the full AsyncAPI 3.0 spec:
doc, err := b.AsyncAPISpec()
yamlBytes, _ := doc.MarshalYAML()

The output follows AsyncAPI 3.0: separate channels and operations top-level keys, with action: receive / action: send:

asyncapi: 3.0.0
channels:
  sensors/{sensorID}/measurements:
    address: sensors/{sensorID}/measurements
    parameters:
      sensorID:
        schema: {type: string, format: uuid}
    messages:
      Measurement:
        payload: {$ref: '#/components/schemas/Measurement'}
operations:
  receiveSensorMeasurement:
    action: receive
    channel: {$ref: '#/channels/sensors/{sensorID}/measurements'}

Using the low-level v3 DocumentBuilder directly

For cases where you build channel descriptors manually (without api/events):

import "github.com/DaniDeer/go-codex/render/asyncapi/v3"

doc, err := v3.NewDocumentBuilder(v3.Info{
    Title:   "User Events",
    Version: "1.0.0",
}).
    AddServer("production", v3.Server{
        URL:      "broker.example.com",
        Protocol: "amqp",
    }).
    AddChannel("userCreated", v3.ChannelItem{
        Address: "user/created",
        Subscribe: &v3.Operation{
            Summary: "User created",
            Message: v3.Message{
                Schema:     UserCreatedEventCodec.Schema,
                SchemaName: "UserCreatedEvent", // → $ref + registered in components
            },
        },
    }).
    Build()

yamlBytes, _ := doc.MarshalYAML()

AsyncAPI 2.6 (frozen)

render/asyncapi/v2 is preserved for existing users who want 2.6 output. Use it directly via DocumentBuilder:

import "github.com/DaniDeer/go-codex/render/asyncapi/v2"

doc, err := v2.NewDocumentBuilder(v2.Info{Title: "User Events", Version: "1.0.0"}).
    AddServer("production", v2.Server{URL: "broker.example.com", Protocol: "amqp"}).
    AddChannel("user/created", v2.ChannelItem{
        Subscribe: &v2.Operation{
            Summary: "User created",
            Message: v2.Message{
                Schema:     UserCreatedEventCodec.Schema,
                SchemaName: "UserCreatedEvent",
            },
        },
    }).
    Build()

yamlBytes, _ := doc.MarshalYAML()

Builder options

Option Effect
WithTopicCodec(c) Validates topics against codec c at Register time
WithTopicConstraints(cs...) Validates topics against one or more constraints at Register time

{varName} placeholders are replaced with x before validation, so topic constraints work correctly on template topics.

Security schemes (AsyncAPI)

b.AddSecurityScheme("bearerAuth", events.SecurityScheme{
    SecurityScheme: route.BearerScheme("JWT"),
}.WithCodec(codex.String().Refine(validate.BearerToken)))

userCreated, _ := events.NewChannel[UserCreatedEvent]("user/created", codec,
    events.Subscribe{
        Summary:  "Receive user created events",
        Security: []route.SecurityRequirement{route.Require("bearerAuth")},
    },
).Register(b)

Error types

Error When returned
events.InvalidTopicError{Topic, Err} Topic fails builder-level validation
events.TopicParamError{Name, Value, Err} Topic variable value fails its codec
events.MissingTopicVarError{Name} Template variable absent from vars map
events.InvalidTopicParamError{Name, Topic} TopicParams entry names a variable not in the template

See also