basysKom AnwendungsEntwicklung

Qt Quick and Swagger/OpenAPI – a tutorial
Essential Summary
We take a close look how to generate Qt specific code from Swagger/OpenAPI and explain how to integrate the generated code with a Qt Quick Application.

Introduction

OpenAPI provides a structured, integrated and language agnostic way to design, test and maintain REST APIs. One of the big selling points of OpenAPI is the ability to generate server-/client-side code for various languages and frameworks. In the following article we will have a closer look at the Qt5 client support offered by the OpenAPI Generator and the Swagger tooling.

Motivation

REST is the de facto standard today when it comes to services and APIs in the IT world. REST itself is not a tightly defined standard but rather a set of principles and practices (which are, at times, interpreted differently by its users). ADLs (API description languages), amongst which OpenAPI/Swagger is the most popular, have been developed to provide a more structured approach to REST, making it easier to develop, test, maintain and document APIs.

When working in an IT context Swagger/OpenAPI is well established, but we also encounter such APIs at the boundary between the OT-world (machines/devices) and the IT-world – often with C++/Qt as an implementation environment. 

Qt has a built-in HTTP-client since 4.4 (~2003) which is sufficient to interact with REST/OpenAPI/Swagger APIs, but this means to write quite a bit of boilerplate by hand. As writing boilerplate code can be pretty exhausting when the API has a lot to offer, we came a cross Swagger and OpenAPI Generator that creates code automatically. We took a close look on the generated code and provide a small example how to integrate with a Qt Quick HMI.

What is OpenAPI, what is Swagger and OpenAPI Generator?

OpenAPI is a specification (OAS) of an API description format for REST APIs. The current version of the specification is [3.0.3][oas 3.0]. To keep it simple, we stick to the nomenclature used by version 3.0 of the OpenAPI specification onward. An example for an OAS document (in yaml)
swagger: "2.0"
info:
  description: "This is a sample ERP server to simulate distribution of jobs."
  version: "1.0.0"
  title: "ERP job distribution"
host: "localhost:3000"
tags:
- name: "order"
  description: "Everything about an order"
schemes:
- "http"
paths:
  /order:
    get:
      tags:
      - "order"
      summary: "list orders"
      description: "w/o any query parameter all orders will be returned.
      operationId: "listOrders"
      produces:
      - "application/json"
      parameters:
      - name: "status"
        in: "query"
        description: "Status values that need to be considered for filter"
[...] 
Swagger is a set of tools, built around the OAS, that can help you design, document and consume REST APIs. The three major tools provided are
  • Swagger Editor
  • Swagger UI
  • Swagger Generator
The Swagger Editor is a powerful tool that allows you to test and modify your API endpoints in the browser. Here is a screenshot of the Petstore example that Swagger uses. On the left side is the OpenAPI document and on the right are all the endpoints.
Swagger Editor
You can try them with dummy data and you are able to inspect the REST calls. The server response is also shown. If you are interested you can try it out.

The Swagger UI is basically a stripped down version of the Swagger editor. It only allows to test the API endpoints but does not allow to modify them.

The Swagger generator is a commandline tool which translates a Swagger API document into either server- or client-side code for around ~40 different programming languages and frameworks. It also supports the generation of Qt5 API-clients. Unfortunately we found that there are some downsides with the Qt5 support. Examples include

  • Qt coding paradigms are ignored. Most serious strings and containers are passed around as owning pointers instead of taking advantage of Qt COW mechanisms.
  • QNetworkManager isn’t used as it is meant to be (a new instance is created for each request).
  • Code is slightly broken when trying to generate JSON from a received object.
  • Only the outdated Swagger 2.0 documents are supported

We tried to fix these but upstream seems to be dead/deeply asleep. Around that time we noticed that there is a very active fork of the Swagger Generator called OpenAPI generator. Most of these issues mentioned above are fixed there. For the rest of the article we will use a mix of the original Swagger tooling and the forked Generator.

For more information on the OpenAPI Generator, take a look at their website.

An example, the tutorial

As an example that sits at the boundary between IT and OT we defined the following scenario: We have a factory that cuts wooden boards to size. All the orders the factory receives get handled by a central ERP server. Cutting machines are connected to that ERP server. The cutting machines have a Qt Quick HMI which is used by the machine operators to select pending orders and process it. After finishing the job it is marked as „processed“ or it gets deferred if it cannot by completed.

The ERP server provides a REST-API defined via Swagger/OpenAPI. The full OpenAPI document can be found here. It is an OpenAPI 2.0 document, that can be used with swagger as well. The API offers the following functionality.

  • GET /order : list all orders based on an optional list of statuses
  • GET /order/{ID} : find an order object by ID
  • PATCH /order/{ID} : update the status of an order object identified by ID

The rest of the article will provide a walk through how to build that HMI.

