This workshop aims to:

During this workshop we will use different tools, practices and languages:

Getting involved?

The source code is available on GitHub.

Feel free to raise any issues or participate if you want!

Main functionalities

This platform exposes a set of API providing the following functionalities:

Guitar management

Handle the stock of guitars. The data is stored into a PostgreSQL database and exposed through the following API:

Here is a sample of one guitar entity:

  {
  "guitarId": "831a3019-6be0-4e26-b904-a97d6608e6ca",
  "name": "Les Paul",
  "type": "ELECTRIC",
  "price": 3000,
  "stock": 2
}

Order Requests management

We create an orderRequest of one or many guitars and may ask for a discount. It will be challenged during the quote creation.

The data is then stored into the PostgreSQL database too and exposed through the following API:

Here is a sample of one orderRequest entity:

 {
  "orderId": "a4c25cca-37e5-47eE-D052-A27f0b8eA8ab",
  "guitarIds": [
    "CE8eDEaA-Ce12-6D18-7C35-1887a60514Ae"
  ],
  "discountRequested": 100,
  "createdAt": "2022-03-10T12:15:50-04:00"
}

Quote creation

After the orderRequest is stored, it's time to create a quote and provide it to our customers.

First and foremost, to check if the discount is fair, the system requests theEBay Browse API to pinpoint what is the current price of this guitar on the market. Then, if the stock is too low, the system broadcasts automatically a new command to the supply chain backoffice through a Kafka Topic.

As above, the data is stored then into the PostgreSQL database and exposed through the following API:

Finally, here is a sample of a quote:

{
  "orderId": "1711642e-e6be-406f-9a90-0be03f755ca8",
  "discount": 10,
  "totalPriceWithDiscount": 10,
  "createdAt": "2022-03-10T12:15:50-04:00"
}

High level design

Context View

Context View

  1. Our platform reaches the EBAY Browse API to challenge the discount requested by the customer against the current price of the market on EBay.
  2. When asking for a quote, if the current stock is too low (i.e., comparing to a threshold), it automatically broadcasts a message to a Supply Chain Back Office through Kafka.

Container View

Context View

The business logic is implemented in a good old monolith built on a light Hexagonal Architecture way.

To cut long story short, here is a short explanation of the packaging:

  1. The API is located in the info.touret.guitarheaven.application
src/main/java/info/touret/guitarheaven/application/
├── dto
│   ├── GuitarDto.java
│   ├── LinksDto.java
│   ├── OrderRequestDto.java
│   ├── PageableGuitarDto.java
│   └── QuoteDto.java
├── mapper
│   ├── ExceptionMapper.java
│   ├── GuitarMapper.java
│   ├── OrderRequestMapper.java
│   └── QuoteMapper.java
├── PaginationLinksFactory.java
└── resource
    ├── GuitarResource.java
    ├── OrderRequestResource.java
    └── QuoteResource.java

  1. The business logic is implemented in the info.touret.guitarheaven.domain
src/main/java/info/touret/guitarheaven/domain/
├── exception
│   ├── EntityNotFoundException.java
├── model
│   ├── Guitar.java
│   ├── OrderRequest.java
│   ├── Page.java
│   └── Quote.java
├── port
│   ├── GuitarPort.java
│   ├── OrderRequestPort.java
│   ├── QuotePort.java
│   ├── SupplierCatalogPort.java
│   └── SupplyChainPort.java
└── service
    ├── DiscountService.java
    ├── GuitarService.java
    ├── OrderRequestService.java
    └── QuoteService.java

  1. The connection to the different backends (databases, message brokers, external API ) is implemented in the info.touret.guitarheaven.infrastructure
src/main/java/info/touret/guitarheaven/infrastructure/
├── database
│   ├── adapter
│   │   ├── GuitarDBAdapter.java
│   │   ├── OrderRequestDBAdapter.java
│   │   └── QuoteDBAdapter.java
│   ├── entity
│   │   ├── GuitarEntity.java
│   │   ├── OrderRequestEntity.java
│   │   └── QuoteEntity.java
│   ├── mapper
│   │   ├── GuitarEntityMapper.java
│   │   ├── OrderRequestEntityMapper.java
│   │   └── QuoteEntityMapper.java
│   └── repository
│       ├── GuitarRepository.java
│       ├── OrderRequestRepository.java
│       └── QuoteRepository.java
├── ebay
│   ├── EbayClient.java
│   ├── EbayDiscounterAdapter.java
│   ├── Image.java
│   ├── ItemSummary.java
│   ├── Price.java
│   └── SearchPagedCollection.java
└── kafka
    ├── GuitarRequestDeserializer.java
    ├── GuitarRequest.java
    ├── GuitarRequestSerializer.java
    ├── KafkaClient.java
    └── SupplyChainAdapter.java

Quote creation workflow

You can check out the whole workflow below:

Quote Sequence

Skills

Skill

Level

Java

novice

Maven

novice

Quarkus

novice

REST API

proficient

Icons & Conventions

Before starting, we will use the following icons during the workshop. Let us check their meaning:

🛠️ An action to perform,
📝 A file to modify,
👀 Something to observe,
✅ Validate something,
ℹ️ Some information.

Tools

If you want to execute this workshop on your desktop with DevContainers

I stored a configuration to set the project up in DevContainer. You can check it out in the project .devcontainer/devcontainer.json file.

If you want to know more about DevContainers, you can check out this documentation.

You MUST have set up these tools first:

🛠️ You can validate your environment running these commands:

Docker

$ docker version
    Client:
    Docker Engine -Community
    Version:
    27.4.1
    API version:1.47
    Go version:go1.22.10
    Git commit:b9d17ea
    Built:Tue Dec 17 15:45:46 2024
    OS/Arch:linux/amd64
    Context:default

If you want to execute this workshop on your desktop (without DevContainer)

You MUST have set up these tools first:

🛠️ You can validate your environment running these commands:

Java

$ java -version
    openjdk version "21.0.1" 2023 - 10 - 17 LTS
    OpenJDK Runtime Environment Temurin-21.0.1 + 12 (build 21.0.1 + 12 - LTS)
    OpenJDK 64-Bit Server VM Temurin-21.0.1+12(build21.0.1+12-LTS,mixed mode,sharing)

Maven

$ mvn --version
    Apache Maven 3.9.9 (8e8579a9e76f7d015ee5ec7bfcdc97d260186937)
    Maven home:/home/alexandre/.sdkman/candidates/maven/current
    Java version:21.0.4,vendor:Eclipse Adoptium,runtime:/home/alexandre/.sdkman/candidates/java/21.0.4-tem
    Default locale:en,platform encoding:UTF-8
    OS name:"linux",version:"5.15.153.1-microsoft-standard-wsl2",arch:"amd64",family:"unix"

Docker

$ docker version
    Client:
    Docker Engine -Community
    Version:
    27.4.1
    API version:1.47
    Go version:go1.22.10
    Git commit:b9d17ea
    Built:Tue Dec 17 15:45:46 2024
    OS/Arch:linux/amd64
    Context:default

If you don't want to bother with a local setup It's strongly recommended to use Github Codespaces. You must create an account first and fork this repository.

You can then open this project in either your local VS Code or directly in your browser.

For the rest of this workshop, I will assume you will use GitHub Codespaces.

🛠 Open Github Codespaces

start codespace

