Skip to content

sda-commons-async-api

Experimental

Please be aware that SDA SE is likely to change or remove this artifact in the future

This module contains the AsyncApiGenerator to generate AsyncAPI specs from a template and model classes in a code first approach. The AsyncAPI specification is the industry standard for defining asynchronous APIs.

Usage

If the code first approach is used to create an AsyncAPI spec this module provides assistance. The suggested way to use this module is:

  • A template file defining the general information, channels and components.messages using the AsyncAPI spec. components.schemas should be omitted.
  • The schema is defined and documented as Java classes in the code as they are used in message handlers and consumers. Jackson, Jakarta Validation and Swagger 2 annotations can be used for documentation.
  • The root classes of messages are referenced in components.messages.YourMessage.payload.$ref as class://your.package.MessageModel.
  • The AsyncApiGenerator is used to combine the template and the generated Json Schema of the models to a self-contained spec file.
  • The generated AsyncAPI spec is committed into source control. This way, the commit history will show intended and unintended changes to the API and the API spec is accessible any time without executing any code.
  • The API can be view in AsyncAPI Studio.

It is suggested to use it as a test dependency, build the AsyncAPI in a unit test and verify that it is up-to-date. The GoldenFileAssertions from the test module help here.

Example: Build AsyncAPI for Cars

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
asyncapi: '2.5.0'
id: 'urn:org:sdase:example:cars'
defaultContentType: application/json

info:
  title: Cars Example
  description: This example demonstrates how to define events around *cars*.
  version: '1.0.0'

channels:
  'car-events':
    publish:
      operationId: publishCarEvents
      summary: Car related events
      description: These are all events that are related to a car
      message:
        oneOf:
          - $ref: '#/components/messages/CarManufactured'
          - $ref: '#/components/messages/CarScrapped'

components:
  messages:
    CarManufactured:
      title: Car Manufactured
      description: An event that represents when a new car is manufactured
      payload:
        # referencing the full name of the Class
        $ref: 'class://org.sdase.commons.spring.boot.asyncapi.test.data.models.CarManufactured'
    CarScrapped:
      title: Car Scrapped
      description: An event that represents when a car is scrapped
      payload:
        # referencing the full name of the Class
        $ref: 'class://org.sdase.commons.spring.boot.asyncapi.test.data.models.CarScrapped'
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/*
 * Copyright 2022- SDA SE Open Industry Solutions (https://www.sda.se)
 *
 * Use of this source code is governed by an MIT-style
 * license that can be found in the LICENSE file or at
 * https://opensource.org/licenses/MIT.
 */
package org.sdase.commons.spring.boot.asyncapi.test.data.models;

import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.time.Instant;

@Schema(title = "Car manufactured", description = "A new car was manufactured")
@SuppressWarnings("unused")
public class CarManufactured extends BaseEvent {

  @NotBlank
  @Schema(description = "The registration of the vehicle", example = "BB324A81")
  private String vehicleRegistration;

  @NotNull
  @JsonPropertyDescription("The time of manufacturing")
  private Instant date;

  @NotNull
  @JsonPropertyDescription("The model of the car")
  private CarModel model;

  public String getVehicleRegistration() {
    return vehicleRegistration;
  }

  public CarManufactured setVehicleRegistration(String vehicleRegistration) {
    this.vehicleRegistration = vehicleRegistration;
    return this;
  }

  public Instant getDate() {
    return date;
  }

  public CarManufactured setDate(Instant date) {
    this.date = date;
    return this;
  }

  public CarModel getModel() {
    return model;
  }