Prepare the environment

Prerequisites

We provide the full source code for the example here. To follow along you will need to install:

  • Qt >=v5.12 and a working C++ toolchain
  • Java (optional, required for running the Generator locally)
  • OpenAPI Generator
  • Node.js (for running the test server)

Using the OpenAPI Generator

Generating Code

Using the OpenAPI code generator is pretty simple if you already have the Java runtime installed. In our example your can run these steps from the root of the repository.
wget https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/4.3.1/openapi-generator-cli-4.3.1.jar -O openapi-generator-cli.jar
By running the code generator without any arguments will list all the languages it supports. We’ll use cpp-qt5-client to generate our code:

java -jar openapi-generator-cli.jar
For generating code you need a valid OAS 2.0 or 3.0 document and the name of the generator to use from step 2. As output directory we will use the MachineTerminal directory where our QML application is located in. The code generator will create a subdirectory called client where it places all the generated code:

java -jar openapi-generator-cli.jar generate -i swagger.yaml -g cpp-qt5-client -o MachineTerminal

Inspecting the generated code

Let’s have a look at the generated code. You can find it in the MachineTerminal/client folder.
client/
├── client.pri
├── CMakeLists.txt
├── OAIEnum.h
├── OAIHelpers.cpp
├── OAIHelpers.h
├── OAIHttpFileElement.cpp
├── OAIHttpFileElement.h
├── OAIHttpRequest.cpp
├── OAIHttpRequest.h
├── OAIObject.h
├── OAIOrderApi.cpp
├── OAIOrderApi.h
├── OAIOrder.cpp
├── OAIOrder_dimensions.cpp
├── OAIOrder_dimensions.h
├── OAIOrder.h
├── OAIPartialOrder.cpp
├── OAIPartialOrder.h
├── OAIServerConfiguration.h
└── OAIServerVariable.h 
The first thing to point out is that the generated code contains a client.pri file for integration with Qt’s qmake build system (cmake support is also available). To add the new code to your existing project you just have to include this file in your project file (.pro):
include(client/client.pri) 

This works very well, all the files should instantly show up in your QtCreator and building the project should still work as well.

Which of these files do we need to actually care about and which of these are boilerplate we don’t want to look at?

API

OAIOrderApi.h/.cpp 
The API source files are holding the classes and methods your code will directly interface with. When inspecting these files you will notice two things:
  • the API classes are named by the tags you specified in your OpenAPI document. If you don’t specify a tag it the generator create a OAIDefaultApi class to place these endpoints in.
  • the methods are named by the operationId you set on every endpoint.
The generator has some fallback mechanisms implemented for missing tags and operationId fields.
  • tags Swagger uses tags to group endpoints logically. If you do not specify a tag the endpoint will be included in Swaggers default group. It is exactly the same as if you have added the value „default“ to the tags field. This same behavior is implemented in the Swagger code generator and you well find endpoints without tags in the OAIDefaultAPI class. In case you have specified more than one tag in the tags list that one API endpoint will end up in several API classes.
  • operationId If an operationId is missing the code generator will use the endpoints path and HTTP method to generate a method name for it. You are allowed to use non-ASCII characters inside an operationId. If you used one or more of those the code generator will simply drop those characters to form a valid C++ method name.

To call endpoints on the REST API you just need to call the corresponding method of an API class. If you can/must provide data to an endpoint you simply give it as arguments to these methods. There is no need for you to care about serialization or data transformation.

Because a method call results in a HTTP call to the REST API the handling of the result is asynchronous. Therefor you connect to the corresponding two signals for the method you called to receive the response or error when something goes wrong.

To use the /order endpoint from our example you have to call the method:

void OAIOrderApi::listOrders(QList<QString> status); 
connect to the response signal to receive the result:
void OAIOrderApi::listOrdersSignal(QList<OAIOrder> summary); 
and connect to one of signals for error handling:
// error signal with the data that was received with it
void OAIOrderApi::listOrdersSignalE(QList<OAIOrder> summary, QNetworkReply::NetworkError error_type, QString& error_str);
// error signal with pointer to the failing worker instance
void OAIOrderApi::listOrdersSignalEFull(OAIHttpRequestWorker* worker, QNetworkReply::NetworkError error_type, QString& error_str); 
Because there is no identifier allowing to map a given call to a given completion signal, you should make sure to make calls one after an other and wait for a call to return before the next one is executed.

Models

OAIOrder.h/.cpp
OAIOrder_dimensions.h/.cpp
OAIPartialOrder.h/.cpp 

Models are the data classes the Generator will create for all schemas defined in your OpenAPI document. Each schema results in it’s own class using the name you specified for it in the title field of that schema. In case you used an anonymous schema the name is made up from the parameter it is defined in. If a schema defines a complex property that will end up in it’s own class. The name will be made up from the containing schema and the name of the property that holds the complex type. If you don’t want that you can specify a title in the complex type and the code generator will use it as class name.

