Code structure

Generated code design #

The code generated by go-asyncapi roughly follows the AsyncAPI specification structure, but it is not a 1:1 mapping. On the figure below it’s shown what the code looks like from high level.

Code structure overview

The structure of types is shown below.

Types structure diagram

Roughly speaking, the generated code consists of several parts:

  • Go types that are generated separately for every protocol it is bound to (channel, operation, etc.)
  • Go types that are generated once (message, model, etc.)
  • Implementation code for every protocol used in the AsyncAPI document

This reminds the Bridge pattern. It has an abstraction layer, that follows the AsyncAPI entities, and an implementation layer, that can be selected from the built-in implementations in tool’s configuration or be provided by the user. These layers are isolated from each other via Go interfaces.

Let’s take a look what these objects are for:

  • Server. Keeps the information defined in document (bindings, URL, etc.) to open channels\operations to this server. The Server implements only one protocol.
  • Channel. It is used to send\receive envelopes using protocol publisher\subscriber, and to seal a protocol-agnostic message into a protocol-specific envelope and back. One type per protocol of every server the Channel bounds to, keeping the information defined in document (bindings, parameters, etc.).
  • Operation. In fact, this is a wrapper around the Channel implementing a part of its functionality, but with Operation-specific logic in opening and message handling. One type per protocol of every server the Operation bounds to, keeping the information defined in document (bindings, bound messages, etc.).
  • Message. Protocol-agnostic message with payload data, metadata, etc. Implements the marshalling/unmarshalling logic to\from Envelopes of every protocol it is bound to. Also, Message keeps the information defined in document (bindings, etc.).
  • Schema. is a general purpose type crafted from jsonschema defined in components.schemas AsyncAPI section. Can be referred by any other entity.

Server, Channel, Operation and Message may be declared in root section of the AsyncAPI document (e.g. servers) and in the components section (e.g. components.servers).

The code generates only for the first case. components section defines reusable parts, so produces the code only when these parts are referenced, otherwise they are ignored.

Every Implementation has its own set of types, that are used to interact with the message broker. There are:

  • Producer/Consumer. Represents the network connection to the message broker, but not used to send or receive messages. The main purpose is to open Publisher/Subscriber within an opened connection.
  • Publisher/Subscriber. Represents a data channel inside a network connection. It is used to send and receive messages
  • Envelope. A Message marshalled into a protocol data. Usually, Envelope is the concrete Go struct, that represents a message in an Implementation library.

The reason of separating the connection creation and channel creation process is the fact, that some protocols (like Kafka) allow to open multiple channels (produce/consume topics) within a single connection or a client object. Other protocols (like WebSocket) treat a connection as a channel. Moreover, different libraries for the same protocol may have their own specifics in how they open channels and connections.

So, this approach helps to abstract from these details.

Runtime package #

The only external dependency required in the generated code (except for the protocol implementations code) is a small package called the runtime package. By default, it is github.com/bdragon300/go-asyncapi/run, which is configurable in the go-asyncapi configuration file.

Runtime package is a part of go-asyncapi project. It contains the interfaces and types that are used in the generated code, and also some utility functions.

The reason this package is external to the generated code is to reduce the amount of generated code and simplify the code generation process.

Design principles #

The requirements for the code generated by go-asyncapi are not significantly different from the requirements for the code generated by other tools:

  • The code should be easy to read, maintainable, flexible, modular, and easy to integrate into existing projects
  • The result must be idempotent – it must not change if the same AsyncAPI document is processed multiple times
  • It should follow the Go conventions and idioms, be compatible with stdlib Go interfaces and types
  • Minimal or zero external dependencies
  • Performance optimizations are preferable
  • The codegen process should be easy to run (including inside CI/CD pipelines) and to be configurable on different levels (document, configuration file, command line, etc.)

Other than that, there are some specific requirements for go-asyncapi.

The first one is that because the complex environments may be described in dozens of AsyncAPI documents referenced to each other, we should track these dependencies and reflect them in the resulting code.

Finally, the generated code should be functional without additional user’s effort (except that he should provide the connection settings). This speeds up the prototyping, testing and gives a good start to write an application for simple use cases. However, this behavior should be optional.