  public CarManufactured setModel(CarModel model) {
    this.model = model;
    return this;
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/*
 * Copyright 2022- SDA SE Open Industry Solutions (https://www.sda.se)
 *
 * Use of this source code is governed by an MIT-style
 * license that can be found in the LICENSE file or at
 * https://opensource.org/licenses/MIT.
 */
package org.sdase.commons.spring.boot.asyncapi.test.data.models;

import com.fasterxml.jackson.annotation.JsonClassDescription;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.time.Instant;

@JsonClassDescription("A car was scrapped")
@SuppressWarnings("unused")
public class CarScrapped extends BaseEvent {

  @NotBlank
  @JsonPropertyDescription("The registration of the vehicle")
  @Schema(example = "BB324A81")
  private String vehicleRegistration;

  @NotNull
  @JsonPropertyDescription("The time of scrapping")
  private Instant date;

  @JsonPropertyDescription("The location where the car was scrapped")
  private String location;

  public String getVehicleRegistration() {
    return vehicleRegistration;
  }

  public CarScrapped setVehicleRegistration(String vehicleRegistration) {
    this.vehicleRegistration = vehicleRegistration;
    return this;
  }

  public Instant getDate() {
    return date;
  }

  public CarScrapped setDate(Instant date) {
    this.date = date;
    return this;
  }

  public String getLocation() {
    return location;
  }

  public CarScrapped setLocation(String location) {
    this.location = location;
    return this;
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/*
 * Copyright 2022- SDA SE Open Industry Solutions (https://www.sda.se)
 *
 * Use of this source code is governed by an MIT-style
 * license that can be found in the LICENSE file or at
 * https://opensource.org/licenses/MIT.
 */
package org.sdase.commons.spring.boot.asyncapi;

import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.junit.jupiter.api.Test;
import org.sdase.commons.spring.boot.web.testing.GoldenFileAssertions;

class AsyncApiTest {

  @Test
  void generateAndVerifySpec() throws IOException {
    // get template
    var template = getClass().getResource("/demo/asyncapi_template.yaml");
    // generate AsyncAPI yaml
    String expected = AsyncApiGenerator.builder().withAsyncApiBase(template).generateYaml();
    // specify where to store the result, e.g. Path.of("asyncapi.yaml") for the project root.
    Path filePath = Paths.get("asyncapi.yaml");
    // check and update the file
    GoldenFileAssertions.assertThat(filePath).hasYamlContentAndUpdateGolden(expected);
  }
}
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
---
asyncapi: "2.5.0"
id: "urn:org:sdase:example:cars"
defaultContentType: "application/json"
info:
  title: "Cars Example"
  description: "This example demonstrates how to define events around *cars*."
  version: "1.0.0"
channels:
  car-events:
    publish:
      operationId: "publishCarEvents"
      summary: "Car related events"
      description: "These are all events that are related to a car"
      message:
        oneOf:
        - $ref: "#/components/messages/CarManufactured"
        - $ref: "#/components/messages/CarScrapped"
components:
  messages:
    CarManufactured:
      title: "Car Manufactured"
      description: "An event that represents when a new car is manufactured"
      payload:
        $ref: "#/components/schemas/CarManufactured"
    CarScrapped:
      title: "Car Scrapped"
      description: "An event that represents when a car is scrapped"
      payload:
        $ref: "#/components/schemas/CarScrapped"
  schemas:
    CarManufactured:
      allOf:
      - type: "object"
        properties:
          id:
            type: "string"
            minLength: 1
            pattern: "^.*\\S+.*$"
            description: "The id of the message"
            examples:
            - "626A0F21-D940-4B44-BD36-23F0F567B0D0"
          type:
            allOf:
            - $ref: "#/components/schemas/Type"
            - description: "The type of message"
          vehicleRegistration:
            type: "string"
            minLength: 1
            pattern: "^.*\\S+.*$"
            description: "The registration of the vehicle"
            examples:
            - "BB324A81"
          date:
            type: "string"
            format: "date-time"
            description: "The time of manufacturing"
          model:
            allOf:
            - $ref: "#/components/schemas/CarModel"
            - description: "The model of the car"
        required:
        - "id"
        - "vehicleRegistration"
        - "date"
        - "model"
        title: "Car manufactured"
        description: "A new car was manufactured"
      - type: "object"
        properties:
          type:
            const: "CAR_MANUFACTURED"
        required:
        - "type"
    CarModel:
      anyOf:
      - $ref: "#/components/schemas/Electrical"
      - $ref: "#/components/schemas/Combustion"
    CarScrapped:
      allOf:
      - type: "object"
        properties:
          id:
            type: "string"
            minLength: 1
            pattern: "^.*\\S+.*$"
            description: "The id of the message"
            examples:
            - "626A0F21-D940-4B44-BD36-23F0F567B0D0"
          type:
            allOf:
            - $ref: "#/components/schemas/Type"
            - description: "The type of message"
          vehicleRegistration:
            type: "string"
            minLength: 1
            pattern: "^.*\\S+.*$"
            description: "The registration of the vehicle"
            examples:
            - "BB324A81"
          date:
            type: "string"
            format: "date-time"
            description: "The time of scrapping"
          location:
            type: "string"
            description: "The location where the car was scrapped"
        required:
        - "id"
        - "vehicleRegistration"
        - "date"
        description: "A car was scrapped"
      - type: "object"
        properties:
          type:
            const: "CAR_SCRAPPED"
        required:
        - "type"
    Combustion:
      allOf:
      - type: "object"
        properties:
          name:
            type: "string"
            description: "The name of the car model"
            examples:
            - "Tesla Roadster"
          engineType:
            type: "string"
            description: "The type of engine"
          tankVolume:
            type: "integer"
            description: "The capacity of the tank in liter"
            examples:
            - 95
        required:
        - "tankVolume"
        title: "Combustion engine"
        description: "An car model with a combustion engine"
      - type: "object"
        properties:
          engineType:
            const: "COMBUSTION"
        required:
        - "engineType"
    Electrical:
      allOf:
      - type: "object"
        properties:
          name:
            type: "string"
            description: "The name of the car model"
            examples:
            - "Tesla Roadster"
          engineType:
            type: "string"
            description: "The type of engine"
          batteryCapacity:
            type: "integer"
            description: "The capacity of the battery in kwH"
            examples:
            - 200
        required:
        - "batteryCapacity"
        title: "Electrical engine"
        description: "An car model with an electrical engine"
      - type: "object"
        properties:
          engineType:
            const: "ELECTRICAL"
        required:
        - "engineType"
    Type:
      type: "string"
      enum:
      - "CAR_MANUFACTURED"
      - "CAR_SCRAPPED"

Usage with Existing Schemas

In some cases it is not possible to generate a schema with appropriate documentation, e.g. when a framework requires to use classes from dependencies that do not contain the expected annotations.

In this case the schema may be added to the template. This should be used as fallback only, because the schema is not connected to the actual code, it may diverge over time.

Example: Build AsyncAPI with handcrafted schema

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
asyncapi: '2.5.0'
id: 'urn:org:sdase:example'
defaultContentType: application/json

info:
  title: Example
  description: This example demonstrates how to define messages with hand crafted schemas.
  version: '1.0.0'

channels:
  'car-events':
    publish:
      summary: An entity stream
      description: What happens to an entity
      message:
        oneOf:
          - $ref: '#/components/messages/Created'
          - $ref: '#/components/messages/Deleted'

components:
  messages:
    Created:
      title: Entity created
      payload:
        # referencing the full name of the Class
        $ref: 'class://org.sdase.commons.spring.boot.asyncapi.test.data.models.Created'
    Deleted:
      title: Entity deleted
      description: Deletion of the entity is represented by an external tombstone message.
      payload:
        # referencing the existing schema
        $ref: '#/components/schemas/Tombstone'

  schemas:
    Tombstone:
      type: object
      description: |
        The tombstone event is published to indicate that the entity has been deleted.
        All copies of data related to the entity must be deleted.
      properties:
        id:
          type: string
        tombstone:
          type: boolean
          const: true
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/*
 * Copyright 2022- SDA SE Open Industry Solutions (https://www.sda.se)
 *
 * Use of this source code is governed by an MIT-style
 * license that can be found in the LICENSE file or at
 * https://opensource.org/licenses/MIT.
 */
package org.sdase.commons.spring.boot.asyncapi.test.data.models;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;

@SuppressWarnings("unused")
public class Created {

  @NotNull
  @Pattern(regexp = "[a-zA-Z0-9-_]{10,}")
  private String id;

  @NotBlank private String name;

  public String getId() {
    return id;
  }

  public Created setId(String id) {
    this.id = id;
    return this;
  }

  public String getName() {
    return name;
  }

  public Created setName(String name) {
    this.name = name;
    return this;
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/*
 * Copyright 2022- SDA SE Open Industry Solutions (https://www.sda.se)
 *
 * Use of this source code is governed by an MIT-style
 * license that can be found in the LICENSE file or at
 * https://opensource.org/licenses/MIT.
 */
package org.sdase.commons.spring.boot.asyncapi;

import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.junit.jupiter.api.Test;
import org.sdase.commons.spring.boot.web.testing.GoldenFileAssertions;

class ApiWithSchemaTest {

  @Test
  void generateAndVerifySpec() throws IOException {
    // get template
    var template = getClass().getResource("/demo/template_with_schema.yaml");
    // generate AsyncAPI yaml
    String expected = AsyncApiGenerator.builder().withAsyncApiBase(template).generateYaml();
    // specify where to store the result, e.g. Path.of("asyncapi.yaml") for the project root.
    Path filePath = Paths.get("asyncapi-schema.yaml");
    // check and update the file
    GoldenFileAssertions.assertThat(filePath).hasYamlContentAndUpdateGolden(expected);
  }
}
1

Generating Schema Files

If desired, the module also allows to generate the JSON schema files, for example to use them to validate test data. Please take a look at JsonSchemaBuilder and it's implementations.