Specifying names for many schemas can end up with conflicting names. The code generator detects name clashes between models and appends a integral counter.

Looking into the code itself will show that all models share a common interface with four helper functions to create an object with data from JSON or serialize an object as JSON. The methods to read read data from JSON are not static and will not create a new object but fill the current object with the data read from JSON. Which is a bit odd when knowing Qt’s from* functions that are creating new objects.

The rest

The rest of the generated files contain helper code, wrapper objects and base classes. Most of these classes and functions are used internally by the client code only and you don’t have to worry about.
OAIHelpers.h/.cpp 
These files contain a set of functions that helps the generated code when transforming the data structures to / from JSON
OAIHttpRequest.h/.cpp 
You will find the OAIHttpRequestInput and OAIHttpRequestWorker here. The first will be used to prepare a HTTP request, configure it by setting headers and method as well as setting variables and values to send in the request. The second class is a wrapper around QNetworkManager that handles all the formatting and encoding of OAIHttpRequestInput objects and sends and receives the response from the server.
OAIObject.h 
Defines an interface used as base class for all models we already described.

Using the client in QML

Qt applications can be written either using the traditional C++/QWidget API or using the Qt Quick/QML approach. Qt Quick is a popular choice for device/machine HMIs therefore we also picked it for our example scenario. Unfortunately we were not able to directly hand the API and model classes directly into a QML context as the OpenAPI generated code does not fit this use case. Writing an API wrapper class is done quite fast (when we do not care about type safety and hand over the data as QVariant types). Because the model classes can be serialized back into QJsonObjects this is simple as:
void ErpService::onUpdateOrderWithReturned(OpenAPI::OAIOrder summary)
{
    QVariantMap order = summary->asJsonObject().toVariantMap();
    emit getOrderByIdFinished(order)
} 
order can now be passed (e.g. by signal) to QML and accessed like any other JavaScript object. The other way around to call a method can be written as:
void ErpService::updateOrderWith(qint64 order_id, QVariantMap updates)
{
    OpenAPI::OAIPartialOrder partial;
    partial.fromJsonObject(QJsonObject::fromVariantMap(updates));
    m_orderApi->updateOrderWith(order_id, partial);
} 

Note that you first have to create an instance of the model and fill it afterward with the data from the QJsonObject.

When handling a signal you need to create a deep copy of the data you received, if you want to keep it using it, as it will be deleted as soon as the QEventLoop continues. Here we have implicitly done it by serializing it. But continue using the created model types can be tricky because the models do not provide a copy/move constructor or an assignment operator. Also remember that these models can be nested and you have to copy all the object contained in the model you received. If you do not care about performance you could also use the serialization/deserialization mechanism to copy the data into a new model instance.

Putting all the pieces together we and up with a Qt Quick application which sits on top of a REST API. All the low level code required to interact with the API was auto-generated. To try the example yourself check out the code here. The repository also contains the ERP server.

Qt Quick and Swagger/OpenAPI - a tutorial 1 basysKom, HMI Dienstleistung, Qt, Cloud, Azure

Good practice

Some Do’s and Dont’s for creating your own OpenAPI descriptions.

  • do use tags as they end up as API class name.
  • do name every endpoint (set it’s operationId) as it ends up as method / signal name.
  • do name every schema as it ends up as class name.
  • do not put spaces in model, parameter or any other names. The generator will create broken code for it.
  • do not name schemas like *Api.

Conclusion

The generation of boilerplate code for accessing REST-APIs is a good approach because it generally leads to fewer errors and takes away tedious work. Also the developer does not have to worry about the communication details of the API and can focus on the real tasks. Swagger/OpenAPI provides good tooling to develop, test and validate APIs in an integrated fashion. When working with Qt make sure to use the OpenAPI generator instead of the original project. The Qt5 support is not perfect but works reasonably well.

basysKom is offering consulting, coaching and development services. Feel free to get in contact with us info@basyskom.com. Or just drop a comment below.

References

Picture of Lutz Schönemann

Lutz Schönemann

To be written.

3 Antworten

  1. Great article for Swagger/OpenAPI and Qt, following the interesting and nice presentation occurred during Qt Dev/Des days!

    I am already enrolling in Qt and Swagger/OpenAPI in our project.

    Thanks, basyskom!

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

Weitere Blogartikel

basysKom Newsletter

We collect only the data you enter in this form (no IP address or information that can be derived from it). The collected data is only used in order to send you our regular newsletters, from which you can unsubscribe at any point using the link at the bottom of each newsletter. We will retain this information until you ask us to delete it permanently. For more information about our privacy policy, read Privacy Policy