When a messages invites you making a URL public, select and validate it.

Wait until the codespace is ready.

build codespace

During the first startup, the maven build is automatically started. Please wait until it is finished.

🛠 Start the app

In a new terminal, start the Quarkus Dev environment:

$ ./mvnw quarkus:dev

👀 Wait a while until you get the following output:

2024 - 12 - 26 22:27:42,814INFO[io.quarkus](Quarkus Main Thread)guitar-heaven1.0.0-SNAPSHOT on JVM(powered by Quarkus3.17.4)started in27.006s.Listening on:http://localhost:8080
    2024-12-26 22:27:42,815INFO[io.quarkus](Quarkus Main Thread)Profile dev activated.Live Coding activated.
    2024-12-26 22:27:42,816INFO[io.quarkus](Quarkus Main Thread)Installed features:[agroal,cdi,hibernate-orm,hibernate-orm-panache,hibernate-validator,jdbc-postgresql,kafka-client,messaging,messaging-kafka,microcks,narayana-jta,rest,rest-client,rest-client-jackson,rest-jackson,resteasy-problem,smallrye-context-propagation,smallrye-openapi,swagger-ui,vertx]

ℹ️ All the stack is provided through the Quarkus Dev Services. You don't therefore have to bother yourself about setting it up.

✅ Now validate your setup browsing the Quarkus DEV-UI.

Go to the VS Code Port panel.

Select the port tab:

Port VSCODE

And now, go the URL which exposes the 8080 port:

start-8080

and add the /q/dev-ui suffix.

For instance: https://laughing-giggle-x5x4rqxpwfv5pj-8080.app.github.dev/q/dev-ui

ℹ️ You can also browse the dev-ui to the Extensions>SmallRye OpenAPI.

Small Rye

👀 From a user perspective

Click then to SwaggerUI or go to the /q/swagger-ui/ URI.

For instance: https://laughing-giggle-x5x4rqxpwfv5pj-8080.app.github.dev/q/swagger-ui.

Under the Hood

👀 Check out the API located in the info.touret.guitarheaven.application.resource package. Look into how the API documentation is generated.

tree src/main/java/info/touret/guitarheaven/application/
src/main/java/info/touret/guitarheaven/application/
└── resource
    ├── GuitarResource.java
    ├── OrderResource.java
    └── QuoteResource.java

In this projet, we used the Microprofile OpenAPI annotations.

For instance:

@Operation(summary = "Gets all guitars")
@APIResponse(responseCode = "200", description = "Success ")
@APIResponse(responseCode = "500", description = "Server unavailable")
@GET
public List<GuitarDto> retrieveAllGuitars() {
    return guitarMapper.toGuitarsDto(guitarService.findAllGuitars());
}

👀 Pinpoint the drawbacks

A bunch of examples:

🛠 Validate the generated API

Automatic validation

🛠 Go to the generated OpenAPI through the Extensions>SmallRye OpenAPI menu (or through this URI /q/dev-ui/io.quarkus.quarkus-smallrye-openapi/schema-yaml) and download the OPENAPI to the src/main/resources/openapi folder (create the openapi subfolder first).

Name it adding the suffix -code-first-openapi.yaml (e.g.,: guitarheaven-code-first-openapi.yaml).

It is time to use a linter to validate the OpenAPI. For this workshop, we will use Vacuum.

Open a new shell in VSCode and run the following command at the project's root folder:

$ ./bin/vacuum.sh -d src/main/resources/openapi/guitarheaven-corde-first-openapi.yaml

You would get the following output summary:

ategory     | Errors | Warnings | Info
Tags         | 0      | 11       | 0
Schemas      | 1      | 0        | 0
Descriptions | 0      | 30       | 20
Examples     | 0      | 54       | 0

                                                                                
          Linting file 'src/main/resources/openapi/guitarheaven-code-f          
          irst-openapi.yaml' failed with 1 errors, 95 warnings and 20           
          informs                                                               
                                                                                

ℹ️ The current issues seem insignificant. However, they would be harmful for your customers to:

Check the OpenAPI manually & pinpoint the differences

Open the OpenAPI

Check how the UUID are generated and validated:

You could find in the file src/main/resources/openapi/guitarheaven-openapi.yaml several fixes of the generated OpenAPI to make it "Api-First (or Contract-First) compatible".

Let us check what are the differences:

First, check them using your code editor. Then, let us use OASDIFF to automatically pinpoint the differences and the potential breaking changes.

bin/oasdiff.sh diff /data/src/main/resources/openapi/guitarheaven-corde-first-openapi.yaml /data/src/main/resources/openapi/guitarheaven-openapi.yaml

Remark: The /data prefix is only mentioned for compatibility with the Docker image used for running OASDIFF.

You would get such an output:

 discount:
   min:
     from: null
     to: 0
   orderId:
     pattern:
       from: '[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}'
       to: ""
   quoteId:
     pattern:
       from: '[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}'
       to: ""

You can then generate the changelog with the following command

bin/oasdiff.sh changelog /data/src/main/resources/openapi/guitarheaven-corde-first-openapi.yaml /data/src/main/resources/openapi/guitarheaven-openapi.yaml

You would get such a content:

info    [request-parameter-pattern-removed] at /data/src/main/resources/openapi/guitarheaven-openapi.yaml       
        in API GET /guitars/{guitarId}
                removed the pattern '[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}' from the 'path' request parameter 'guitarId'

info    [response-property-pattern-removed] at /data/src/main/resources/openapi/guitarheaven-openapi.yaml       
        in API GET /guitars/{guitarId}
                the 'name' response's property pattern '\S' was removed for the status '200'

You can also pinpoint the breaking changes with the following command:

bin/oasdiff.sh breaking /data/src/main/resources/openapi/guitarheaven-code-first-openapi.yaml /data/src/main/resources/openapi/guitarheaven-openapi.yaml

and get the output:

error   [request-property-type-changed] at /data/src/main/resources/openapi/guitarheaven-openapi.yaml   
        in API POST /quotes
                the 'orderId' request property type/format changed from 'string'/'' to 'string'/'uuid'

error   [request-property-type-changed] at /data/src/main/resources/openapi/guitarheaven-openapi.yaml   
        in API POST /quotes
                the 'quoteId' request property type/format changed from 'string'/'' to 'string'/'uuid'

📝 Updating the Maven configuration

ℹ️ We will set up Maven to automatically generate the server code from the OpenAPI file stored into the src/main/resources/openapi/guitarheaven-openapi.yaml.

The corresponding source code will be generated in the target/generated-sources/openapi directory.

Let us do it now!

In the build>plugins section, add the following plugin:

<plugin>
    <groupId>org.openapitools</groupId>
    <artifactId>openapi-generator-maven-plugin</artifactId>
    <version>7.10.0</version>
    <executions>
        <execution>
            <id>generate-server</id>
            <goals>
                <goal>generate</goal>
            </goals>
            <configuration>
                <inputSpec>${project.basedir}/src/main/resources/openapi/guitarheaven-openapi.yaml
                </inputSpec>
                <generatorName>jaxrs-spec</generatorName>
                <configOptions>
                    <apiPackage>info.touret.guitarheaven.application.generated.resource</apiPackage>
                    <modelPackage>info.touret.guitarheaven.application.generated.model</modelPackage>
                    <library>quarkus</library>
                    <dateLibrary>java8</dateLibrary>
                    <generateBuilders>true</generateBuilders>
                    <openApiNullable>false</openApiNullable>
                    <useBeanValidation>true</useBeanValidation>
                    <generatePom>false</generatePom>
                    <interfaceOnly>true</interfaceOnly>
                    <legacyDiscriminatorBehavior>false</legacyDiscriminatorBehavior>
                    <openApiSpecFileLocation>openapi/openapi.yaml</openApiSpecFileLocation>
                    <returnResponse>true</returnResponse>
                    <sourceFolder>.</sourceFolder>
                    <useJakartaEe>true</useJakartaEe>
                    <useMicroProfileOpenAPIAnnotations>true</useMicroProfileOpenAPIAnnotations>
                    <useSwaggerAnnotations>false</useSwaggerAnnotations>
                    <withXml>false</withXml>
                </configOptions>
                <output>${project.build.directory}/generated-sources/open-api-yaml</output>
                <ignoreFileOverride>${project.basedir}/.openapi-generator-ignore</ignoreFileOverride>
                <modelNameSuffix>Dto</modelNameSuffix>
            </configuration>
        </execution>
    </executions>
</plugin>

The plugin generates useless classes for Quarkus. We can ignore their creation adding the file using the feature and creating the file .openapi-generator-ignore at the root of your project:

# Exclude Configurator classes
**/RestApplication.java
**/RestResourceRoot.java
pom.xml

Add then the following plugin in the build>plugins> section:

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>build-helper-maven-plugin</artifactId>
    <version>3.6.0</version>
    <executions>
        <execution>
            <id>add-source</id>
            <phase>generate-sources</phase>
            <goals>
                <goal>add-source</goal>
            </goals>
            <configuration>
                <sources>
                    <source>${project.build.directory}/generated-sources/open-api-yaml</source>
                </sources>
            </configuration>
        </execution>
    </executions>
</plugin>

This configuration enables the support of two separate source folders in your project.

Now this plugin should be configured as following:

<plugin>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>${compiler-plugin.version}</version>
    <configuration>
        <parameters>true</parameters>
        <annotationProcessorPaths>
            <path>
                <groupId>org.mapstruct</groupId>
                <artifactId>mapstruct-processor</artifactId>
                <version>${org.mapstruct.version}</version>
            </path>
            <!-- other annotation processors -->
        </annotationProcessorPaths>
        <compilerArgs>
            <arg>-Amapstruct.suppressGeneratorTimestamp=true</arg>
            <arg>-Amapstruct.suppressGeneratorVersionInfoComment=true</arg>
            <arg>-Amapstruct.verbose=true</arg>
            <arg>-Amapstruct.defaultComponentModel=jakarta-cdi</arg>
        </compilerArgs>
        <generatedSourcesDirectory>${project.build.outputDirectory}/generated-source/openapi</generatedSourcesDirectory>
    </configuration>
</plugin>

✅ Now let us check it. Run the following command:

$ ./mvnw clean compile

Normally, it ends successfully and you would get such an output:

[INFO] --- openapi-generator:7.9.0:generate (default) @ guitar-heaven ---
[WARNING] Generation using 3.1.0 specs is in development and is not officially supported yet. If you would like to expedite development, please consider working on the open issues in the 3.1.0 project: https://github.com/orgs/OpenAPITools/projects/4/views/1 and reach out to our team on Slack at https://join.slack.com/t/openapi-generator/shared_invite/zt-12jxxd7p2-XUeQM~4pzsU9x~eGLQqX2g
[INFO] Generating with dryRun=false
[INFO] OpenAPI Generator: jaxrs-spec (server)
[INFO] Generator 'jaxrs-spec' is considered stable.
[INFO] Environment variable JAVA_POST_PROCESS_FILE not defined so the Java code may not be properly formatted. To define it, try 'export JAVA_POST_PROCESS_FILE="/usr/local/bin/clang-format -i"' (Linux/Mac)
[INFO] NOTE: To enable file post-processing, 'enablePostProcessFile' must be set to `true` (--enable-post-process-file for CLI).
[INFO] Invoker Package Name, originally not set, is now derived from api package name: info.touret.guitarheaven.application.generated
[INFO] Processing operation retrieveAllGuitars
[INFO] Processing operation createGuitar
[INFO] Processing operation findAllGuitarsWithPagination
[INFO] Processing operation getGuitar
[INFO] Processing operation updateGuitar
[INFO] Processing operation deleteGuitar
[INFO] Processing operation getAllOrders
[INFO] Processing operation create
[INFO] Processing operation getOrder
[INFO] Processing operation findAll
[INFO] Processing operation createQuote
[INFO] 'host' (OAS 2.0) or 'servers' (OAS 3.0) not defined in the spec. Default to [http://localhost] for server URL [http://localhost/]
[WARNING] Generation using 3.1.0 specs is in development and is not officially supported yet. If you would like to expedite development, please consider working on the open issues in the 3.1.0 project: https://github.com/orgs/OpenAPITools/projects/4/views/1 and reach out to our team on Slack at https://join.slack.com/t/openapi-generator/shared_invite/zt-12jxxd7p2-XUeQM~4pzsU9x~eGLQqX2g
[INFO] 'host' (OAS 2.0) or 'servers' (OAS 3.0) not defined in the spec. Default to [http://localhost] for server URL [http://localhost/]
[INFO] 'host' (OAS 2.0) or 'servers' (OAS 3.0) not defined in the spec. Default to [http://localhost] for server URL [http://localhost/]
[WARNING] OffsetDateTime (reserved word) cannot be used as model name. Renamed to ModelOffsetDateTime
[INFO] writing file /home/alexandre/dev/src/api-first-workshop/target/generated-sources/openapi/./info/touret/guitarheaven/application/generated/model/GuitarDto.java
[INFO] writing file /home/alexandre/dev/src/api-first-workshop/target/generated-sources/openapi/./info/touret/guitarheaven/application/generated/model/LinksDto.java
[INFO] writing file /home/alexandre/dev/src/api-first-workshop/target/generated-sources/openapi/./info/touret/guitarheaven/application/generated/model/OrderDto.java
[INFO] writing file /home/alexandre/dev/src/api-first-workshop/target/generated-sources/openapi/./info/touret/guitarheaven/application/generated/model/PageableGuitarDto.java
[INFO] writing file /home/alexandre/dev/src/api-first-workshop/target/generated-sources/openapi/./info/touret/guitarheaven/application/generated/model/Quote.java
[INFO] writing file /home/alexandre/dev/src/api-first-workshop/target/generated-sources/openapi/./info/touret/guitarheaven/application/generated/model/QuoteDto.java
[INFO] writing file /home/alexandre/dev/src/api-first-workshop/target/generated-sources/openapi/./info/touret/guitarheaven/application/generated/model/TYPE.java
[INFO] 'host' (OAS 2.0) or 'servers' (OAS 3.0) not defined in the spec. Default to [http://localhost] for server URL [http://localhost/]
[INFO] writing file /home/alexandre/dev/src/api-first-workshop/target/generated-sources/openapi/./info/touret/guitarheaven/application/generated/resource/GuitarsApi.java
[INFO] 'host' (OAS 2.0) or 'servers' (OAS 3.0) not defined in the spec. Default to [http://localhost] for server URL [http://localhost/]
[INFO] writing file /home/alexandre/dev/src/api-first-workshop/target/generated-sources/openapi/./info/touret/guitarheaven/application/generated/resource/OrdersApi.java
[INFO] 'host' (OAS 2.0) or 'servers' (OAS 3.0) not defined in the spec. Default to [http://localhost] for server URL [http://localhost/]
[INFO] writing file /home/alexandre/dev/src/api-first-workshop/target/generated-sources/openapi/./info/touret/guitarheaven/application/generated/resource/QuotesApi.java
[INFO] Skipping generation of Webhooks.
[INFO] 'host' (OAS 2.0) or 'servers' (OAS 3.0) not defined in the spec. Default to [http://localhost] for server URL [http://localhost/]
[INFO] writing file /home/alexandre/dev/src/api-first-workshop/target/generated-sources/openapi/README.md
[INFO] Ignored /home/alexandre/dev/src/api-first-workshop/target/generated-sources/openapi/info/touret/guitarheaven/application/generated/RestResourceRoot.java (Ignored by rule in ignore file.)
[INFO] Ignored /home/alexandre/dev/src/api-first-workshop/target/generated-sources/openapi/info/touret/guitarheaven/application/generated/RestApplication.java (Ignored by rule in ignore file.)
[INFO] writing file /home/alexandre/dev/src/api-first-workshop/target/generated-sources/openapi/openapi/openapi.yaml
[INFO] writing file /home/alexandre/dev/src/api-first-workshop/target/generated-sources/openapi/src/main/resources/application.properties
[INFO] writing file /home/alexandre/dev/src/api-first-workshop/target/generated-sources/openapi/src/main/docker/Dockerfile.jvm
[INFO] writing file /home/alexandre/dev/src/api-first-workshop/target/generated-sources/openapi/src/main/docker/Dockerfile.native
[INFO] writing file /home/alexandre/dev/src/api-first-workshop/target/generated-sources/openapi/.dockerignore
[INFO] writing file /home/alexandre/dev/src/api-first-workshop/target/generated-sources/openapi/.openapi-generator-ignore
[INFO] writing file /home/alexandre/dev/src/api-first-workshop/target/generated-sources/openapi/.openapi-generator/VERSION
[INFO] writing file /home/alexandre/dev/src/api-first-workshop/target/generated-sources/openapi/.openapi-generator/FILES
################################################################################
# Thanks for using OpenAPI Generator.                                          #
# Please consider donation to help us maintain this project 🙏                 #
# https://opencollective.com/openapi_generator/donate                          #
################################################################################

📝 Update the server code

DTO

Delete first the info.touret.guitarheaven.application.dto package.

API

For the following classes : GuitarResouce, OrderRequestResource and QuoteResource, do the following steps:

Change the import declarations of DTOs from info.touret.guitarheaven.application.dto to info.touret.guitarheaven.application.dto:

For instance, from :

import info.touret.guitarheaven.application.dto.GuitarDto;
import info.touret.guitarheaven.application.dto.PageableGuitarDto;

to

import info.touret.guitarheaven.application.generated.model.GuitarDto;
import info.touret.guitarheaven.application.generated.model.PageableGuitarDto;

Declare the resources as implementing their Api (e.g., GuitarsApi).

For example:

public class GuitarResource implements GuitarsApi {...

Remove all the Microprofile OpenAPI and the jakarta.ws.rs annotations (e.g. @GET)

For instance:

@Operation(summary = "Gets all guitars")
@APIResponse(responseCode = "200", description = "Success ")
@APIResponse(responseCode = "500", description = "Server unavailable")
@GET

Change all the method declarations.

Remove the jakarta.validation.constraints annotations such as @NotNull

Instead of returning a POJO, you will have now to return a Response object.

For instance:

public Response findAll() {
    return Response.ok(quoteService.findAll()).build();
}

If a method requires now returning a response, you can use the Response.noContent().build() functionality.

For instance:

public Response deleteGuitar(@NotNull UUID guitarId) {
    guitarService.deleteGuitarByUUID(guitarId);
    return Response.noContent().build();
}

At the end, you will have these API resource classes:

GuitarResource

@ApplicationScoped
public class GuitarResource implements GuitarsApi {

    private final GuitarService guitarService;

    private final GuitarMapper guitarMapper;
    private final PaginationLinksFactory pageUtils;

    @Inject
    public GuitarResource(GuitarService guitarService, GuitarMapper guitarMapper, PaginationLinksFactory pageUtils) {
        this.guitarService = guitarService;
        this.guitarMapper = guitarMapper;
        this.pageUtils = pageUtils;
    }

    @Context
    private UriInfo uriInfo;

    @Override
    public Response retrieveAllGuitars() {
        return Response.ok(guitarMapper.toGuitarsDto(guitarService.findAllGuitars())).build();
    }

    @Override
    public Response createGuitar(GuitarDto guitarDto) {
        return Response.status(201).entity(Map.of("guitarId", guitarService.createGuitar(guitarMapper.toGuitar(guitarDto)))).build();
    }

    @Override
    public Response updateGuitar(UUID guitarId, GuitarDto guitarDto) {
        return Response.ok(guitarMapper.toGuitarDto(guitarService.updateGuitar(guitarMapper.toGuitar(guitarDto)))).build();
    }

    @Override
    public Response deleteGuitar(UUID guitarId) {
        guitarService.deleteGuitarByUUID(guitarId);
        return Response.noContent().build();
    }

    @Override
    public Response getGuitar(UUID guitarId) {
        var guitars = guitarService.findGuitarsByGuitarIds(List.of(guitarId));
        if (guitars.isEmpty()) {
            throw new WebApplicationException("Guitar " + guitarId + " not found", Status.NOT_FOUND);
        } else {
            return Response.ok(guitarMapper.toGuitarDto(guitars.getFirst())).build();
        }
    }

    @Override
    public Response findAllGuitarsWithPagination(Integer pageNumber, Integer pageSize) {
        var guitarsByPage = guitarService.findAllGuitarsByPage(pageNumber, pageSize);
        try {
            return Response.ok(new PageableGuitarDto().guitars(guitarMapper.toGuitarsDto(guitarsByPage.entities())).links(pageUtils.createLinksDto(uriInfo, guitarsByPage, pageSize))).build();
        } catch (URISyntaxException | MalformedURLException e) {
            throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
        }
    }

}

OrderRequestResource

@ApplicationScoped
public class OrderRequestResource implements OrdersRequestsApi {

    private final OrderRequestService orderRequestService;
    private final OrderRequestMapper orderRequestMapper;

    public OrderRequestResource(OrderRequestService orderRequestService, OrderRequestMapper orderRequestMapper) {
        this.orderRequestService = orderRequestService;
        this.orderRequestMapper = orderRequestMapper;
    }

    @Override
    public Response create(OrderRequestDto order) {
        return Response.status(201).entity(Map.of("orderId", orderRequestService.create(orderRequestMapper.toOrder(order)))).build();
    }

    @Override
    public Response getAllOrders() {
        return Response.ok(orderRequestMapper.toOrderDtoList(orderRequestService.findAllOrders())).build();
    }

    @Override
    public Response getOrder(UUID orderId) {
        return Response.ok(orderRequestMapper.toOrderDto(orderRequestService.findByUUID(orderId).orElseThrow(
                () -> new WebApplicationException(Response.Status.NOT_FOUND)))).build();
    }
}

QuoteResource

@ApplicationScoped
public class QuoteResource implements QuotesApi {


    private final QuoteService quoteService;
    private final QuoteMapper quoteMapper;

    @Inject
    public QuoteResource(QuoteService quoteService, QuoteMapper quoteMapper) {
        this.quoteService = quoteService;
        this.quoteMapper = quoteMapper;
    }


    @Override
    public Response createQuote(QuoteDto quoteDto) {
        return Response.status(201).entity(Map.of("quoteId", quoteService.createQuote(quoteMapper.fromDto(quoteDto)))).build();
    }

    @Override
    public Response findAll() {
        return Response.ok(quoteService.findAll()).build();
    }
}

Mapper

For the GuitarMapper, OrderRequestMapper and QuoteMapper located in the info.touret.guitarheaven.application.mapper package, update the import declaration in the same way as before.

For instance, you can update this import declaration:

import info.touret.guitarheaven.application.dto.GuitarDto;

to

import info.touret.guitarheaven.application.generated.model.GuitarDto;

LinksFactory

Update the import declaration as above and change the creation of the LinksDto class from:

return new LinksDto(self, first, prev, next, last);

to:

return new LinksDto().self(self.toString()).first(first.toString()).prev(prev.toString()).next(next.toString()).last(last.toString());

Integration tests

Change then the DTO creation in the integration tests : GuitarResourceTest,OrderRequestResourceTest and QuoteResourceTest.

For example, change

var guitar = new GuitarDto(UUID.fromString("628766d4-fee3-46dd-8bcb-426cffb4d585"), "Gibson ES 335", ELECTRIC, 2500.0, 9);

to

var guitar = new GuitarDto().guitarId(UUID.fromString("628766d4-fee3-46dd-8bcb-426cffb4d585")).name("Gibson ES 135").type(ELECTRIC).price(2500.0).stock(9);

Update then the import declarations.

🛠 Validation

Run the following command:

$ ./mvnw clean verify

It might be successful.

Now, you can run again the application and go to the dev-ui

$ ./mvnw quarkus:dev

👀 Object Naming

If we look into our API specification carefully, we can guess our backend is built on top of a Java platform.

To make it fully agnostic, let us revamp it without (mostly) impacting the Java code.

📝 Dto

In the guitarheaven-openapi.yaml file, remove all the Dto suffixes.

In the pom.xml, add the following configuration parameter into the build>plugins>openapi-generator-maven-plugin>configuration

<modelNameSuffix>Dto</modelNameSuffix>

You will therefore have the following configuration for this plugin:

<plugin>
    <groupId>org.openapitools</groupId>
    <artifactId>openapi-generator-maven-plugin</artifactId>
    <version>7.9.0</version>
    <executions>
        <execution>
            <goals>
                <goal>generate</goal>
            </goals>
            <configuration>
                <inputSpec>${project.basedir}/src/main/resources/openapi/guitarheaven-openapi.yaml</inputSpec>
                <generatorName>jaxrs-spec</generatorName>
                <configOptions>
                    <apiPackage>info.touret.guitarheaven.application.generated.resource</apiPackage>
                    <modelPackage>info.touret.guitarheaven.application.generated.model</modelPackage>
                    <library>quarkus</library>
                    <dateLibrary>java8</dateLibrary>
                    <generateBuilders>true</generateBuilders>
                    <openApiNullable>false</openApiNullable>
                    <useBeanValidation>true</useBeanValidation>
                    <generatePom>false</generatePom>
                    <interfaceOnly>true</interfaceOnly>
                    <legacyDiscriminatorBehavior>false</legacyDiscriminatorBehavior>
                    <openApiSpecFileLocation>openapi/openapi.yaml</openApiSpecFileLocation>
                    <returnResponse>true</returnResponse>
                    <sourceFolder>.</sourceFolder>
                    <useJakartaEe>true</useJakartaEe>
                    <useMicroProfileOpenAPIAnnotations>true</useMicroProfileOpenAPIAnnotations>
                    <useSwaggerAnnotations>false</useSwaggerAnnotations>
                    <withXml>false</withXml>
                </configOptions>
                <ignoreFileOverride>${project.basedir}/.openapi-generator-ignore</ignoreFileOverride>
                <modelNameSuffix>Dto</modelNameSuffix>
            </configuration>
        </execution>
    </executions>

</plugin>

Stop the quarkus dev, and generate again the code

$ ./mvnw clean compile

In the GuitarResourceTest class, update the import declaration

From:

import static info.touret.guitarheaven.application.generated.model.TYPE.ELECTRIC;

to:

import static info.touret.guitarheaven.application.generated.model.TYPEDto.ELECTRIC;

Yes it is a side effect :-(

📝 Date Time

In the OpenAPI, remove the OffsetDateTime schema type and update the fields using it as following:

createdAt:
  type: string
  format: date-time

Run again the following command:

$ ./mvnw clean compile

and now run the Quarkus dev environment to check it again:

$ ./mvnw quarkus:dev

You can go through the SmallRye Swagger UI to catch the differences.

Other Data Constraints

Check out the Open API and add when it is necessary, the different constraints to the different schemas and endpoints:

Next, generate the code and see how they are "converted" into a Java code with the following command:

./mvnw clean compile

For instance for the OrderRequest object:

OrderRequest:
    type: object
    required:
      - guitarIds
    properties:
      orderId:
        type: string
        format: uuid
      guitarIds:
        type: array
        minItems: 1
        items:
          type: string
          format: uuid
      discountRequestedInUSD:
        type: number
        format: double
      createdAt:
        type: string
        format: date-time

We will have the corresponding generated Java code in the target/generated-sources/open-api-yaml/info/touret/guitarheaven/application/generated/model/ folder:

@org.eclipse.microprofile.openapi.annotations.media.Schema(description="")
@JsonTypeName("OrderRequest")
@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2025-01-07T20:15:49.979612552+01:00[Europe/Paris]", comments = "Generator version: 7.10.0")
public class OrderRequestDto   {
    private UUID orderId;
    private @Valid List<UUID> guitarIds = new ArrayList<>();
    private Double discountRequestedInUSD;
    private OffsetDateTime createdAt;

    public OrderRequestDto() {
    }

    /**
     **/
    public OrderRequestDto orderId(UUID orderId) {
        this.orderId = orderId;
        return this;
    }

    @org.eclipse.microprofile.openapi.annotations.media.Schema(description = "")
    @JsonProperty("orderId")
    public UUID getOrderId() {
        return orderId;
    }

    @JsonProperty("orderId")
    public void setOrderId(UUID orderId) {
        this.orderId = orderId;
    }


    @org.eclipse.microprofile.openapi.annotations.media.Schema(required = true, description = "")
    @JsonProperty("guitarIds")
    @NotNull  @Size(min=1)public List<UUID> getGuitarIds() {
        return guitarIds;
    }

    @JsonProperty("guitarIds")
    public void setGuitarIds(List<UUID> guitarIds) {
        this.guitarIds = guitarIds;
    }


    @org.eclipse.microprofile.openapi.annotations.media.Schema(description = "")
    @JsonProperty("discountRequestedInUSD")
    public Double getDiscountRequestedInUSD() {
        return discountRequestedInUSD;
    }

    @JsonProperty("discountRequestedInUSD")
    public void setDiscountRequestedInUSD(Double discountRequestedInUSD) {
        this.discountRequestedInUSD = discountRequestedInUSD;
    }

    @org.eclipse.microprofile.openapi.annotations.media.Schema(description = "")
    @JsonProperty("createdAt")
    public OffsetDateTime getCreatedAt() {
        return createdAt;
    }

    @JsonProperty("createdAt")
    public void setCreatedAt(OffsetDateTime createdAt) {
        this.createdAt = createdAt;
    }

👀 Explore Microcks

To be in your customer's shoes, we will experiment how they would use your API during their development. After testing the API through a Swagger UI or Redocly console, they would probably integrate your API and mock it.

Among other things, mocking external API help isolate the code from external resources and streamline the SDLC (Software Development LifeCycle).

Some projects could help in this field : Microcks, Wiremock, MockServer.

In this workshop, we will use the first one. Beyond simply mocking external endpoints, it may help to do contract testing and provide a standalone mock which could be sat up in an easy way.

Go to the dev-ui and select the Extensions>Microcks extension.

microcks extension

Select then APIs | Services.

Click on Guitar Heaven API.

guitar heaven microcks conformance score

Check out the conformance index score.

Compare with the EBay Browse API. As you can see, this is mainly due to the fact that no examples are provided in the OpenAPI.

Add examples

For more information about OpenAPI examples, you can check out the specification.

To help you avoid wasting time with the examples, you can directly use the OpenAPI file guitarheaven-with-examples.yaml.

On the Microcks web page, open then the service Guitar Heaven API with Examples - 1.0.1. You will see the score is increased and now we have mocks.

You can now run any query you want.

For instance:

curl -X GET 'http://localhost:32805/rest/Guitar+Heaven+API+with+Examples/1.0.1/guitars' -H 'Accept: application/json'

You will get the output configured in the OpenAPI.

Let us dig into it and see what is under the hood:

If you look into the POST /guitars endpoint definition, you will see we coded examples for both the request and the results:

Here we specified the order_es335 example

    post:
      summary: Creates a guitar
      operationId: createGuitar
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/Guitar"
            examples:
              es335:
                value:
                  name: "ES 335"
                  type: ELECTRIC
                  priceInUSD: 2500
                  stock: 10
              stratocaster:
                value:
                  name: "Stratocaster"
                  type: ELECTRIC
                  priceInUSD: 1500
                  stock: 5
        required: true
      responses:
        "201":
          description: Guitar creation successful
          content:
            application/json:
              schema:
                type: object
                properties:
                  guitarId:
                    type: string
                    format: uuid
              examples:
                es335:
                  summary: ES 335 created
                  value:
                    guitarId: "756733e5-c247-49d8-bf40-9a481c2f0bc3"
                stratocaster:
                  summary: Stratocaster created
                  value:
                    guitarId: 628226d4-fee3-46dd-8bcb-426cffb4a665
        "400":
          description: 'The request is invalid '
        "500":
          description: Server unavailable

      tags:
        - Guitar Resource

In this way, your customer must stick to your specification during their tests.

Be aware, to be detected and usable in Microcks, the name of the examples declared in the request must also be present in the response specification.

Now, how to deal with endpoints which don't return any content?

Microcks offers the x-microcks-refs extension attribute for that purpose:

For instance, for the DELETE /guitars endpoint:

    delete:
      summary: Deletes a guitar
      operationId: deleteGuitar
      parameters:
        - name: guitarId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          examples:
            es335:
              summary: "ES 335"
              value: 628226d4-fee3-46dd-8bcb-426cffb4a685
            stratocaster:
              summary: "Fender Stratocaster"
              value: 628226d4-fee3-46dd-8bcb-426cffb4a666
      responses:
        "204":
          description: 'Guitar update successful '
          x-microcks-refs:
            - es335
            - stratocaster
        "400":
          description: 'The request is invalid '
        "500":
          description: Server unavailable
      tags:
        - Guitar Resource

Contract testing

We have now an API built using an OpenAPI description. How to be fully sure your program fully complies with your specification?

You can use the Contract Testing for that!

contract testing

If you want to know more about contract testing, you can check out :

These tests could be implemented in a different ways:

In this workshop, we will implement the latter.

Create the following class in the src/test/resouces directory and in the info.touret.guitarheaven.test.application package:

@QuarkusTest
public class APIContractTest {
    private final static Logger LOGGER = LoggerFactory.getLogger(APIContractTest.class);

    @ConfigProperty(name = "quarkus.http.test-port")
    int quarkusHttpPort;

    @ConfigProperty(name = "quarkus.microcks.default.http")
    String microcksContainerUrl;

    @Inject
    ObjectMapper mapper;

    @Test
    public void testOpenAPIContract() throws Exception {
        TestRequest testRequest = new TestRequest.Builder()
                .serviceId("Guitar Heaven API with Examples:1.0.1")
                .runnerType(TestRunnerType.OPEN_API_SCHEMA.name())
                .testEndpoint("http://host.testcontainers.internal:" + quarkusHttpPort)
                .build();
        TestResult testResult = MicrocksContainer.testEndpoint(microcksContainerUrl, testRequest);
        LOGGER.error(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(testResult));
        assertTrue(testResult.isSuccess());
    }
}

Under the hood, this test will run Microcks and checks every endpoint declared in the OpenAPI using the examples filled earlier.

To check it , stop the Quarkus Dev and run the integration tests.

$ ./mvnw clean verify

It would end successfully.

👀 Look into the existing code

Check out the infrastructure code in the package info.touret.guitarheaven.infrastructure.kafka and see how the connection to Kakfa is handled.

You could find the following classes:

If we want to apply the same principes we implemented earlier, this client layer should be generated from a specification.

How to do that for even-driven API? The AsyncAPI standard could help us in this challenge!

It is based on OpenAPI and specifies event-driven API accessible through Kafka, AMQP or MQTT.

🛠️ Draf an event-driven API

In this chapter, we will

First, create the file supplychain-asyncapi.yaml in the folder src/main/resources/asyncapi and copy the following content:

asyncapi: 3.0.0
info:
  title: Guitar Supply Chain API
  version: 1.0.0
  description: This API is notified whenever the stock is too low

servers:
  dev:
    host: kafka://localhost:8092
    description: Kafka broker running in a Quarkus Dev Service
    protocol: kafka

operations:
  onGuitarRequestOut:
    action: send
    channel:
      $ref: '#/channels/guitar-requests-out'
  onGuitarRequestIn:
    action: receive
    channel:
      $ref: '#/channels/guitar-requests-in'

channels:
  guitar-requests-out:
    description: This channel is used to broadcast guitars supply requests
    address: guitar-requests
    messages:
      guitarRequest:
        $ref: '#/components/messages/guitarRequest'
  guitar-requests-in:
    description: This channel is used to fetch guitar requests
    address: guitar-requests
    messages:
      guitarRequest:
        $ref: '#/components/messages/guitarRequest'
components:
  messages:
    guitarRequest:
      name: guitarRequest
      title: Guitar Request
      contentType: application/json
      payload:
        $id: "GuitarRequest"
        $ref: '#/components/schemas/guitarRequest'
  schemas:
    guitarRequest:
      additionalProperties: false
      type: object
      properties:
        requestId:
          type: string
          format: uuid
          description: This property describes the UUID of the request
        guitarName:
          type: string
          description: This property describes the name of the guitar
        quantity:
          type: integer
          description: The quantity to order and supply

If you want to know more about the different elements, you can browse

✅ Validation

The Async API initiative provides a bunch of tools. We will use some of them in this workshop the cli & the generator.

Unfortunately, the generator doesn't provide any model for Quarkus.

We will use it through its Docker image.

Run then the following command:

$ ./bin/asyncapi-validate.sh

You would get such an output:

bin/asyncapi-validate.sh 
Pulling & Running ASYNCAPIh  (command)  ...asyncapi-validate.sh  (command)

AsyncAPI anonymously tracks command executions to improve the specification and tools, ensuring no sensitive data reaches our servers. It aids in comprehending how AsyncAPI tools are used and adopted, facilitating ongoing improvements to our specifications and tools.

To disable tracking, please run the following command:
  asyncapi config analytics --disable

Once disabled, if you want to enable tracking back again then run:
  asyncapi config analytics --enable


File /app/example/main/resources/asyncapi/supplychain-asyncapi.yaml is valid! File /app/example/main/resources/asyncapi/supplychain-asyncapi.yaml and referenced documents don't have governance issues.

If you want to edit it you can also use the AsyncAPI studio.

🛠️ Model Generation

This chapter only illustrates the model generation at build time. We won't include this step in our app build workflow.

Run the following command:

$ ./bin/asyncapi-generate-model.sh

You would get the following output:

Pulling & Running ASYNCAPIh  (command)  ...asyncapi-validate.sh  (command)

AsyncAPI anonymously tracks command executions to improve the specification and tools, ensuring no sensitive data reaches our servers. It aids in comprehending how AsyncAPI tools are used and adopted, facilitating ongoing improvements to our specifications and tools.

To disable tracking, please run the following command:
  asyncapi config analytics --disable

Once disabled, if you want to enable tracking back again then run:
  asyncapi config analytics --enable

│
 ›   Warning: Overwriting existing model with name guitarRequest, are there two models with the same name present? Overwriting the old model.
◇  Successfully generated the following models: GuitarRequest

and the generated class in the target/generated-sources/asyncapi folder.

Mocking your Event-Driven API

We can then mock our Event Driven API in the same way we did for our REST API.

Start Quarkus

$ ./mvnw quarkus:dev

Go then to the Microcks extension page and check out the Guitar Supply Chain API.

You can see its conformance index is low. It is the same reason as before. We must therefore add examples.

Now, import this new description with examples:

Create the supplychain-with-examples-asyncapi.yaml with the following content:

asyncapi: 3.0.0
info:
  title: Guitar Supply Chain API
  version: 1.0.0
  description: This API is notified whenever the stock is too low

defaultContentType: application/json
channels:
  guitar-requests-out:
    address: guitar-requests
    description: Requests
    messages:
      guitarRequest.message:
        description: Event to ask a guitar
        payload:
          type: object
          additionalProperties: false
          properties:
            requestId:
              type: string
              format: uuid
            guitarName:
              type: string
            quantity:
              type: integer
        examples:
          - name: "Gibson ES 335"
            summary: "Example for Gibson ES 335"
            payload:
              requestId: ba4bf043-ee08-47c7-8b4f-75427d7213bf
              guitarName: "Gibson ES 335"
              quantity: 10
          - name: "Fender Stratocaster"
            summary: "Example for Fender Stratocaster"
            payload:
              requestId: ba4bf043-ee08-47c7-8b4f-75427d7213be
              guitarName: "Fender Stratocaster"
              quantity: 5
  guitar-requests-in:
    address: guitar-requests
    description: Requests
    messages:
      guitarRequest.message:
        description: Event to ask a guitar
        payload:
          type: object
          additionalProperties: false
          properties:
            requestId:
              type: string
              format: uuid
            guitarName:
              type: string
            quantity:
              type: integer
        examples:
          - name: "Gibson ES 335"
            summary: "Example for Gibson ES 335"
            payload:
              requestId: ba4bf043-ee08-47c7-8b4f-75427d7213bf
              guitarName: "Gibson ES 335"
              quantity: 10
          - name: "Fender Stratocaster"
            summary: "Example for Fender Stratocaster"
            payload:
              requestId: ba4bf043-ee08-47c7-8b4f-75427d7213be
              guitarName: "Fender Stratocaster"
              quantity: 5
operations:
  onGuitarRequestOut:
    action: send
    channel:
      $ref: '#/channels/guitar-requests-out'
    summary: Send command
    messages:
      - $ref: '#/channels/guitar-requests-out/messages/guitarRequest.message'
  onGuitarRequestin:
    action: receive
    channel:
      $ref: '#/channels/guitar-requests-in'
    summary: Fetches command
    messages:
      - $ref: '#/channels/guitar-requests-in/messages/guitarRequest.message'


servers:
  dev:
    host: kafka://localhost:8092
    description: Kafka broker running in a Quarkus Dev Service
    protocol: kafka

Go back to the dev-ui Microcks web page.

Delete the Guitar Heaven Supply Chain API

microcks delete

Go to the Importers menu and Click on Upload

microcks importer

Select the new file and click on Upload.

microcks upload

Go back to the APIs|Services menu and look into the Guitar Heaven Supply Chain API to check if there is samples now.

microcks asyncapi

Unfortunately, we will not go further during this workshop.

If you want to see a demo on how you can integrate Microcks & Kafka, you can check out this example.

Now, let us streamline our API client.

Check out how the API Client is built in the info.touret.guitarheaven.infrastructure.ebay package.

We have the following classes:

You can also browse the OpenAPI description located in the src/main/resources/openapi-client folder.

Update the Maven configuration

We need to enable the Quarkus OpenAPI Generator extension.

Add therefore a new dependency into the pom.xml:

<dependency>
  <groupId>io.quarkiverse.openapi.generator</groupId>
  <artifactId>quarkus-openapi-generator</artifactId>
  <version>2.7.1-lts</version>
</dependency>

Update the Quarkus configuration

We must define the Quarkus configuration extension with some items.

In the file src/main/resources/application.properties add the following properties:

quarkus.openapi-generator.codegen.input-base-dir=src/main/resources/openapi-client
quarkus.openapi-generator.codegen.spec.ebay_buy_openapi_yaml.base-package=info.touret.guitarheaven.infrastructure.ebay
quarkus.openapi-generator.codegen.spec.ebay_buy_openapi_yaml.model-name-suffix=Dto
quarkus.openapi-generator.codegen.spec.ebay_buy_openapi_yaml.use-bean-validation=true

In both the files src/main/resources/application.properties and src/test/resources/application.properties add the following properties: Remove the property quarkus.rest-client."info.touret.guitarheaven.infrastructure.ebay.EbayClient".url and add the new property:

quarkus.rest-client.ebay_buy_openapi_yaml.url=${quarkus.microcks.default.http}/rest/Browse+API/v1.19.9

Update the Ebay API client code

Now, let us streamline our API Client code.

In the package info.touret.guitarheaven.infrastructure.ebay, only keep the EbayDiscounterAdapter class.
Remove the others.

Modify the adapter with the following code:

@ApplicationScoped
public class EbayDiscounterAdapter implements SupplierCatalogPort {

    public static final int SEARCH_THRESHOLD = 1;
    @RestClient
    private ItemSummaryApi ebayClient;

    @Override
    public OptionalDouble getAverageGuitarPrice(String guitarName) {

        var searchPagedCollection = ebayClient.search(guitarName, "foo", "foo", "foo");

        if (searchPagedCollection.getTotal() > SEARCH_THRESHOLD) {
            return searchPagedCollection.getItemSummaries()
                    .stream()
                    .mapToDouble(value -> value.getPrice().getValue().doubleValue())
                    .average();
        } else return OptionalDouble.empty();
    }

    @ClientExceptionMapper
    static RuntimeException toException(Response response) {
        if (response.getStatus() == 400) {
            return new RuntimeException("The remote service responded with HTTP 400");
        }
        // Disabling some issues with the EBAY Mock
        return null;
    }
}

Verification

Check if the code compiles first:

$ ./mvnw clean compile

Check then if the integration tests run well:

$ ./mvnw clean verify

Now you can run the Quarkus app. The modification should be transparent in a customer point of view.

To check it, you can go to the SmallRye SwaggerUI and try the API.

JSONAPI Introduction

Check out (and try) how the pagination is handled on the GET /guitars/pages endpoint. It's based on the JSON API specification and the Hypertext Application Language (HAL).

Update the API

We will base on our API on the

guitarheaven-with-examples-openapi.yaml

OpenAPI description.

Update your pom.xml to use it instead of guitarheaven-openapi.yaml in the build>plugins>openapi-generator-maven-plugin section

<plugin>
  <groupId>org.openapitools</groupId>
  <artifactId>openapi-generator-maven-plugin</artifactId>
  <version>7.10.0</version>
  <executions>
      <execution>
          <id>generate-server</id>
          <goals>
              <goal>generate</goal>
          </goals>
          <configuration>
              <inputSpec>${project.basedir}/src/main/resources/openapi/guitarheaven-with-examples-openapi.yaml
              </inputSpec>

Copy-paste the guitarheaven-openapi-with-examples.yaml file and name the new file as guitarheaven-with-examples-openapi-ori .yaml.

First, go to the guitarheaven-openapi-with-examples.yaml and merge the definition of the GET /guitars GET /guitars/pages endpoints.

For the following steps, you can use the Swagger OpenAPI Editor.

Add the parameters and examples of the second one to the first one and remove the GET /guitars/pages endpoint.

Change the schema of the response content schema to :

content:
  application/json:
    schema:
      type: array
      items:
        $ref: "#/components/schemas/Guitars"

Rename the schema PageableGuitar to Guitars

We can also improve the constraints on the parameters adding the required, maximum and minimum constraints.

You should therefore have the following endpoint:

    get:
      summary: Gets all guitars
      operationId: findAllGuitarsWithPagination
      parameters:
        - name: pageNumber
          in: query
          required: true
          schema:
            type: integer
            format: int32
            minimum: 0
          examples:
            list:
              value: 0
        - name: pageSize
          in: query
          required: true
          schema:
            type: integer
            format: int32
            maximum: 10
          examples:
            list:
              value: 10
      responses:
        "200":
          description: 'Success '
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Guitars"
              examples:
                list:
                  value:
                    guitars:
                      - guitarId: "19fd8f49-745d-42e7-9e66-d0449442e3d1"
                        name: "ES 335"
                        type: "ELECTRIC"
                        priceInUSD: 2500
                        stock": 10
                      - guitarId: "5ef4d8fc-7d71-4599-a46e-c8693e7f5de7"
                        name: "Stratocaster"
                        type: "ELECTRIC"
                        priceInUSD: 1500
                        stock: 5
                      - guitarId: "0f29005c-cfac-4a6b-ba2f-7c21d7fbd3a2"
                        name: "Les Paul"
                        type: "ELECTRIC"
                        priceInUSD: 3000
                        stock: 2
                    links:
                      self: "http://localhost:8080/guitars/pages?pageNumber=http://localhost:8080/guitars/pages&pageSize=0"
                      first: "http://localhost:8080/guitars/pages?pageNumber=http://localhost:8080/guitars/pages&pageSize=0"
                      prev: "http://localhost:8080/guitars/pages?pageNumber=http://localhost:8080/guitars/pages&pageSize=0"
                      next: "http://localhost:8080/guitars/pages?pageNumber=http://localhost:8080/guitars/pages&pageSize=1"
                      last: "http://localhost:8080/guitars/pages?pageNumber=http://localhost:8080/guitars/pages&pageSize=9"
        "500":
          description: Server unavailable
      tags:
        - Guitar Resource

To check this API, you can use vacuum:

$ ./bin/vacuum.sh -d src/main/resources/openapi/guitarheaven-with-examples-openapi.yaml

Pinpointing the differences

Now, let us check what are the differences and specially the breaking changes:

$ ./bin/oasdiff.sh diff /data/src/main/resources/openapi/guitarheaven-with-examples-openapi-ori.yaml /data/src/main/resources/openapi/guitarheaven-with-examples-openapi.yaml

Updating the code

Run the following command:

$ ./mvnw clean compile

Go to the GuitarResource.

Remove the retrieveAllGuitars() method.

In the method findAllGuitarsWithPagination() `change the reference to PageableGuitarDto to GuitarsDto.

You will get the following content:

@Override
public Response findAllGuitarsWithPagination(Integer pageNumber, Integer pageSize) {
    var guitarsByPage = guitarService.findAllGuitarsByPage(pageNumber, pageSize);
    try {
        return Response.ok(new GuitarsDto().guitars(guitarMapper.toGuitarsDto(guitarsByPage.entities())).links(pageUtils.createLinksDto(uriInfo, guitarsByPage, pageSize))).build();
    } catch (URISyntaxException | MalformedURLException e) {
        throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
    }
}

Remove the PageableGuitarDto import declaration.

Updating the tests

In the class GuitarResourceTest

Update the should_get_a_list_successfully() test with the following content:

@Order(1)
@Test
void should_get_a_list_successfully() {
    RestAssured.given()
            .get("/guitars?pageNumber=0&pageSize=10")
            .then()
            .statusCode(200)
            .assertThat().body("isEmpty()", Is.is(false))
            .assertThat().body("links.size()", Is.is(5))
            .assertThat().body("links.self", IsAnything.anything())
            .assertThat().body("links.next", IsAnything.anything())
            .assertThat().body("links.last", IsAnything.anything())
            .assertThat().body("links.prev", IsAnything.anything())
            .assertThat().body("links.first", IsAnything.anything());
}

Update then the should_find_guitar_page_successfully() method:

@Order(5)
@Test
void should_find_guitar_page_successfully() {
    RestAssured.given()
            .get("/guitars?pageNumber=0&pageSize=10")
            .then()
            .statusCode(200)
            .assertThat().body("links.size()", Is.is(5))
            .assertThat().body("links.self", IsAnything.anything())
            .assertThat().body("links.next", IsAnything.anything())
            .assertThat().body("links.last", IsAnything.anything())
            .assertThat().body("links.prev", IsAnything.anything())
            .assertThat().body("links.first", IsAnything.anything());
}

Verification

Run the following command:

$ ./mvnw verify

It should be successful.

Run Quarkus

You can finally check it live running Quarkus:

$ ./mvnw quarkus:dev