Introduction
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?
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 Editor
- Swagger UI
- Swagger Generator
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
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
java -jar openapi-generator-cli.jar
java -jar openapi-generator-cli.jar generate -i swagger.yaml -g cpp-qt5-client -o MachineTerminal
Inspecting the generated code
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
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 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.
- 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);
void OAIOrderApi::listOrdersSignal(QList<OAIOrder> summary);
// 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);
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
OAIHelpers.h/.cpp
OAIHttpRequest.h/.cpp
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
Using the client in QML
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.
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.
3 Responses
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!
why we are not able to access any of the source code , and github page is showing 404
Hi,
thanks for the information. We fixed the links.