Operational guide for AI agents working in github.com/pb33f/libasyncapi.
- Purpose: AsyncAPI 3.0+ parser for Go with high-level and low-level APIs.
- Entry point:
libasyncapi.NewDocument([]byte) (Document, error). - High-level model:
doc.Model() *asyncapi.AsyncAPI. - Low-level model:
doc.GoLow() *lowasync.AsyncAPI. - AsyncAPI 2.x is intentionally unsupported and returns an error.
- This file is the single source of truth.
CLAUDE.mdshould only point here.
Run the smallest useful set first, then broaden when the change touches shared behavior.
go test ./...
go vet ./...
go build ./...For visitor changes, also run:
go test -race ./visitor/...For formatting after Go edits:
gofmt -w <changed-go-files>asyncapi.go package docs and NewDocument entry point
document.go Document interface
document_config.go parser configuration
version.go version detection and validation
errors.go public error types
datamodel/high/asyncapi/ high-level user-facing models
datamodel/low/asyncapi/ low-level YAML-aware models
datamodel/low/asyncapi/constants.go field labels
datamodel/low/asyncapi/create_document.go document builder and index setup
visitor/ visitor interfaces, walker, context helpers
test_fixtures/ integration and binding fixtures
High-level and low-level files mostly mirror each other by object name:
server.go, channel.go, operation.go, message.go, components.go,
security_scheme.go, and bindings.go.
- Prefer high-level models for normal application behavior.
- Use low-level models when line numbers, raw YAML nodes, source structure, or render fidelity matter.
- Every high-level type should expose
GoLow()andGoLowUntyped(). - Low-level fields are wrapped in
low.NodeReference[T]; useIsEmpty()to test whether a field was present. - Preserve YAML order with
orderedmap.Map[K, V]. Do not replace parsed maps with plain Go maps unless the source model already uses one intentionally. - Optional scalar fields may be pointers at the high level. Check for nil before
dereferencing, for example
channel.Address != nil. - Schemas come from
libopenapi(highbase.SchemaProxyandhighbase.Schema). Always handleproxy.Schema() == nil.
Parse a document:
spec, err := os.ReadFile("asyncapi.yaml")
if err != nil {
return err
}
doc, err := libasyncapi.NewDocument(spec)
if err != nil {
return err
}
model := doc.Model()
fmt.Println(model.Info.Title)Handle partial parses:
doc, err := libasyncapi.NewDocument(spec)
if err != nil {
return err
}
if doc.IsPartial() {
for _, parseErr := range doc.Errors() {
log.Printf("parse warning: %v", parseErr)
}
}Iterate ordered maps:
for name, channel := range model.Channels.FromOldest() {
if channel.Address != nil {
fmt.Printf("%s: %s\n", name, *channel.Address)
}
}Access source metadata:
lowModel := doc.GoLow()
fmt.Println(lowModel.Info.Value.Title.KeyNode.Line)Configure multi-file references:
config := libasyncapi.NewDocumentConfiguration()
config.BasePath = "/path/to/specs"
config.AllowFileReferences = true
config.AllowRemoteReferences = false
doc, err := libasyncapi.NewDocumentWithConfiguration(spec, config)| Need | API |
|---|---|
| High-level model | doc.Model() |
| Low-level model | doc.GoLow() |
| Version | doc.GetVersion() |
| Spec info | doc.GetSpecInfo() |
| Reference index | doc.Index() |
| Multi-file manager | doc.Rolodex() |
| Root YAML node | doc.RootNode() |
| Parse errors | doc.Errors() |
| Partial parse state | doc.IsPartial() |
Most low-level types follow this shape:
Build(ctx, keyNode, root, idx)parses from YAML.Hash()returns a content hash that is stable only within the current process. Hashes use a process-randommaphashseed and must not be persisted or compared across runs.GetRootNode()andGetKeyNode()expose YAML nodes.GetExtensions()exposesx-*extensions.GetIndex()andGetContext()preserve parser context.
Binding low-level types in datamodel/low/asyncapi/bindings.go embed
BaseBinding. Use initBuild() for shared metadata/index/context setup and
hashExtensions() at the end of binding Hash() implementations.
Use structured extraction helpers instead of ad hoc YAML traversal:
title := low.ExtractValueNode[string](ctx, "title", root)
info, err := low.ExtractObject[*Info](ctx, "info", root, idx)
servers, keyNode, valueNode, err := low.ExtractMap[*Server](ctx, "servers", root, idx)
tags, keyNode, valueNode, err := low.ExtractArray[*Tag](ctx, "tags", root, idx)- High-level
MarshalYAML()should usehigh.NewNodeBuilder(value, value.low)when low-level metadata must be preserved. - Do not patch low-level structs to "fix" high-level rendering unless parsing metadata is actually wrong.
- For binding render fidelity, add
MarshalYAML()on high-level bindings and nested helper structs that must preserve extensions, key order, or raw nodes.
Example:
func (s *SQSChannelBinding) MarshalYAML() (interface{}, error) {
nb := high.NewNodeBuilder(s, s.low)
return nb.Render(), nil
}Basic visitor:
type Recorder struct {
Paths []string
}
func (r *Recorder) Visit(ctx context.Context, node any) error {
r.Paths = append(r.Paths, visitor.Path(ctx))
return nil
}
rec := &Recorder{}
walker := visitor.NewWalker(rec)
err := walker.Walk(context.Background(), doc.Model())Context helpers:
visitor.Path(ctx)
visitor.Depth(ctx)
visitor.Stack(ctx)
visitor.Parent(ctx)
visitor.AppendPath(ctx, "segment")
visitor.AppendIndex(ctx, 5)Schema visitors can implement EnterSchema, LeaveSchema, and
SkipCircularRef. Polymorphic visitors can implement EnterAllOf,
LeaveAllOf, EnterOneOf, LeaveOneOf, EnterAnyOf, and LeaveAnyOf.
When adding a new traversable type, update visitor/walker.go and add coverage
in visitor/examples_test.go or a focused walker test.
Binding availability by protocol:
| Protocol | Server | Channel | Operation | Message |
|---|---|---|---|---|
| HTTP | HTTPServerBinding |
HTTPChannelBinding |
HTTPOperationBinding |
HTTPMessageBinding |
| Kafka | KafkaServerBinding |
KafkaChannelBinding |
KafkaOperationBinding |
KafkaMessageBinding |
| WebSocket | - | WebSocketChannelBinding |
- | - |
| AMQP | - | AMQPChannelBinding |
AMQPOperationBinding |
AMQPMessageBinding |
| MQTT | MQTTServerBinding |
- | MQTTOperationBinding |
MQTTMessageBinding |
| SQS | SQSServerBinding |
SQSChannelBinding |
SQSOperationBinding |
SQSMessageBinding |
Access pattern:
if server.Bindings != nil && server.Bindings.Kafka != nil {
kafka := server.Bindings.Kafka
fmt.Println(kafka.SchemaRegistryURL)
}SQS has typed bindings and queue helper types:
| Type | Key fields |
|---|---|
SQSServerBinding |
Extensions only |
SQSChannelBinding |
Queue, DeadLetterQueue, BindingVersion |
SQSOperationBinding |
Queues, BindingVersion |
SQSMessageBinding |
Extensions only |
SQSQueue |
Name, ARN, FifoQueue, DeduplicationScope, FifoThroughputLimit, DeliveryDelay, VisibilityTimeout, ReceiveMessageWaitTime, MessageRetentionPeriod, RedrivePolicy, Policy, Tags |
SQSRedrivePolicy |
DeadLetterQueue, MaxReceiveCount |
SQSPolicy |
Statements |
SQSPolicyStatement |
Effect, Principal, Action, Resource, Condition |
if channel.Bindings != nil && channel.Bindings.SQS != nil {
sqs := channel.Bindings.SQS
if sqs.Queue != nil {
fmt.Println(sqs.Queue.Name)
fmt.Println(sqs.Queue.VisibilityTimeout)
}
}
if op.Bindings != nil && op.Bindings.SQS != nil {
for _, queue := range op.Bindings.SQS.Queues {
fmt.Println(queue.Name)
}
}SQS policy Principal, Action, Resource, and Condition fields are raw
*yaml.Node values because the binding allows scalar, sequence, or mapping
forms. Inspect the node shape or marshal the node when typed access is needed.
The walker visits SQS bindings at paths like /channels/<name>/bindings/sqs
and /operations/<name>/bindings/sqs. Queue, redrive policy, and policy helper
objects are data on the binding and are not walked as separate visitor nodes.
Use nearby objects as the primary template. The shortest safe checklist is:
- Add labels in
datamodel/low/asyncapi/constants.go. - Add the low-level type with
Build,Hash, metadata accessors, and extension handling. - Add the high-level type with a
NewType(low *lowasync.Type)constructor,GoLow(),GoLowUntyped(),Render(), andMarshalYAML()when render fidelity matters. - Wire the type into its parent low-level
Build()and high-level constructor. - Update
visitor/walker.goif the type should be traversed. - Add tests for high-level fields, low-level metadata, hashes where relevant, rendering, and visitor paths when applicable.
Bindings have extra wiring:
- Add protocol and field labels in
constants.go. - Add low-level binding structs in
datamodel/low/asyncapi/bindings.go. - Add high-level binding structs and constructors in
datamodel/high/asyncapi/bindings.go. - Add fields to the relevant containers:
ServerBindings,ChannelBindings,OperationBindings, andMessageBindings. - Update
visitor/walker.goso the binding is visited under/.../bindings/<protocol>. - Add or extend
bindings_test.go. - Add representative examples to
test_fixtures/comprehensive-bindings.yaml.
For bindings with nested helper types or extensions, include render round-trip coverage. SQS bindings are the reference pattern for that round-trip wiring.
test_fixtures/streetlights-kafka.yaml: full Kafka example with bindings.test_fixtures/multi-protocol.yaml: HTTP, Kafka, and WebSocket.test_fixtures/comprehensive-bindings.yaml: binding coverage, including SQS queue/redrive/policy cases.test_fixtures/shared-message.yaml: component references.
- Direct map iteration loses YAML order. Use
.FromOldest()or.FromNewest(). - Optional high-level pointer fields can be nil.
- Low-level fields need
IsEmpty()checks. - Schema resolution can fail; check
proxy.Schema() != nil. - Do not mutate low-level models as application state. Treat them as parsed source metadata.
- Do not bypass tests or pre-commit hooks. Fix the underlying issue.
Rules for any AI agent working in this repo:
- Keep changes scoped to the request and existing code patterns.
- Preserve user edits already present in the worktree.
- Prefer focused tests first, then broader tests when behavior is shared.
- If verification cannot be run, say exactly why.
- Use
apply_patchfor manual file edits. - Do not run destructive git commands unless explicitly requested.
- Do not commit unless the user explicitly asks for a commit.
- When editing docs, keep text ASCII unless the file already requires otherwise.
- Report changed files and verification in the final response.
- Claude should read this file through the repository
CLAUDE.mdredirect. - Claude in the pb33f org is not permitted to run
git commit; stage changes and leave committing to the user.