Panel użytkownika
Nazwa użytkownika:
Hasło:
Nie masz jeszcze konta?
Autor: Grzegorz 'baziorek' Bazior
Inne artykuły

[Oat++] Tworzenie Webservice'ów w C++ z wygenerowaną dokumentacją

[artykuł]

Wprowadzenie

Coraz popularniejsze są web-service'y, które powstają niezależnie, a mogą się komunikować przy pomocy pewnego interfejsu np. REST API. C++ również jest językiem umożliwiającym utworzenie serwera/klienta do komunikacji przez REST API, a dzięki jego szybkości jest to bardzo dobra technologia do tego celu.

Biblioteki do web service'ów w C++

Pisząc ten artykuł, zrobiłem przegląd dostępnych bibliotek do tego celu. Listę bibliotek znalazłem na Stack Overflow, oraz przeszukując internet. Szukałem czegoś open-source o licencji pozwalającej na swobodne wykorzystanie (nie GPL). Biblioteka powinna mieć stosunkowo dużą liczbę commitów, zawierać stosunkowo niedawne commity oraz być w pełni funkcjonalna w C++ (czyli może być napisana w C, byleby miała wrapper w C++ udostępniający wszystkie funkcje). Po przeanalizowaniu zdecydowałem się na bibliotekę Oat++, która poza powyższymi wymaganiami wspiera web service'y (różne adresy URL w ramach tego samego hosta), zawiera moduły do ORM (Object Relational Mapping), a także wspiera generowanie dokumentacji REST przy pomocy Swagger UI.

Biblioteka Oat++ ma również pewną specyfikę projektową, która powoduje, iż jest znacznie ciężej ją ogarnąć - duża liczba pomocniczych makr preprocesora. Stąd ten artykuł sugeruję czytać od początku do końca. Kolejnym zarzutem względem biblioteki jest istnienie tylko jednego, krótkiego tutoriala wprowadzającego w bibliotekę, a resztę trzeba "wykminić" z przykładów, które też są różnie aktualizowane (może występować pewna rozbieżność między wersjami biblioteki).

Analizowane biblioteki, które odrzuciłem

  • Pistache - nie mają jeszcze wersji produkcyjnej (na czas pisania artykułu)
  • cpprestsdk (dawniej Casablanca) - Microsoft zaprzestał rozwijania i napisali, aby go nie używać w nowych projektach
  • RESTinio - ostatni commit sprzed 5 miesięcy, oraz bazuje na C++17 (a mamy kilka standardów do przodu)
  • Crow - już nie utrzymywany
  • ffead-cpp - mało commitów w stosunku do innych
  • Boost.beast - jest to framework bazujący na boost.asio, obecny na wielu konferencjach poświęconych C++, ale wg opisu nie ma jeszcze release'u i interfejs biblioteki może się zmienić
  • Webcc - oparty na Boost.Asio, więc lepiej użyć Boost.beast, który jest oficjalnie wspierany przez społeczność Boost
  • CivetWeb - jest to biblioteka napisana w C, która posiada wrapper do C++, ale nie implementujący wszystkich funkcjonalności
  • libhv - wydaje się obsługiwać różne rodzaje gniazd, ale nie wspiera robienia web-service'ów
  • Restbed - ostatni commit 2 lata temu
  • nghttp2 - biblioteka w C
  • Drogon - biblioteka, wg mnie II wyboru, ale ma mniej commitów niż Oat++
  • userver - biblioteka III wyboru - olbrzymie możliwości, wiele baz danych, brak możliwości wygenerowania dokumentacji

Biblioteka Oat++

Funkcjonalności biblioteki

Wg strony domowej biblioteka ma być wysoce skalowalna oraz efektywna pod względem zasobów, bez dodatkowych zależności (chyba, że podmoduły np. do bazy danych), zawierająca:

Obsługiwane systemy operacyjne

  • Linux
  • BSD
  • MacOS
  • Windows
  • OpenWRT
  • i inne

Wyjaśnienie o co chodzi w web-service'ach (dla osób, które nie pisały takich aplikacji)

Web service'y to aplikacje sieciowe, które umożliwiają komunikację między różnymi systemami poprzez standardowe protokoły internetowe. Najczęściej używanym protokołem jest HTTP, a interfejsy tych usług są zazwyczaj oparte na standardzie REST (Representational State Transfer). Web service'y pozwalają na wymianę danych i funkcjonalności między aplikacjami w sposób niezależny od platformy i języka programowania.

Podstawowe operacje w web service'ach REST opierają się na metodach protokołu HTTP, takich jak:
  • GET - Pobieranie zasobów z serwera, np. pobieranie listy użytkowników lub szczegółów konkretnej książki
  • POST - Tworzenie nowych zasobów na serwerze, np. dodawanie nowego użytkownika lub książki
  • PUT - Aktualizacja istniejących zasobów na serwerze, np. edytowanie danych użytkownika lub książki
  • DELETE - Usuwanie zasobów z serwera, np. usuwanie użytkownika lub książki

Przykłady użycia tych komend w kontekście konkretnego przykładu, opisywanego w ramach tego artykułu - aplikacji do zarządzania książkami:
  • GET /books
     - Pobieranie listy wszystkich książek
  • GET /books/{id}
    - Pobieranie szczegółów książki o danym ID
  • POST /books
    - Dodawanie nowej książki (wymaga danych książki w formacie JSON)
  • PUT /books/{id}
    - Aktualizacja danych książki o danym ID (wymaga danych książki w formacie JSON)
  • DELETE /books/{id}
    - Usuwanie książki o danym ID

W ten sposób, web service'y pozwalają na tworzenie elastycznych i skalowalnych aplikacji, które mogą być łatwo integrowane z innymi systemami.

Cel artykułu

W niniejszym artykule wprowadzam bibliotekę Oat++ wraz z wybranymi modułami, aby przedstawić zrozumiały (i, niestety, lepszy niż w oficjalnych materiałach) tutorial krok po kroku, od instalacji biblioteki do serwera z prostą funkcjonalnością. Obejmuje on obsługę różnych endpointów (ścieżek w adresie URL), obsługujących różne metody protokołu HTTP, na przykładzie bazy danych do przechowywania różnych książek. W dalszych etapach zaprezentowana jest również autoryzacja oraz szyfrowanie. Mając serwer, nie zapominam o kliencie do komunikacji z serwerem przy pomocy protokołów HTTP lub HTTPS. Oczywiście, nasz serwer posiada automatycznie wygenerowaną dokumentację, dostępną w ramach popularnego frameworka webowego Swagger UI.

Instalacja

Według instrukcji na oficjalnej stronie, należy przy pomocy CMake zbudować bibliotekę. Jednakże, preferuję instalację przez menedżer pakietów VCPKG. Aby zainstalować, wystarczy zastosować analogiczne kroki jak w artykule o instalacji zależności przez VCPKG.
Dzięki instalacji przez VCPKG mamy możliwość zainstalowania wielu modułów z zależnościami niemalże jedną komendą, niezależnie od systemu operacyjnego.
Przykładowo, aby zainstalować samo
oatpp
:
# instalacja VCPKG (jeśli go jeszcze nie mamy)
git clone https://github.com/Microsoft/vcpkg.git --depth=1
./vcpkg/bootstrap-vcpkg.sh -disableMetrics

# instalacja samej biblioteki oatpp, bez modułów i zależności:
./vcpkg/vcpkg install oatpp

# jednak na potrzeby pełnego artykułu proponuję również zainstalowanie obsługi dokumentacji Swagger UI:
./vcpkg/vcpkg install oatpp oatpp-swagger

# w późniejszej części artykułu stosuję również szyfrowanie HTTPS, stąd potrzebujemy jeszcze libressl:
./vcpkg/vcpkg install oatpp oatpp-swagger oatpp-libressl
W rezultacie powyższego pojawia się automatycznie wygenerowany tekst do dodania do naszego pliku
CMakeLists.txt
:
 # this is heuristically generated, and may not be correct
find_package(oatpp CONFIG REQUIRED)
target_link_libraries(main PRIVATE oatpp::oatpp oatpp::oatpp-test)
Aby podlinkować powyższe, należy wywołać:
./vcpkg/vcpkg integrate install

Jeśli korzystamy z systemu Windows, to po zawołaniu powyższej komendy mamy już wszystko podlinkowane w środowisku Visual Studio. W innych systemach operacyjnych pojawi się dodatkowy argument do wywołania CMake'a zawierający:
-DCMAKE_TOOLCHAIN_FILE=
.
Możemy utworzyć plik
CMakeLists.txt
:
cmake_minimum_required(VERSION 3.22)

project(OatText LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

add_executable(${PROJECT_NAME} main.cpp)

find_package(oatpp CONFIG REQUIRED)
target_link_libraries(${PROJECT_NAME} PRIVATE oatpp::oatpp oatpp::oatpp-test)
Mając ten plik, należy pamiętać, aby poza systemem Windows użyć opcji
-DCMAKE_TOOLCHAIN_FILE=
 przy generowaniu konfiguracji.

Inne możliwości instalacji przez VCPKG

Zasadniczo sprowadzają się one do wyboru innej konfiguracji dla oat++ przez VCPKG. Możemy wyświetlić możliwe konfiguracje:
./vcpkg/vcpkg search oatpp
oatpp                    1.3.0#2          Modern web framework.
oatpp-consul             1.3.0#1          OAT++ Modern web framework consul module.
oatpp-curl               1.3.0#1          Oat++ Modern web framework curl module to use libcurl as a RequestExecutor...
oatpp-libressl           1.3.0#1          Oat++ libressl module providing secure server and client connection provid...
oatpp-mbedtls            1.3.0            Oat++ Mbed TLS submodule providing secure server and client connection pro...
oatpp-mongo              1.3.0#1          Oat++ MongoDB adapter for Oat++ ORM (native client). It contains DTO to BS...
oatpp-openssl            1.3.0            Oat++ openssl module providing secure server and client connection providers.
oatpp-postgresql         1.3.0#1          Oat++ PostgreSQL adapter for Oat++ ORM (alpha - not all datatypes are supp...
oatpp-sqlite             1.3.0#2          Oat++ SQLite adapter for Oat++ ORM.
oatpp-ssdp               1.3.0#1          Oat++ SSDP (Simple Service Discovery Protocol) submodule.
oatpp-swagger            1.3.0#1          Oat++ OpenApi (Swagger) UI submodule.
oatpp-websocket          1.3.0            Oat++ websocket module.
oatpp-zlib               1.3.0#2          Oat++ functionality for automatically compressing/decompressing content wi...
Mimo wygody użycia VCPKG występuje potencjalnie pewna wada - wersja biblioteki w ramach menedżera pakietów nie zawsze jest najnowsza. Ale każdy może utworzyć Pull Request podbijający wersje, więc jeśli zauważysz taki stan rzeczy, masz możliwość to zmienić ;).

Przykłady użycia biblioteki

Przykłady zaczynają się od krótkiego opisu, następnie przedstawiam efekt końcowy, a dopiero potem kod i opis trudniejszych miejsc w kodzie. Efekt końcowy to często wywołanie udostępnionego API przez komendę curl wraz z odpowiedzią.

Komenda Witaj Świecie

Przykład ten sprowadza się do tego, że tworzymy aplikację, która po uruchomieniu jest serwerem HTTP, nasłuchującym na konkretnym porcie, oraz udostępnia endpoint
/hello
. Pierwszy przykład bazuje na oficjalnym startowym przykładzie (pełny kod przykładu).

Oczekiwany efekt: Kiedy wejdziemy przy pomocy przeglądarki na adres: http://localhost:8000/hello, zobaczymy tekst:
Hello World!

Z kolei, jeśli spróbujemy wejść na inny endpoint, np. http://localhost:8000/user, pojawi się:
server=oatpp/1.3.0
code=404
description=Not Found
message=No mapping for HTTP-method: 'GET', URL: '/user'

Kod dający taki efekt

C/C++
#include <oatpp/web/server/HttpConnectionHandler.hpp>
#include <oatpp/network/Server.hpp>
#include <oatpp/network/tcp/server/ConnectionProvider.hpp>


constexpr const int listeningPort = 8000;
constexpr const char listeningAddress[ ] = "localhost";
constexpr const char routeSubaddress[ ] = "/hello";


class CustomRequestHandler
    : public oatpp::web::server::HttpRequestHandler // (1)
{
public:
   
/// Handle incoming request and return outgoing response.
   
std::shared_ptr < OutgoingResponse > handle( const std::shared_ptr < IncomingRequest > & request ) override
    {
       
return ResponseFactory::createResponse( Status::CODE_200, "Hello World!" );
   
}
}
;

void runApp( const char * programName );

int main( int, char * * argv )
{
   
oatpp::base::Environment::init();
   
   
runApp( argv[ 0 ] );
   
   
oatpp::base::Environment::destroy();
}

void runApp( const char * programName )
{
   
auto router4HttpRequestsRouting = oatpp::web::server::HttpRouter::createShared();
   
   
/// Route GET - "/hello" requests to Handler
   
router4HttpRequestsRouting->route( "GET", routeSubaddress, std::make_shared < CustomRequestHandler >() ); // (2)
   
    /// Create HTTP connection handler with router
   
auto httpConnectionHandler = oatpp::web::server::HttpConnectionHandler::createShared( router4HttpRequestsRouting );
   
   
auto tcpConnectionProvider = oatpp::network::tcp::server::ConnectionProvider::createShared( { listeningAddress, listeningPort, oatpp::network::Address::IP_4 } ); // (3)
   
    /// Create server which takes provided TCP connections and passes them to HTTP connection handler
   
oatpp::network::Server server( tcpConnectionProvider, httpConnectionHandler );
   
   
/// Print info about server port (logging)
   
OATPP_LOGI( programName, "Server running on port %s", tcpConnectionProvider->getProperty( "port" ).getData() );
   
   
server.run();
}

Czas na wyjaśnienie kilku miejsc w programie:
1. Utworzenie handlera, handler powinien zawierać metodę
handle
.
2. Utworzenie endpointa
/hello
, który nasłuchuje na metodzie
GET
 protokołu HTTP i wywołuje wyżej wymieniony handler.
3. Konfiguracja serwera - jaki adres i jaki port.

Efekt uruchomienia programu znajduje się powyżej kodu, natomiast serwer wydrukuje coś na kształt poniższego (dla nazwy programu "OatTest"):
 I |2024-07-23 22:51:26 1721767886239535| OatTest:Server running on port 8000


Przed uruchomieniem naszego serwera trzeba upewnić się, że dany port jest wolny, w przeciwnym razie zostanie wyrzucony wyjątek.

Obsługa JSONów

W webservice'ach na ogół nie operuje się na czystym tekście, ale używa się formatu JSON, z którego łatwo i szybko można wyjąć pewne informacje. Wygodnie jest też, zamiast samodzielnie operować na parsowaniu JSONów, skorzystać z obiektów transferu danych (DTO, ang. Data Transfer Object), które "automatycznie" zamienią reprezentacje z JSONa na obiekt w kodzie C++.
Oczekiwany efekt: jak wejdziemy przy pomocy przeglądarki na adres: http://localhost:8000/hello, to tym razem dostaniemy odpowiedź w formie JSONa (przeglądarka ładniej to zaprezentuje):
{"statusCode":1024,"message":"Hello DTO!"}


Kod z poprzedniego przykładu po modyfikacjach

C/C++
#include <oatpp/parser/json/mapping/ObjectMapper.hpp>
#include <oatpp/web/server/HttpConnectionHandler.hpp>
#include <oatpp/network/Server.hpp>
#include <oatpp/network/tcp/server/ConnectionProvider.hpp>
#include <oatpp/core/macro/codegen.hpp>  
// (1)


constexpr const int listeningPort = 8000;
constexpr const char listeningAddress[ ] = "localhost";
constexpr const char routeSubaddress[ ] = "/hello";

/// Begin DTO code-generation
#include OATPP_CODEGEN_BEGIN(DTO)

/// Message Data-Transfer-Object
class MessageDto
    : public oatpp::DTO // (2)
{
   
DTO_INIT( MessageDto, DTO /* Extends */ )
   
   
DTO_FIELD( Int32, statusCode ); // Status code field
   
DTO_FIELD( String, message ); // Message field
};

/// End DTO code-generation
#include OATPP_CODEGEN_END(DTO)

class CustomRequestHandler2
    : public oatpp::web::server::HttpRequestHandler
{
private:
   
std::shared_ptr < oatpp::data::mapping::ObjectMapper > m_objectMapper;
public:
   
/** Constructor with object mapper.
    * @param objectMapper - object mapper used to serialize objects. */
   
CustomRequestHandler2( const std::shared_ptr < oatpp::data::mapping::ObjectMapper > & objectMapper )
        :
m_objectMapper( objectMapper )
   
{ }
   
   
std::shared_ptr < OutgoingResponse > handle( const std::shared_ptr < IncomingRequest > & request ) override
    {
       
auto message = MessageDto::createShared();
       
message->statusCode = 1024;
       
message->message = "Hello DTO!";
       
return ResponseFactory::createResponse( Status::CODE_200, message, m_objectMapper );
   
}
}
;

void runApp( const char * programName );

int main( int, char * * argv )
{
   
oatpp::base::Environment::init();
   
   
runApp( argv[ 0 ] );
   
   
oatpp::base::Environment::destroy();
}

void runApp( const char * programName )
{
   
auto jsonObjectMapper = oatpp::parser::json::mapping::ObjectMapper::createShared(); // (3)
   
   
auto router4HttpRequestsRouting = oatpp::web::server::HttpRouter::createShared();
   
   
/// Route GET - "/hello" requests to Handler
   
router4HttpRequestsRouting->route( "GET", routeSubaddress, std::make_shared < CustomRequestHandler2 >( jsonObjectMapper ) );
   
   
auto httpConnectionHandler = oatpp::web::server::HttpConnectionHandler::createShared( router4HttpRequestsRouting );
   
   
auto tcpConnectionProvider = oatpp::network::tcp::server::ConnectionProvider::createShared( { listeningAddress, listeningPort, oatpp::network::Address::IP_4 } );
   
   
/// Create server which takes provided TCP connections and passes them to HTTP connection handler
   
oatpp::network::Server server( tcpConnectionProvider, httpConnectionHandler );
   
   
/// Print info about server port (logging)
   
OATPP_LOGI( programName, "Server running on port %s", tcpConnectionProvider->getProperty( "port" ).getData() );
   
   
server.run();
}

Czas na wyjaśnienie kilku miejsc w programie (pomijam wyjaśnione wcześniej fragmenty w poprzednim programie):
1. Włączamy nagłówek odpowiedzialny za generację obiektów DTO.
2. Jest to klasa, wewnątrz której zostaną wygenerowane pewne pola, proszę zobaczyć, że jest ona otoczona
#include OATPP_CODEGEN_BEGIN(DTO)
 i
#include OATPP_CODEGEN_END(DTO)
. Oczywiście takie klasy DTO powinny być w oddzielnych plikach.
3. Tworzymy obiekt mapujący obiekty do JSONów, który to potem jest przekazywany w konstruktorze naszego Handlera.

Komponenty w jednym miejscu + makra preprocesora

Wg opisu w pierwszym tutorialu na stronie domowej zamiast tworzyć pewne rzeczy na piechotę, zalecane jest stosowanie makr, które wiele kroków zrobią automatycznie skracając zapis. A także, wg twórców, wszystkie główne komponenty są inicjalizowane w jednym miejscu, co daje większą łatwość w konfiguracji aplikacji. Poza tym wszystkie praktyczne przykłady później bazują na tych makrach.

Takimi makrami są:
#define OATPP_CREATE_COMPONENT(TYPE_OF_COMPONENT, NAME_OF_COMPONENT_FIELD)
, które rozwija się na postać:
C/C++
#define OATPP_CREATE_COMPONENT(TYPE_OF_COMPONENT, NAME_OF_COMPONENT_FIELD) \
oatpp::Environment::Component NAME_OF_COMPONENT_FIELD = oatpp::Environment::Component<TYPE_OF_COMPONENT>
oraz
#define OATPP_COMPONENT(TYPE_OF_COMPONENT, NAME_OF_VARIABLE, QUALIFIER_NAME)

które tak prawdę powiedziawszy ma formę:
#define OATPP_COMPONENT(TYPE_OF_COMPONENT, ...)
, a ostatni argument jest opcjonalny. Jest on konieczny, gdy zarejestrowanych jest wiele komponentów tego samego typu, wtedy potrzebujemy nazwę.

Poza tym mamy jeszcze makro:
#define ENDPOINT(HTTP_METHOD, PATH_TO_ENDPOINT_WITHOUT_HOST, NAME_OF_GENERATED_METHOD)

a dokładniej:
#define ENDPOINT(HTTP_METHOD, PATH_TO_ENDPOINT_WITHOUT_HOST, ...)
 (ostatni argument jest opcjonalny), ono generuje metodę zwracającą
std::shared_ptr < oatpp::web::protocol::http::outgoing::Response >
.

Możemy tworzyć endpointy na różne metody HTTP, metody HTTP to m.in.:
  • GET
  • POST
  • PUT
  • DELETE
Powyższego makra możemy użyć, jak zamiast dziedziczyć po
oatpp::web::server::HttpRequestHandler
, będziemy dziedziczyć po
oatpp::web::server::api::ApiController
. Będziemy tego sposobu używali w dalszych przykładach.

Oczekiwany efekt: Działanie kodu analogiczne jak wcześniejszego, jedynie zastosowane mechanizmy są bardziej zalecane przez producentów biblioteki. Z różnic widocznych na zewnątrz dodałem drugi endpoint: http://localhost:8000/bye

Kody używane w tym przykładzie

W oparciu o zastosowanie makr tworzy się klasę w pliku
AppComponent.hpp
 np.:
C/C++
#pragma once

#include <oatpp/parser/json/mapping/ObjectMapper.hpp>

#include <oatpp/web/server/HttpConnectionHandler.hpp>
#include <oatpp/network/tcp/server/ConnectionProvider.hpp>

#include <oatpp/core/macro/component.hpp>

constexpr const int listeningPort = 8000;
constexpr const char listeningAddress[ ] = "localhost";
constexpr const char routeSubaddress[ ] = "/hello";

/// Class which creates and holds Application components and registers components in oatpp::base::Environment
class ApplicationComponents
{
public:
   
/// Create ConnectionProvider component which listens on the port
   
OATPP_CREATE_COMPONENT( std::shared_ptr
   
    < oatpp::network::ServerConnectionProvider >, serverConnectionProvider )([ ] {
       
return oatpp::network::tcp::server::ConnectionProvider::createShared( { listeningAddress, listeningPort, oatpp::network::Address::IP_4 } );
   
}() );
   
   
/// Create Router component
   
OATPP_CREATE_COMPONENT( std::shared_ptr < oatpp::web::server::HttpRouter >, httpRouter )([ ] {
       
return oatpp::web::server::HttpRouter::createShared();
   
}() );
   
   
/// Create ConnectionHandler component which uses Router component to route requests
   
OATPP_CREATE_COMPONENT( std::shared_ptr < oatpp::network::ConnectionHandler >, serverConnectionHandler )([ ] {
       
OATPP_COMPONENT( std::shared_ptr < oatpp::web::server::HttpRouter >, router ); // get Router component
       
return oatpp::web::server::HttpConnectionHandler::createShared( router );
   
}() );
   
   
/// Create ObjectMapper component to serialize/deserialize DTOs in Contollers
   
OATPP_CREATE_COMPONENT( std::shared_ptr < oatpp::data::mapping::ObjectMapper >, apiObjectMapper )([ ] {
       
return oatpp::parser::json::mapping::ObjectMapper::createShared();
   
}() );
};

Cały pozostały kod:
C/C++
#include <oatpp/network/Server.hpp>
#include <oatpp/core/macro/codegen.hpp>
#include <oatpp/web/server/api/ApiController.hpp>
#include "AppComponent.hpp"

/// Begin DTO code-generation
#include OATPP_CODEGEN_BEGIN(DTO)

/// Message Data-Transfer-Object
class MessageDto
    : public oatpp::DTO
{
   
DTO_INIT( MessageDto, DTO /* Extends */ )
   
   
DTO_FIELD( Int32, statusCode ); // Status code field
   
DTO_FIELD( String, message ); // Message field
};

/// End DTO code-generation
#include OATPP_CODEGEN_END(DTO)


#include OATPP_CODEGEN_BEGIN(ApiController)  
///< Begin Codegen // (1)

class MyApiController
    : public oatpp::web::server::api::ApiController
{
public:
   
/** Constructor with object mapper.
      * @param objectMapper - default object mapper used to serialize/deserialize DTOs. */
   
MyApiController( OATPP_COMPONENT( std::shared_ptr < ObjectMapper >, objectMapper ) )
        :
oatpp::web::server::api::ApiController( objectMapper )
   
{ }
public:
   
ENDPOINT( "GET", "/hello", root ) // (2)
   
{
       
auto dto = MessageDto::createShared();
       
dto->statusCode = 200;
       
dto->message = "Hello World!";
       
return createDtoResponse( Status::CODE_200, dto );
   
}
   
ENDPOINT( "GET", "/bye", bye )
   
{
       
auto dto = MessageDto::createShared();
       
dto->statusCode = 200;
       
dto->message = "Bye bye!";
       
return createDtoResponse( Status::CODE_200, dto );
   
}
}
;

#include OATPP_CODEGEN_END(ApiController)  ///< End Codegen

void runApp( const char * programName );

int main( int, char * * argv )
{
   
oatpp::base::Environment::init();
   
   
runApp( argv[ 0 ] );
   
   
oatpp::base::Environment::destroy();
}

void runApp( const char * programName )
{
   
/// Register Components in scope of run() method
   
ApplicationComponents components;
   
   
/// Get router component
   
OATPP_COMPONENT( std::shared_ptr < oatpp::web::server::HttpRouter >, router4HttpRequestsRouting );
   
   
/// Create MyApiController and add all of its endpoints to router
   
auto myApiController = std::make_shared < MyApiController >(); // (3)
   
router4HttpRequestsRouting->addController( myApiController );
   
   
/// Get connection handler component
   
OATPP_COMPONENT( std::shared_ptr < oatpp::network::ConnectionHandler >, connectionHandler );
   
   
/// Get connection provider component
   
OATPP_COMPONENT( std::shared_ptr < oatpp::network::ServerConnectionProvider >, connectionProvider );
   
   
/// Create server which takes provided TCP connections and passes them to HTTP connection handler
   
oatpp::network::Server server( connectionProvider, connectionHandler );
   
   
/// Priny info about server port
   
OATPP_LOGI( programName, "Server running on port %s", connectionProvider->getProperty( "port" ).getData() );
   
   
server.run();
}

Komentarz do pewnych miejsc w kodzie:
1. Używam ApiController, który zarządza endpointami. Na uwagę zasługuje fakt, że przed tą klasą i po niej musimy aktywować i deaktywować odpowiednie makra preprocesora, odpowiednio
#include OATPP_CODEGEN_BEGIN(ApiController)
 i
#include OATPP_CODEGEN_END(ApiController)
.
2. Teraz w wygodny sposób można tworzyć endpointy.
3. Tutaj zamiast tworzyć endpoint w funkcji
runApp
 tworzy się jedynie kontroler.

Baza książek: dodawanie, pobieranie, usuwanie

Teraz wiemy już wystarczająco dużo, aby zrobić coś konkretniejszego - bazę książek, do której można dodawać, pobierać informacje, poprawiać czy usuwać. Co prawda, dla uproszczenia będę korzystał ze zwykłej
std::map
 zamiast z bazy danych.

Oczekiwany efekt:
  • Dodanie książki (dodałem więcej książek przez wielokrotne wywołanie tej komendy):
    curl -X POST "http://localhost:8000/books" -H "Content-Type: application/json" -d '{
    "title": "Piec jezykow milosci", "author": "Gary Chapman"
    }'
    {"id":0,"title":"Piec jezykow milosci","author":"Gary Chapman"}
  • Pobranie konkretnej książki:
    curl -X GET "http://localhost:8000/books/0"
    {"id":0,"title":"Piec jezykow milosci","author":"Gary Chapman"}
    curl -X GET "http://localhost:8000/books/1000"
    Book not found
  • Pobranie wszystkich książek:
    curl -X GET "http://localhost:8000/books"
    [{"id":0,"title":"Piec jezykow milosci","author":"Gary Chapman"},{"id":0,"title":"4 funkcje mezczyzny","author":"Jacek Pulikowski"},{"id":0,"title":"Krotki przewodnik po rodzinie","author":"Piotr Pawlukiewicz"}]
  • Usunięcie książki:
    curl -X DELETE "http://localhost:8000/books/0"
    Book successfully deleted
    curl -X DELETE "http://localhost:8000/books/0"
    server=oatpp/1.3.0
    code=404
    description=Not Found
    message=Book not deleted. Perhaps no such Book in the Database
  • Modyfikacja istniejącej książki:
    curl -X PUT "http://localhost:8000/books/1" -d '{ "title": "Cztery funkcje mezczyzny"}'
    {"id":1,"title":"Cztery funkcje mezczyzny","author":"Jacek Pulikowski"}

Kod bazy książek v1

A oto kod, który daje takie możliwości (natomiast plik
AppComponent.hpp
 jest dokładnie ten sam co ostatnio, dlatego nie ponawiam):
C/C++
#include <map>
#include <atomic>
#include <mutex>
#include <oatpp/network/Server.hpp>
#include <oatpp/core/macro/codegen.hpp>
#include <oatpp/web/server/api/ApiController.hpp>
#include <oatpp/core/macro/component.hpp>
#include "AppComponent.hpp"


/// Begin DTO code-generation
#include OATPP_CODEGEN_BEGIN(DTO)

/// Book Data-Transfer-Object
class BookDto
    : public oatpp::DTO // (1)
{
   
DTO_INIT( BookDto, DTO /* Extends */ )
   
   
DTO_FIELD( UInt64, id );
   
DTO_FIELD( String, title, "title" );
   
DTO_FIELD( String, author, "author" );
   
public:
   
BookDto() = default;
   
   
BookDto( v_uint64 pId, const String & pTitle, const String & pAuthor )
        :
id( pId )
       
, title( pTitle )
       
, author( pAuthor )
   
{ }
}
;

/// End DTO code-generation
#include OATPP_CODEGEN_END(DTO)

struct Book // (2)
{
   
v_uint64 id;
   
oatpp::String title, author;
   
   
Book() = default;
   
explicit Book( const oatpp::Object < BookDto > & bookDto )
   
{
       
if( bookDto->id )
       
{
           
id = bookDto->id;
       
}
       
title = bookDto->title;
       
author = bookDto->author;
   
}
   
   
oatpp::Object < BookDto > toDto() const
   
{
       
auto dto = BookDto::createShared();
       
dto->id = id;
       
dto->title = title;
       
dto->author = author;
       
return dto;
   
}
}
;

std::map < v_uint64, Book > books; // (3)
std::atomic < v_uint64 > bookIdCounter { 0 };
std::mutex booksMutex;

#include OATPP_CODEGEN_BEGIN(ApiController)  ///< Begin Codegen

class BookController
    : public oatpp::web::server::api::ApiController
{
public:
   
BookController( OATPP_COMPONENT( std::shared_ptr < ObjectMapper >, objectMapper ) /* Inject object mapper */ )
        :
oatpp::web::server::api::ApiController( objectMapper )
   
{ }
   
   
ENDPOINT( "POST", "/books", createBook,
   
BODY_DTO( Object < BookDto >, bookDto ) ) // (4)
   
{
       
OATPP_ASSERT_HTTP( bookDto->title, Status::CODE_400, "'title' is required!" );
       
const auto bookId = bookIdCounter++;
       
Book book { bookDto };
       
book.id = bookId;
       
       
std::lock_guard < std::mutex > lock( booksMutex );
       
books.insert( std::make_pair( bookId, std::move( book ) ) );
       
bookDto->id = bookId;
       
return createDtoResponse( Status::CODE_200, bookDto );
   
}
   
   
ENDPOINT( "PUT", "/books/{bookId}", putBook,
   
PATH( UInt64, bookId ), // (5)
   
BODY_DTO( Object < BookDto >, bookDto ) )
   
{
       
std::lock_guard < std::mutex > lock( booksMutex );
       
try
       
{
           
auto & book = books.at( bookId );
           
if( bookDto->title && bookDto->title->size() )
               
 book.title = bookDto->title;
           
           
if( bookDto->author && bookDto->author->size() )
               
 book.author = bookDto->author;
           
           
return createDtoResponse( Status::CODE_200, book.toDto() );
       
}
       
catch( const std::out_of_range & )
       
{
           
return createResponse( Status::CODE_404, "Book not found" );
       
}
    }
   
   
ENDPOINT( "GET", "/books/{bookId}", getBookById,
   
PATH( UInt64, bookId ) )
   
{
       
std::lock_guard < std::mutex > lock( booksMutex );
       
try
       
{
           
auto book = books.at( bookId );
           
Object < BookDto > bookDto = book.toDto();
           
return createDtoResponse( Status::CODE_200, bookDto );
       
}
       
catch( const std::out_of_range & )
       
{
           
return createResponse( Status::CODE_404, "Book not found" );
       
}
    }
   
   
ENDPOINT( "GET", "/books", getBooks )
   
{
       
std::lock_guard < std::mutex > lock( booksMutex );
       
auto booksDtos = List < Object < BookDto >>::createShared(); // (6)
       
for( const auto &[ bookId, book ]: books )
       
{
           
const auto bookDto = book.toDto();
           
booksDtos->push_back( std::move( bookDto ) );
       
}
       
return createDtoResponse( Status::CODE_200, booksDtos );
   
}
   
   
ENDPOINT( "DELETE", "/books/{bookId}", deleteBook,
   
PATH( UInt64, bookId ) )
   
{
       
std::lock_guard < std::mutex > lock( booksMutex );
       
bool success = books.erase( bookId ) > 0;
       
OATPP_ASSERT_HTTP( success, Status::CODE_404, "Book not deleted. Perhaps no such Book in the Database" );
       
return createResponse( Status::CODE_200, "Book successfully deleted" );
   
}
}
;

#include OATPP_CODEGEN_END(ApiController)  ///< End Codegen

void runApp( const char * programName );

int main( int, char * * argv )
{
   
oatpp::base::Environment::init();
   
   
runApp( argv[ 0 ] );
   
   
oatpp::base::Environment::destroy();
}

void runApp( const char * programName )
{
   
/// Register Components in scope of run() method
   
ApplicationComponents components;
   
   
/// Get router component
   
OATPP_COMPONENT( std::shared_ptr < oatpp::web::server::HttpRouter >, router4HttpRequestsRouting );
   
   
/// Create MyApiController and add all of its endpoints to router
   
auto myApiController = std::make_shared < BookController >();
   
router4HttpRequestsRouting->addController( myApiController );
   
   
/// Get connection handler component
   
OATPP_COMPONENT( std::shared_ptr < oatpp::network::ConnectionHandler >, connectionHandler );
   
   
/// Get connection provider component
   
OATPP_COMPONENT( std::shared_ptr < oatpp::network::ServerConnectionProvider >, connectionProvider );
   
   
/// Create server which takes provided TCP connections and passes them to HTTP connection handler
   
oatpp::network::Server server( connectionProvider, connectionHandler );
   
   
/// Print info about server port
   
OATPP_LOGI( programName, "Server running on port %s", connectionProvider->getProperty( "port" ).getData() );
   
   
server.run();
}

Wyjaśnienie trudniejszych fragmentów powyższego programu:
1. Teraz obiektem transferu danych jest książka, która posiada tytuł i autora.
2. Oprócz obiektu transferu danych jest potrzebna struktura trzymająca te dane w systemie. Oczywiście jeśli byśmy skorzystali z mechanizmu mapowania obiektowo-relacyjnego (ORM) z bazą danych to nie musieli byśmy tworzyć zduplikowanych obiektów w kodzie C++.
3. W tym miejscu tworzę bazę książek, która jest globalną mapą. Poniżej jest atomowy licznik IDków, aby przy równoczesnym dodawaniu wielu książek licznik zadziałał poprawnie.
4. Wewnątrz makra
ENDPOINT
 używam również makra
BODY_DTO
, które podaną wartość JSONa zamieni na obiekt, bez konieczności robienia tego przez nas ręcznie.
Poniżej jest makro
OATPP_ASSERT_HTTP
, które w razie niespełnienia warunku zwróci informacje o błędnej odpowiedzi (w naszym przypadku przy niepodaniu tytułu).
5. Kolejne makro
PATH
 umożliwia parsowanie fragmentu wyjmując z niego zmienną odpowiedniego typu, dzięki temu my nie musimy tego robić ręcznie.
6. Mimo iż
oatpp::List
 zachowuje się niby jak
std::list
 to są pewne różnice, choćby taka, że trzeba zmienną zainicjalizować specjalnie.

Automatyczna dokumentacja Swagger UI do bazy książek

W powyższym kodzie utworzyliśmy service do przechowywania książek, brakuje nam w nim jeszcze dokumentacji, która może być wygenerowana automatycznie, po dodaniu pewnych drobiazgów w kodzie. Dokumentacja ta jest używana w ramach mechanizmu Swagger UI, gdzie możemy nie tylko podpatrzyć dostępne komendy, ale jeszcze je wykonać z poziomu przeglądarki widząc podgląd wysłanych komend i otrzymanej odpowiedzi. Poniższy kod bazuje na oficjalnym przykładzie Oatpp-CRUD, jednakże bez mechanizmu bazy danych, oraz na oficjalnym opisie modułu oatpp-swagger.
Oczekiwany efekt:
Po wejściu na stronę: http://localhost:8000/swagger/ui (gdy wszystko mamy dobrze skonfigurowane) powinniśmy zobaczyć ekran podobny do:
Rozwinięta komenda API po kliknięciu w jedną z komend
Rozwinięta komenda API po kliknięciu w jedną z komend
Jak klikniemy "Try it out", to pojawi się nam możliwość wysłania tej komendy z podaniem argumentów, oraz po wysłaniu otrzymamy odpowiedź, czyli zobaczymy coś takiego jak:
Rozwinięta komenda API po wysłaniu komendy do serwera
Rozwinięta komenda API po wysłaniu komendy do serwera
Poza tym na samym dole mamy możliwość podejrzenia, jakie pola zawierają nasze obiekty transferu danych DTO.

Zademonstrowałem również statyczną stronę HTML, która jest dostępna pod endpointem-rootem: http://localhost:8000/, wyświetlona strona HTML to:
<html lang='pl'>
<head>
  <meta charset=utf-8/>
</head>
<body>
  <p>Witaj w bazie ksiazek!</p>
  <a href='swagger/ui'>Zobacz dokumentacje wygenerowana przez Swagger-UI</a>
</body>
</html>

Kod potrzebny do osiągnięcia powyższego celu

Zasadniczo większość kodu jest taka, jak do tej pory, jednakże występują pewne rozbieżności. Po pierwsze musimy mieć zainstalowany oatpp-swagger, który musimy dołączyć w pliku
CMakeLists.txt
. Przykładowy plik:
cmake_minimum_required(VERSION 3.22)

project(OatText LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

add_executable(${PROJECT_NAME} main.cpp AppComponent.hpp SwaggerComponent.hpp)

find_package(oatpp CONFIG REQUIRED)
find_package(oatpp-swagger CONFIG REQUIRED)
target_link_libraries(${PROJECT_NAME} PRIVATE oatpp::oatpp oatpp::oatpp-swagger)

Teraz czas na zmiany w kodzie. Jedną z nich jest stworzenie klasy zawierającej globalne informacje o API, plik nazywa się
SwaggerComponent.hpp
, a oto jego zawartość:
C/C++
#pragma once

#include <oatpp-swagger/Model.hpp>
#include <oatpp-swagger/Resources.hpp>
#include <oatpp/core/macro/component.hpp>

/** Swagger ui is served at
 *  http://host:port/swagger/ui */
class SwaggerComponent
{
public:
   
/// General API docs info
   
OATPP_CREATE_COMPONENT( std::shared_ptr < oatpp::swagger::DocumentInfo >, swaggerDocumentInfo )([ ]
   
{
       
oatpp::swagger::DocumentInfo::Builder builder;
       
       
builder
        .setTitle( "Book API" )
        .
setDescription( "Example Book API with Swagger Documentation" )
        .
setVersion( "1.0" )
        .
setContactName( "Example Contact" )
        .
setContactUrl( "https://example.com" )
        .
setLicenseName( "MIT" )
        .
setLicenseUrl( "https://opensource.org/licenses/MIT" )
        .
addServer( "http://localhost:8000", "server on localhost" );
       
       
return builder.build();
   
}() );
   
   
/// Swagger-Ui Resources (<oatpp-examples>/lib/oatpp-swagger/res)
   
OATPP_CREATE_COMPONENT( std::shared_ptr < oatpp::swagger::Resources >, swaggerResources )([ ]
   
{
       
// Make sure to specify correct full path to oatpp-swagger/res folder !!!
       
constexpr const char OATPP_SWAGGER_RES_PATH[ ] = "/home/agh/Desktop/tmp/vcpkg/installed/x64-linux/include/oatpp-1.3.0/bin/oatpp-swagger/res";
       
return oatpp::swagger::Resources::loadResources( OATPP_SWAGGER_RES_PATH ); // (0)
   
}() );
};
W powyższym kodzie na uwagę zasługuje jedynie ścieżka do zasobów
( 0 )
 oraz linijka wyżej. Jest to ścieżka, gdzie znajdują się zasoby Swagger, który przypomnę - jest frameworkiem webowym, dlatego potrzebuje pliki CSS, JavaScript i HTML. W oficjalnych przykładach stałą
OATPP_SWAGGER_RES_PATH
 ustawiają w CMake'u w następujący sposób:
add_definitions(-DOATPP_SWAGGER_RES_PATH="${oatpp-swagger_INCLUDE_DIRS}/../bin/oatpp-swagger/res")
, ale dużo zależy od tego gdzie znajduje się ten katalog w naszym projekcie.

Również plik
AppComponent.hpp
 musimy lekko zmienić:
C/C++
#pragma once

#include <oatpp/parser/json/mapping/ObjectMapper.hpp>

#include <oatpp/web/server/HttpConnectionHandler.hpp>
#include <oatpp/network/tcp/server/ConnectionProvider.hpp>

#include <oatpp/core/macro/component.hpp>
#include "SwaggerComponent.hpp"  
// (1)


constexpr const int listeningPort = 8000;
constexpr const char listeningAddress[ ] = "localhost";
constexpr const char routeSubaddress[ ] = "/hello";

/// Class which creates and holds Application components and registers components in oatpp::base::Environment
class ApplicationComponents
{
public:
   
SwaggerComponent swaggerComponent; // (2)
   
    /// Create ConnectionProvider component which listens on the port
   
OATPP_CREATE_COMPONENT( std::shared_ptr < oatpp::network::ServerConnectionProvider >, serverConnectionProvider )([ ] {
       
return oatpp::network::tcp::server::ConnectionProvider::createShared( { listeningAddress, listeningPort, oatpp::network::Address::IP_4 } );
   
}() );
   
   
/// Create Router component
   
OATPP_CREATE_COMPONENT( std::shared_ptr < oatpp::web::server::HttpRouter >, httpRouter )([ ] {
       
return oatpp::web::server::HttpRouter::createShared();
   
}() );
   
   
/// Create ConnectionHandler component which uses Router component to route requests
   
OATPP_CREATE_COMPONENT( std::shared_ptr < oatpp::network::ConnectionHandler >, serverConnectionHandler )([ ] {
       
OATPP_COMPONENT( std::shared_ptr < oatpp::web::server::HttpRouter >, router ); // get Router component
       
return oatpp::web::server::HttpConnectionHandler::createShared( router );
   
}() );
   
   
/// Create ObjectMapper component to serialize/deserialize DTOs in Contoller's API
   
OATPP_CREATE_COMPONENT( std::shared_ptr < oatpp::data::mapping::ObjectMapper >, apiObjectMapper )([ ] {
       
return oatpp::parser::json::mapping::ObjectMapper::createShared();
   
}() );
};

W powyższym kodzie musimy włączyć wcześniej wspomniany plik konfiguracyjny Swaggera
( 1 )
, oraz utworzyć instancję tej klasy w klasie
ApplicationComponents
 
( 2 )
.

Wreszcie czas na resztę kodu (która też powinna być podzielona na pliki):
C/C++
#include <map>
#include <atomic>
#include <mutex>
#include <oatpp/network/Server.hpp>
#include <oatpp/core/macro/codegen.hpp>
#include <oatpp/web/server/api/ApiController.hpp>
#include <oatpp/core/macro/component.hpp>
#include <oatpp-swagger/Controller.hpp>
#include "AppComponent.hpp"


/// Begin DTO code-generation
#include OATPP_CODEGEN_BEGIN(DTO)

/// Book Data-Transfer-Object
class BookDto
    : public oatpp::DTO
{
   
DTO_INIT( BookDto, DTO /* Extends */ )
   
   
DTO_FIELD( UInt64, id );
   
DTO_FIELD( String, title, "title" );
   
DTO_FIELD( String, author, "author" );
   
public:
   
BookDto() = default;
   
   
BookDto( v_uint64 pId, const String & pTitle, const String & pAuthor )
        :
id( pId )
       
, title( pTitle )
       
, author( pAuthor )
   
{ }
}
;

/// End DTO code-generation
#include OATPP_CODEGEN_END(DTO)

struct Book
{
   
v_uint64 id;
   
oatpp::String title, author;
   
   
Book() = default;
   
explicit Book( const oatpp::Object < BookDto > & bookDto )
   
{
       
if( bookDto->id )
       
{
           
id = bookDto->id;
       
}
       
title = bookDto->title;
       
author = bookDto->author;
   
}
   
   
oatpp::Object < BookDto > toDto() const
   
{
       
auto dto = BookDto::createShared();
       
dto->id = id;
       
dto->title = title;
       
dto->author = author;
       
return dto;
   
}
}
;

std::map < v_uint64, Book > books;
std::atomic < v_uint64 > bookIdCounter { 0 };
std::mutex booksMutex;

#include OATPP_CODEGEN_BEGIN(ApiController)  ///< Begin Codegen

class BookController
    : public oatpp::web::server::api::ApiController
{
public:
   
static std::shared_ptr < BookController > createShared(
   
OATPP_COMPONENT( std::shared_ptr < ObjectMapper >, objectMapper ) // Inject objectMapper component here as default parameter
   
) {
       
return std::make_shared < BookController >( objectMapper );
   
}
   
   
BookController( OATPP_COMPONENT( std::shared_ptr < ObjectMapper >, objectMapper ) /* Inject object mapper */ )
        :
oatpp::web::server::api::ApiController( objectMapper )
   
{ }
   
   
ENDPOINT_INFO( createBook ) // (3)
   
{
       
info->summary = "Create new book";
       
info->addConsumes < Object < BookDto >>( "application/json" );
       
info->addResponse < Object < BookDto >>( Status::CODE_200, "application/json" );
       
info->pathParams[ "author" ].required = true;
       
info->pathParams[ "title" ].required = true;
       
info->pathParams[ "title" ].name = "Title of the book";
       
info->pathParams[ "title" ].description = "Official title of book in polish language";
       
info->pathParams[ "title" ].allowEmptyValue = false;
       
// info->pathParams["title"].deprecated = false; // I don't need this
   
}
   
ENDPOINT( "POST", "/books", createBook,
   
BODY_DTO( Object < BookDto >, bookDto ) )
   
{
       
OATPP_ASSERT_HTTP( bookDto->title, Status::CODE_400, "'title' is required!" );
       
OATPP_ASSERT_HTTP( bookDto->author, Status::CODE_400, "'author' is required!" );
       
const auto bookId = bookIdCounter++;
       
Book book { bookDto };
       
book.id = bookId;
       
       
std::lock_guard < std::mutex > lock( booksMutex );
       
books.insert( std::make_pair( bookId, std::move( book ) ) );
       
bookDto->id = bookId;
       
return createDtoResponse( Status::CODE_200, bookDto );
   
}
   
   
ENDPOINT_INFO( putBook )
   
{
       
info->summary = "Update Book by bookId";
       
info->addConsumes < Object < BookDto >>( "application/json" );
       
info->addResponse < Object < BookDto >>( Status::CODE_200, "application/json" );
       
info->addResponse < String >( Status::CODE_404, "text/plain" );
       
// params specific
       
info->pathParams[ "bookId" ].description = "Book Identifier";
   
}
   
ENDPOINT( "PUT", "/books/{bookId}", putBook,
   
PATH( UInt64, bookId ),
   
BODY_DTO( Object < BookDto >, bookDto ) )
   
{
       
std::lock_guard < std::mutex > lock( booksMutex );
       
try
       
{
           
auto & book = books.at( bookId );
           
if( bookDto->title && bookDto->title->size() )
               
 book.title = bookDto->title;
           
           
if( bookDto->author && bookDto->author->size() )
               
 book.author = bookDto->author;
           
           
return createDtoResponse( Status::CODE_200, book.toDto() );
       
}
       
catch( const std::out_of_range & )
       
{
           
return createResponse( Status::CODE_404, "Book not found" );
       
}
    }
   
   
ENDPOINT_INFO( getBookById )
   
{
       
info->summary = "Get one Book by bookId";
       
info->addResponse < Object < BookDto >>( Status::CODE_200, "application/json" );
       
info->addResponse < String >( Status::CODE_404, "text/plain" );
       
// params specific
       
info->pathParams[ "bookId" ].description = "Book Identifier";
   
}
   
ENDPOINT( "GET", "/books/{bookId}", getBookById,
   
PATH( UInt64, bookId ) )
   
{
       
std::lock_guard < std::mutex > lock( booksMutex );
       
try
       
{
           
auto book = books.at( bookId );
           
Object < BookDto > bookDto = book.toDto();
           
return createDtoResponse( Status::CODE_200, bookDto );
       
}
       
catch( const std::out_of_range & )
       
{
           
return createResponse( Status::CODE_404, "Book not found" );
       
}
    }
   
   
ENDPOINT_INFO( getBooks )
   
{
       
info->summary = "Get all stored books";
       
info->addResponse < List < Object < BookDto >> >( Status::CODE_200, "application/json" );
   
}
   
ENDPOINT( "GET", "/books", getBooks )
   
{
       
std::lock_guard < std::mutex > lock( booksMutex );
       
auto booksDtos = List < Object < BookDto >>::createShared();
       
for( const auto &[ bookId, book ]: books )
       
{
           
const auto bookDto = book.toDto();
           
booksDtos->push_back( std::move( bookDto ) );
       
}
       
return createDtoResponse( Status::CODE_200, booksDtos );
   
}
   
   
ENDPOINT_INFO( deleteBook )
   
{
       
// general
       
info->summary = "Delete Book by bookId";
       
info->addResponse < String >( Status::CODE_200, "text/plain" );
       
info->addResponse < String >( Status::CODE_404, "text/plain" );
       
// params specific
       
info->pathParams[ "bookId" ].description = "Book Identifier";
   
}
   
ENDPOINT( "DELETE", "/books/{bookId}", deleteBook,
   
PATH( UInt64, bookId ) )
   
{
       
std::lock_guard < std::mutex > lock( booksMutex );
       
bool success = books.erase( bookId ) > 0;
       
OATPP_ASSERT_HTTP( success, Status::CODE_404, "Book not deleted. Perhaps no such Book in the Database" );
       
return createResponse( Status::CODE_200, "Book successfully deleted" );
   
}
}
;

class StaticController
    : public oatpp::web::server::api::ApiController // (4)
{
public:
   
StaticController( const std::shared_ptr < ObjectMapper > & objectMapper )
        :
oatpp::web::server::api::ApiController( objectMapper )
   
{ }
   
   
static std::shared_ptr < StaticController > createShared(
   
OATPP_COMPONENT( std::shared_ptr < ObjectMapper >, objectMapper ) /* Inject objectMapper component here as default parameter */ )
   
{
       
return std::make_shared < StaticController >( objectMapper );
   
}
   
   
ENDPOINT( "GET", "/", root ) {
       
const char * html =
       
R"(<html lang='pl'> <head> <meta charset=utf-8/> </head> <body> <p>Witaj w bazie ksiazek!</p> <a href='swagger/ui'>Zobacz dokumentacje wygenerowana przez Swagger-UI</a> </body> </html>)";
       
auto response = createResponse( Status::CODE_200, html );
       
response->putHeader( Header::CONTENT_TYPE, "text/html" );
       
return response;
   
}
}
;


#include OATPP_CODEGEN_END(ApiController)  ///< End Codegen

void runApp( const char * programName );

int main( int, char * * argv )
{
   
oatpp::base::Environment::init();
   
   
runApp( argv[ 0 ] );
   
   
oatpp::base::Environment::destroy();
}

void runApp( const char * programName )
{
   
/// Register Components in scope of run() method
   
ApplicationComponents components;
   
   
/// Get router component
   
OATPP_COMPONENT( std::shared_ptr < oatpp::web::server::HttpRouter >, router4HttpRequestsRouting );
   
   
auto bookController = router4HttpRequestsRouting->addController( BookController::createShared() ); // (5)
   
auto bookEndpoints = bookController->getEndpoints();
   
   
oatpp::web::server::api::Endpoints docEndpoints;
   
docEndpoints.append( bookEndpoints );
   
   
router4HttpRequestsRouting->addController( oatpp::swagger::Controller::createShared( docEndpoints ) ); // (6)
   
router4HttpRequestsRouting->addController( StaticController::createShared() ); // (7)
   
    /// Create MyApiController and add all of its endpoints to router
   
auto myApiController = std::make_shared < BookController >();
   
router4HttpRequestsRouting->addController( myApiController );
   
   
/// Get connection handler component
   
OATPP_COMPONENT( std::shared_ptr < oatpp::network::ConnectionHandler >, connectionHandler );
   
   
/// Get connection provider component
   
OATPP_COMPONENT( std::shared_ptr < oatpp::network::ServerConnectionProvider >, connectionProvider );
   
   
/// Create server which takes provided TCP connections and passes them to HTTP connection handler
   
oatpp::network::Server server( connectionProvider, connectionHandler );
   
   
/// Print info about server port
   
OATPP_LOGI( programName, "Server running on port %s", connectionProvider->getProperty( "port" ).getData() );
   
   
server.run();
}

Wyjaśnienie pewnych nowych aspektów kodu:
3. Przy pomocy makra
ENDPOINT_INFO
 tworzę dokumentację dla poszczególnych komend/zapytań do API. Dla każdego z parametrów mamy możliwość konfiguracyjną. W pierwszym przykładzie
( 3 )
 zawarłem wszystkie dostępne opcje.
4. Utworzyłem dodatkowy kontroler, który dla endpointa root (czyli w tym przypadku: http://localhost:8000) wyświetla stronę o statycznej treści.
5. Aby dodać nasze endpointy (czyli podścieżki URL) do wygenerowanej dokumentacji, musimy najpierw stworzyć instancję BookController i dodać ją do routera przy pomocy metody
addController
. Następnie pobieramy listę endpointów z naszego kontrolera za pomocą metody
getEndpoints
. Te endpointy są następnie dodawane do obiektu
docEndpoints
, który przechowuje wszystkie endpointy, które mają być udokumentowane.
6. Aby wygenerować dokumentację Swagger dla naszego API, dodajemy kontroler
oatpp::swagger::Controller
 do routera. Kontroler ten generuje i udostępnia dokumentację Swagger na podstawie dostarczonych endpointów. W naszym przypadku przekazujemy do niego
docEndpoints
, które zawierają endpointy z
BookController
.
7. Dodatkowo dodajemy do routera
StaticController
, który obsługuje endpoint root ("/") i zwraca stronę HTML o statycznej treści. Jest to przydatne, aby użytkownicy mogli łatwo znaleźć link do dokumentacji Swagger UI bezpośrednio z głównej strony naszej aplikacji.

Autoryzacja/logowanie użytkowników (bazowa identyfikacja, ang. "basic")

W poprzednim przykładzie udostępniliśmy funkcje API do zarządzania książkami. Nie wszystkie operacje powinny być jednak dostępne publicznie. Pewne funkcje, takie jak dodawanie, edytowanie czy usuwanie książek, wymagają zalogowania się za pomocą loginu i hasła, podczas gdy podgląd wszystkich książek nie wymaga logowania.
Oat++ wspiera różne sposoby autoryzacji (inne możliwości autoryzacji użytkowników).

Oczekiwany efekt:
Jeśli spróbujemy dodać książkę bez zalogowania, operacja zostanie odrzucona:
curl -X POST "http://localhost:8000/books" -H "Content-Type: application/json" -d '{  "title": "Krotki przewodnik po rodzinie", "author": "Piotr Pawlukiewicz"}'
server=oatpp/1.3.0
code=401
description=Unauthorized
message=Authorization Required
Logowanie odbywa się poprzez podanie loginu i hasła zakodowanych w base64:
echo -n "my_user:my-secret-password" | base64
bXlfdXNlcjpteS1zZWNyZXQtcGFzc3dvcmQ=
Czyli komendę dodawania książki należy wywołać w następujący sposób:
curl -X POST "http://localhost:8000/books" -H "Content-Type: application/json" -H "Authorization: Basic $(echo -n 'my_user:my-secret-password' | base64)"  -d '{  "title": "Krotki przewodnik po rodzinie", "author": "Piotr Pawlukiewicz"}'
{"id":0,"title":"Krotki przewodnik po rodzinie","author":"Piotr Pawlukiewicz"}
# alternatywnie (dla powyższego loginu i hasła):
curl -X POST "http://localhost:8000/books" -H "Content-Type: application/json" -H "Authorization: Basic bXlfdXNlcjpteS1zZWNyZXQtcGFzc3dvcmQ="  -d '{  "title": "Krotki przewodnik po rodzinie", "author": "Piotr Pawlukiewicz"}'
Pobranie książki nie wymaga logowania:
curl -X GET "http://localhost:8000/books"
[{"id":0,"title":"Krotki przewodnik po rodzinie","author":"Piotr Pawlukiewicz"}]
Pozostałe komendy działają analogicznie jak wcześniej, z tym że część z nich teraz wymaga logowania.

Strona z dokumentacją również ulegnie lekkim zmianom:
Strona główna dokumentacji Swagger z aktywną autoryzacją
Strona główna dokumentacji Swagger z aktywną autoryzacją
Pozostałe obrazy wyglądają analogicznie, z tym że podczas próby wysłania komendy wymagającej logowania, przeglądarka zapyta o login i hasło.

Kod potrzebny do osiągnięcia tego efektu

Pliki
AppComponent.hpp
 i
SwaggerComponent.hpp
 pozostają takie same jak poprzednio, natomiast występują różnice w klasie
BookController
. Oto pełny kod:
C/C++
#include <map>
#include <atomic>
#include <mutex>
#include <oatpp/network/Server.hpp>
#include <oatpp/core/macro/codegen.hpp>
#include <oatpp/web/server/api/ApiController.hpp>
#include <oatpp/core/macro/component.hpp>
#include <oatpp-swagger/Controller.hpp>
#include "AppComponent.hpp"

constexpr const char USER[ ] = "my_user";
constexpr const char PASSWORD[ ] = "my-secret-password";


/// Begin DTO code-generation
#include OATPP_CODEGEN_BEGIN(DTO)

/// Book Data-Transfer-Object
class BookDto
    : public oatpp::DTO
{
   
DTO_INIT( BookDto, DTO /* Extends */ )
   
   
DTO_FIELD( UInt64, id );
   
DTO_FIELD( String, title, "title" );
   
DTO_FIELD( String, author, "author" );
   
public:
   
BookDto() = default;
   
   
BookDto( v_uint64 pId, const String & pTitle, const String & pAuthor )
        :
id( pId )
       
, title( pTitle )
       
, author( pAuthor )
   
{ }
}
;

/// End DTO code-generation
#include OATPP_CODEGEN_END(DTO)

struct Book
{
   
v_uint64 id;
   
oatpp::String title, author;
   
   
Book() = default;
   
explicit Book( const oatpp::Object < BookDto > & bookDto )
   
{
       
if( bookDto->id )
       
{
           
id = bookDto->id;
       
}
       
title = bookDto->title;
       
author = bookDto->author;
   
}
   
   
oatpp::Object < BookDto > toDto() const
   
{
       
auto dto = BookDto::createShared();
       
dto->id = id;
       
dto->title = title;
       
dto->author = author;
       
return dto;
   
}
}
;

std::map < v_uint64, Book > books;
std::atomic < v_uint64 > bookIdCounter { 0 };
std::mutex booksMutex;

#include OATPP_CODEGEN_BEGIN(ApiController)  ///< Begin Codegen

class BookController
    : public oatpp::web::server::api::ApiController
{
public:
   
static std::shared_ptr < BookController > createShared( OATPP_COMPONENT( std::shared_ptr < ObjectMapper >, objectMapper ) /* Inject objectMapper component here as default parameter */ )
   
{
       
return std::make_shared < BookController >( objectMapper );
   
}
   
   
BookController( OATPP_COMPONENT( std::shared_ptr < ObjectMapper >, objectMapper ) /* Inject object mapper */ )
        :
oatpp::web::server::api::ApiController( objectMapper )
   
{
       
setDefaultAuthorizationHandler( std::make_shared < oatpp::web::server::handler::BasicAuthorizationHandler >( "Book API" ) ); // (1)
   
}
   
   
ENDPOINT_INFO( createBook )
   
{
       
info->summary = "Create new book";
       
info->addConsumes < Object < BookDto >>( "application/json" );
       
info->addResponse < Object < BookDto >>( Status::CODE_200, "application/json" );
       
info->pathParams[ "author" ].required = true;
       
info->pathParams[ "title" ].required = true;
       
info->pathParams[ "title" ].name = "Title of the book";
       
info->pathParams[ "title" ].description = "Official title of book in polish language";
       
info->pathParams[ "title" ].allowEmptyValue = false;
       
// info->pathParams["title"].deprecated = false; // I don't need this
       
       
info->addSecurityRequirement( "basicAuth" ); // (2)
   
}
   
ENDPOINT( "POST", "/books", createBook,
   
BODY_DTO( Object < BookDto >, bookDto ),
   
AUTHORIZATION( std::shared_ptr < oatpp::web::server::handler::DefaultBasicAuthorizationObject >, authObject ) ) // (3)
   
{
       
OATPP_ASSERT_HTTP( userAuthorised( authObject->userId, authObject->password ), Status::CODE_401, "Unauthorized" ); // (4)
       
OATPP_ASSERT_HTTP( bookDto->title, Status::CODE_400, "'title' is required!" );
       
OATPP_ASSERT_HTTP( bookDto->author, Status::CODE_400, "'author' is required!" );
       
const auto bookId = bookIdCounter++;
       
Book book { bookDto };
       
book.id = bookId;
       
       
std::lock_guard < std::mutex > lock( booksMutex );
       
books.insert( std::make_pair( bookId, std::move( book ) ) );
       
bookDto->id = bookId;
       
return createDtoResponse( Status::CODE_200, bookDto );
   
}
   
   
ENDPOINT_INFO( putBook )
   
{
       
info->summary = "Update Book by bookId";
       
info->addConsumes < Object < BookDto >>( "application/json" );
       
info->addResponse < Object < BookDto >>( Status::CODE_200, "application/json" );
       
info->addResponse < String >( Status::CODE_404, "text/plain" );
       
// params specific
       
info->pathParams[ "bookId" ].description = "Book Identifier";
       
       
info->addSecurityRequirement( "basicAuth" );
   
}
   
ENDPOINT( "PUT", "/books/{bookId}", putBook,
   
PATH( UInt64, bookId ),
   
BODY_DTO( Object < BookDto >, bookDto ),
   
AUTHORIZATION( std::shared_ptr < oatpp::web::server::handler::DefaultBasicAuthorizationObject >, authObject ) )
   
{
       
OATPP_ASSERT_HTTP( userAuthorised( authObject->userId, authObject->password ), Status::CODE_401, "Unauthorized" );
       
       
std::lock_guard < std::mutex > lock( booksMutex );
       
try
       
{
           
auto & book = books.at( bookId );
           
if( bookDto->title && bookDto->title->size() )
               
 book.title = bookDto->title;
           
           
if( bookDto->author && bookDto->author->size() )
               
 book.author = bookDto->author;
           
           
return createDtoResponse( Status::CODE_200, book.toDto() );
       
}
       
catch( const std::out_of_range & )
       
{
           
return createResponse( Status::CODE_404, "Book not found" );
       
}
    }
   
   
ENDPOINT_INFO( getBookById )
   
{
       
info->summary = "Get one Book by bookId";
       
info->addResponse < Object < BookDto >>( Status::CODE_200, "application/json" );
       
info->addResponse < String >( Status::CODE_404, "text/plain" );
       
// params specific
       
info->pathParams[ "bookId" ].description = "Book Identifier";
   
}
   
ENDPOINT( "GET", "/books/{bookId}", getBookById,
   
PATH( UInt64, bookId ) )
   
{
       
std::lock_guard < std::mutex > lock( booksMutex );
       
try
       
{
           
auto book = books.at( bookId );
           
Object < BookDto > bookDto = book.toDto();
           
return createDtoResponse( Status::CODE_200, bookDto );
       
}
       
catch( const std::out_of_range & )
       
{
           
return createResponse( Status::CODE_404, "Book not found" );
       
}
    }
   
   
ENDPOINT_INFO( getBooks )
   
{
       
info->summary = "Get all stored books";
       
info->addResponse < List < Object < BookDto >> >( Status::CODE_200, "application/json" );
   
}
   
ENDPOINT( "GET", "/books", getBooks )
   
{
       
std::lock_guard < std::mutex > lock( booksMutex );
       
auto booksDtos = List < Object < BookDto >>::createShared();
       
for( const auto &[ bookId, book ]: books )
       
{
           
const auto bookDto = book.toDto();
           
booksDtos->push_back( std::move( bookDto ) );
       
}
       
return createDtoResponse( Status::CODE_200, booksDtos );
   
}
   
   
ENDPOINT_INFO( deleteBook )
   
{
       
// general
       
info->summary = "Delete Book by bookId";
       
info->addResponse < String >( Status::CODE_200, "text/plain" );
       
info->addResponse < String >( Status::CODE_404, "text/plain" );
       
// params specific
       
info->pathParams[ "bookId" ].description = "Book Identifier";
       
       
info->addSecurityRequirement( "basicAuth" );
   
}
   
ENDPOINT( "DELETE", "/books/{bookId}", deleteBook,
   
PATH( UInt64, bookId ),
   
AUTHORIZATION( std::shared_ptr < oatpp::web::server::handler::DefaultBasicAuthorizationObject >, authObject ) )
   
{
       
OATPP_ASSERT_HTTP( userAuthorised( authObject->userId, authObject->password ), Status::CODE_401, "Unauthorized" );
       
       
std::lock_guard < std::mutex > lock( booksMutex );
       
bool success = books.erase( bookId ) > 0;
       
OATPP_ASSERT_HTTP( success, Status::CODE_404, "Book not deleted. Perhaps no such Book in the Database" );
       
return createResponse( Status::CODE_200, "Book successfully deleted" );
   
}
   
private:
   
bool userAuthorised( const oatpp::String & user, const oatpp::String & password ) const
   
{
       
return user == USER && password == PASSWORD;
   
}
}
;

class StaticController
    : public oatpp::web::server::api::ApiController
{
public:
   
StaticController( const std::shared_ptr < ObjectMapper > & objectMapper )
        :
oatpp::web::server::api::ApiController( objectMapper )
   
{ }
   
   
static std::shared_ptr < StaticController > createShared(
   
OATPP_COMPONENT( std::shared_ptr < ObjectMapper >, objectMapper ) /* Inject objectMapper component here as default parameter */ )
   
{
       
return std::make_shared < StaticController >( objectMapper );
   
}
   
   
ENDPOINT( "GET", "/", root ) {
       
const char * html =
       
R"(<html lang='pl'> <head> <meta charset=utf-8/> </head> <body> <p>Witaj w bazie ksiazek!</p> <a href='swagger/ui'>Zobacz dokumentacje wygenerowana przez Swagger-UI</a> </body> </html>)";
       
auto response = createResponse( Status::CODE_200, html );
       
response->putHeader( Header::CONTENT_TYPE, "text/html" );
       
return response;
   
}
}
;


#include OATPP_CODEGEN_END(ApiController)  ///< End Codegen

void runApp( const char * programName );

int main( int, char * * argv )
{
   
oatpp::base::Environment::init();
   
   
runApp( argv[ 0 ] );
   
   
oatpp::base::Environment::destroy();
}

void runApp( const char * programName )
{
   
/// Register Components in scope of run() method
   
ApplicationComponents components;
   
   
/// Get router component
   
OATPP_COMPONENT( std::shared_ptr < oatpp::web::server::HttpRouter >, router4HttpRequestsRouting );
   
   
auto bookController = router4HttpRequestsRouting->addController( BookController::createShared() );
   
auto bookEndpoints = bookController->getEndpoints();
   
   
oatpp::web::server::api::Endpoints docEndpoints;
   
docEndpoints.append( bookEndpoints );
   
   
router4HttpRequestsRouting->addController( oatpp::swagger::Controller::createShared( docEndpoints ) );
   
router4HttpRequestsRouting->addController( StaticController::createShared() );
   
   
/// Create MyApiController and add all of its endpoints to router
   
auto myApiController = std::make_shared < BookController >();
   
router4HttpRequestsRouting->addController( myApiController );
   
   
/// Get connection handler component
   
OATPP_COMPONENT( std::shared_ptr < oatpp::network::ConnectionHandler >, connectionHandler );
   
   
/// Get connection provider component
   
OATPP_COMPONENT( std::shared_ptr < oatpp::network::ServerConnectionProvider >, connectionProvider );
   
   
/// Create server which takes provided TCP connections and passes them to HTTP connection handler
   
oatpp::network::Server server( connectionProvider, connectionHandler );
   
   
/// Print info about server port
   
OATPP_LOGI( programName, "Server running on port %s", connectionProvider->getProperty( "port" ).getData() );
   
   
server.run();
}
Komentarz do pewnych fragmentów programu:
1. Ustawienie domyślnego handlera autoryzacji dla kontrolera
BookController
.
BasicAuthorizationHandler
 jest używany do implementacji podstawowej autoryzacji HTTP (Basic Auth). Handler ten sprawdza nagłówek
Authorization
 w żądaniach HTTP. Argument:
"Book API"
 to identyfikator realm, który pojawia się w oknie dialogowym przeglądarki przy żądaniu danych logowania.
2. Dodanie wymogu autoryzacji do endpointa: ta linia kodu informuje, że endpoint wymaga autoryzacji typu
basicAuth
.
3. Deklaracja endpointa z autoryzacją.
4. Sprawdzenie autoryzacji użytkownika w endpoincie: funkcja
userAuthorised
 sprawdza, czy podane dane logowania są poprawne. Jeśli nie, zwracany jest status HTTP 401 ("Unauthorized").
Istnieje możliwość utworzenia handlera do autoryzacji, co zwiększa modularność i ułatwia utrzymanie kodu. Szczegóły można znaleźć w dokumentacji.

Autoryzacja przy użyciu tokena: authorization - Bearer

Oat++ wspiera również możliwość autoryzacji przy pomocy tokenu (ang. "bearer authorization"). Możemy się zalogować, korzystając z dodatkowego endpointa:
curl -X POST "http://localhost:8000/login" -H "Content-Type: application/json" -d '{"username": "my_user", "password": "my-secret-password"}'

W odpowiedzi otrzymamy token, do którego wygenerowania i zweryfikowania możemy użyć biblioteki jwt-cpp.
Następnie używamy tokenu w następujący sposób:
curl -X POST "http://localhost:8000/books" -H "Content-Type: application/json" \
 -H "Authorization: Bearer YOUR_JWT_TOKEN" \
 -d '{"title": "Co widać i czego nie widać", "author": "Frédéric Bastiat"}'
Więcej informacji na temat autoryzacji można znaleźć na oficjalnej stronie.[/div]

Klient do naszej aplikacji

Napisaliśmy aplikację, którą mogą używać użytkownicy, ale warto również umożliwić komunikację z naszym serwerem przez klienta napisanego w C++. Oat++ umożliwia takie rozwiązania. Dużym udogodnieniem jest nie tylko możliwość wysyłania i odbierania danych, ale także możliwość użycia obiektów transferu danych (DTO). Poniższy przykład bazuje na oficjalnym przykładzie API Client.

Oczekiwany efekt: Program kliencki w C++, który dodaje kilka książek, pobiera książki, edytuje i usuwa, stosując mechanizm autoryzacji.

Pełny kod klienta korzystającego z autoryzacji

C/C++
////////// file: BooksDto.hpp:
#include <oatpp/core/macro/codegen.hpp>
#include <oatpp-swagger/Controller.hpp>


/// Begin DTO code-generation
#include OATPP_CODEGEN_BEGIN(DTO)

/// Book Data-Transfer-Object
class BookDto
    : public oatpp::DTO
{
   
DTO_INIT( BookDto, DTO /* Extends */ )
   
   
DTO_FIELD( UInt64, id );
   
DTO_FIELD( String, title, "title" );
   
DTO_FIELD( String, author, "author" );
   
public:
   
BookDto() = default;
   
   
BookDto( v_uint64 pId, const String & pTitle, const String & pAuthor )
        :
id( pId )
       
, title( pTitle )
       
, author( pAuthor )
   
{ }
}
;

/// End DTO code-generation
#include OATPP_CODEGEN_END(DTO)


////////// file: Client.hpp
// #pragma once

#include <oatpp/web/client/ApiClient.hpp>

#include OATPP_CODEGEN_BEGIN(ApiClient)  
//<- Begin Codegen

class BookClient
    : public oatpp::web::client::ApiClient
{
public:
   
API_CLIENT_INIT( BookClient )
   
   
API_CALL( "POST", "/books", createBook, BODY_DTO( Object < BookDto >, bookDto ), HEADER( String, authorization, "Authorization" ) )
   
API_CALL( "GET", "/books", getBooks )
   
API_CALL( "GET", "/books/{id}", getBookById, PATH( UInt64, id ) )
   
API_CALL( "PUT", "/books/{id}", updateBook, PATH( UInt64, id ), BODY_DTO( Object < BookDto >, bookDto ), HEADER( String, authorization, "Authorization" ) )
   
API_CALL( "DELETE", "/books/{id}", deleteBook, PATH( UInt64, id ), HEADER( String, authorization, "Authorization" ) )
};

#include OATPP_CODEGEN_END(ApiClient)  //<- End Codegen


////////// file: main.cpp
#include <iostream>
#include <oatpp/network/tcp/client/ConnectionProvider.hpp>
#include <oatpp/web/client/HttpRequestExecutor.hpp>

constexpr const char * USER = "my_user";
constexpr const char * PASSWORD = "my-secret-password";


void addBook( std::string_view basicAuthHeader, const char * title, const char * author, auto & bookApiClient );

void printBooks( auto & bookApiClient, auto & objectMapper );

void updateBook( std::string_view basicAuthHeader, v_uint64 id, const char * title, const char * author, auto & bookApiClient );

void deleteBook( std::string_view basicAuthHeader, v_uint64 id, auto & bookApiClient );

void printBookById( v_uint64 id, auto & bookApiClient, auto & objectMapper );

void runClient();

std::string base64_encode( const std::string & in ); // ignore this function

std::string createBasicAuthHeader( const std::string & user, const std::string & password )
{
   
return "Basic " + base64_encode( user + ":" + password );
}

///////////////// main()
int main()
{
   
oatpp::base::Environment::init();
   
runClient();
   
   
std::cout << "\nEnvironment:\n";
   
std::cout << "objectsCount = " << oatpp::base::Environment::getObjectsCount() << "\n";
   
std::cout << "objectsCreated = " << oatpp::base::Environment::getObjectsCreated() << "\n\n";
   
   
oatpp::base::Environment::destroy();
}


void runClient()
{
   
auto connectionProvider = oatpp::network::tcp::client::ConnectionProvider::createShared( { "localhost", 8000, oatpp::network::Address::IP_4 } );
   
   
auto httpRequestExecutor = oatpp::web::client::HttpRequestExecutor::createShared( connectionProvider );
   
   
auto jsonObjectMapper = oatpp::parser::json::mapping::ObjectMapper::createShared();
   
   
auto bookApiClient = BookClient::createShared( httpRequestExecutor, jsonObjectMapper );
   
   
std::string basicAuthHeader = createBasicAuthHeader( USER, PASSWORD );
   
   
addBook( basicAuthHeader, "Opus magnum C++11", "Jerzy Grebosz", bookApiClient );
   
addBook( basicAuthHeader, "Skuteczny nowoczesny C++. 42 sposoby lepszego poslugiwania się jezykami C++11 i C++14", "Scott Meyers", bookApiClient );
   
addBook( basicAuthHeader, "C++17 STL. Receptury", "Jacek Galowicz", bookApiClient );
   
   
printBooks( bookApiClient, jsonObjectMapper );
   
   
std::cout << std::endl;
   
   
updateBook( basicAuthHeader, 0, "Misja w nadprzestrzen C++14/17", "Jerzy Grebosz", bookApiClient );
   
   
printBookById( 0, bookApiClient, jsonObjectMapper );
   
   
deleteBook( basicAuthHeader, 1, bookApiClient );
   
   
printBooks( bookApiClient, jsonObjectMapper );
   
   
   
auto rawData = bookApiClient->getBooks()->readBodyToString();
   
OATPP_LOGD( BookClient::TAG, "[doGet] data='%s'", rawData->c_str() );
}

void addBook( std::string_view basicAuthHeader, const char * title, const char * author, auto & bookApiClient )
{
   
auto book = BookDto::createShared();
   
book->title = title;
   
book->author = author;
   
   
auto response = bookApiClient->createBook( book, basicAuthHeader.data() );
   
   
const BookClient::Status status { response->getStatusCode(), "creation book error" };
   
OATPP_ASSERT_HTTP( response->getStatusCode() == 200, status, std::string( "Failed to create book: " ) + title );
}

void printBooks( auto & bookApiClient, auto & objectMapper )
{
   
auto response = bookApiClient->getBooks();
   
   
const BookClient::Status status { response->getStatusCode(), "getting books error" };
   
OATPP_ASSERT_HTTP( response->getStatusCode() == 200, status, "Failed to get books" );
   
   
auto books = response->template readBodyToDto < oatpp::List < oatpp::Object < BookDto >> >( objectMapper );
   
for( const auto & book: * books )
   
{
       
std::cout << "Book ID: " << book->id << std::endl;
       
std::cout << "Title: " << book->title->c_str() << std::endl;
       
std::cout << "Author: " << book->author->c_str() << std::endl;
       
std::cout << "--------" << std::endl;
   
}
}

void updateBook( std::string_view basicAuthHeader, v_uint64 id, const char * title, const char * author, auto & bookApiClient )
{
   
auto book = BookDto::createShared();
   
book->title = title;
   
book->author = author;
   
   
auto response = bookApiClient->updateBook( id, book, basicAuthHeader.data() );
   
const BookClient::Status status { response->getStatusCode(), "updating book error" };
   
OATPP_ASSERT_HTTP( response->getStatusCode() == 200, status, std::string( "Failed to update book: " ) + title );
}

void deleteBook( std::string_view basicAuthHeader, v_uint64 id, auto & bookApiClient )
{
   
auto response = bookApiClient->deleteBook( id, basicAuthHeader.data() );
   
const BookClient::Status status { response->getStatusCode(), "deleting book error" };
   
OATPP_ASSERT_HTTP( response->getStatusCode() == 200, status, std::string( "Failed to delete book with ID: " ) + std::to_string( id ) );
}

void printBookById( v_uint64 id, auto & bookApiClient, auto & objectMapper )
{
   
auto response = bookApiClient->getBookById( id );
   
const BookClient::Status status { response->getStatusCode(), "getting book by id error" };
   
OATPP_ASSERT_HTTP( response->getStatusCode() == 200, status, "Failed to get book by ID" );
   
   
auto book = response->template readBodyToDto < oatpp::Object < BookDto >>( objectMapper );
   
std::cout << "Book ID: " << book->id << std::endl;
   
std::cout << "Title: " << book->title->c_str() << std::endl;
   
std::cout << "Author: " << book->author->c_str() << std::endl;
   
std::cout << "--------" << std::endl;
}


std::string base64_encode( const std::string & in ) // ignore this function
{
   
std::string out;
   
   
int val = 0, valb = - 6;
   
for( unsigned char c: in )
   
{
       
val =( val << 8 ) + c;
       
valb += 8;
       
while( valb >= 0 )
       
{
           
out.push_back( "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"[( val >> valb ) & 0x3F ] );
           
valb -= 6;
       
}
    }
   
if( valb > - 6 )
       
 out.push_back( "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"[(( val << 8 ) >>( valb + 8 ) ) & 0x3F ] );
   
   
while( out.size() % 4 )
       
 out.push_back( '=' );
   
   
return out;
}
W ramach wyjaśnienia trudniejszych miejsc w kodzie:
Na uwagę zasługuje klasa
BookClient
, która ma zdefiniowane dokładnie te same metody, co nasze API. Istotne jest ustawienie autoryzacji - przez dodatkowy nagłówek. Dzięki temu możemy wywoływać metody API jak zwykłe metody, podając odpowiednie argumenty (w tym nagłówek odpowiedzialny za autoryzację). Oprócz możliwości parsowania odpowiedzi do obiektu DTO, można również pobrać odpowiedź w formie tekstowej.

Więcej informacji na temat modułu API Client w dokumentacji. Znajdziesz tam informacje o tym, jak ustawić automatyczne wznawianie połączeń.

Szyfrowane połączenie

W dzisiejszych czasach szyfrowanie połączeń HTTP jest powszechne. Bez obsługi szyfrowania framework byłby mniej przydatny. Co ciekawe, Oat++ zawiera dwa moduły do szyfrowania: oatpp-libressl i oatpp-mbedtls. Ze względu na popularność i szerokie zastosowanie skupimy się na module korzystającym z libressl. Użyty w tym kroku kod bazuje na kodzie serwera z poprzedniego przykładu, ale został wzbogacony o konstrukcje z oficjalnego przykładu używającego libressl.

Oczekiwany efekt:
Zamiast komunikować się z serwerem jak dotychczas (port 8443 zamiast 8000, zwróć uwagę gdzie jest http a gdzie https):
curl -X POST "http://localhost:8443/books" -H "Content-Type: application/json" -H "Authorization: Basic $(echo -n 'my_user:my-secret-password' | base64)" -d '{ "title": "Krotki przewodnik po rodzinie", "author": "Piotr Pawlukiewicz" }'
curl: (1) Received HTTP/0.9 when not allowed

curl -X POST "https://localhost:8443/books" -H "Content-Type: application/json" -H "Authorization: Basic $(echo -n 'my_user:my-secret-password' | base64)" -d '{ "title": "Krotki przewodnik po rodzinie", "author": "Piotr Pawlukiewicz" }'
curl: (60) SSL certificate problem: self-signed certificate
More details here: https://curl.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.
Musimy używać połączenia HTTPS. Możemy to zrobić, pomijając weryfikację certyfikatu (stosując flagę
-k
 lub
--insecure
):
curl -X POST "https://localhost:8443/books" \
  --insecure \
  -H "Content-Type: application/json" \
  -H "Authorization: Basic $(echo -n 'my_user:my-secret-password' | base64)" \
  -d '{ "title": "Krotki przewodnik po rodzinie", "author": "Piotr Pawlukiewicz" }'
{"id":0,"title":"Krotki przewodnik po rodzinie","author":"Piotr Pawlukiewicz"}%
Lepszą wersją jest wskazanie certyfikatu:
curl -X GET "https://localhost:8443/books" \
  --cacert cert.pem -H "Content-Type: application/json"

Wygenerowana dokumentacja (Swagger UI) będzie dostępna przez przeglądarkę jak dotychczas, z tym że będzie to połączenie szyfrowane (zależnie od sposobu pozyskania certyfikatu, jeśli wygenerujemy go sami, pojawi się ostrzeżenie przeglądarki o połączeniu szyfrowanym z niezweryfikowanym certyfikatem).

Kod potrzebny do osiągnięcia tego efektu

Pierwszą rzeczą jest zainstalowanie odpowiedniego modułu. Można to zrobić za pomocą menedżera pakietów VCPKG w następujący sposób:
./vcpkg/vcpkg install oatpp-libressl

Może się okazać, że nie uda się zainstalować modułu z powodu konfliktów między libressl a openssl. W takim przypadku należy odinstalować pakiety zawierające openssl przy użyciu VCPKG. Przykładem może być moduł oatpp-curl, który w zależnościach zawiera openssl. Problem działa też w drugą stronę - jak mamy libressl to nie możemy używać czegoś co go używa, a jest to np. oatpp-postgresql.
Po zainstalowaniu zależności należy zaktualizować plik
CMakeLists.txt
 o moduł
oatpp-libressl
 oraz jego zależności:
find_package(oatpp-libressl 1.3.0 REQUIRED)
find_package(LibreSSL 3.0.0 REQUIRED)

target_link_libraries(${PROJECT_NAME}
        PUBLIC oatpp::oatpp-libressl # <-- oatpp-libressl adapter

        # LibreSSL libraries
        PUBLIC LibreSSL::TLS
        PUBLIC LibreSSL::SSL
        PUBLIC LibreSSL::Crypto
)
Pełna wersja pliku
CMakeLists.txt
:
cmake_minimum_required(VERSION 3.22)

project(OatText LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

add_executable(${PROJECT_NAME} main.cpp
    AppComponent.hpp
    SwaggerComponent.hpp)

find_package(oatpp CONFIG REQUIRED)
find_package(oatpp-swagger CONFIG REQUIRED)
find_package(oatpp-libressl 1.3.0 REQUIRED)
find_package(LibreSSL 3.0.0 REQUIRED)

target_link_libraries(${PROJECT_NAME} PRIVATE oatpp::oatpp oatpp::oatpp-swagger oatpp::oatpp-test)

target_link_libraries(${PROJECT_NAME}
        PUBLIC oatpp::oatpp-libressl # <-- oatpp-libressl adapter

        # LibreSSL libraries
        PUBLIC LibreSSL::TLS
        PUBLIC LibreSSL::SSL
        PUBLIC LibreSSL::Crypto
)

Teraz możemy zająć się zmianami w kodzie, który, mimo pozorów, się znacząco nie zmienia. Należy jedynie zaktualizować pliki
AppComponent.hpp
 w przykładowy sposób:
C/C++
#pragma once

#include <oatpp-libressl/Config.hpp>
#include <oatpp-libressl/server/ConnectionProvider.hpp>
#include <oatpp/core/macro/component.hpp>
#include <oatpp/network/tcp/server/ConnectionProvider.hpp>
#include <oatpp/parser/json/mapping/ObjectMapper.hpp>
#include <oatpp/web/client/HttpRequestExecutor.hpp>
#include <oatpp/web/server/HttpConnectionHandler.hpp>
#include <oatpp/web/server/HttpRouter.hpp>

#include "SwaggerComponent.hpp"


constexpr const int listeningPort = 8000;
constexpr const int listeningPortHttps = 8443;
constexpr const char listeningAddress[ ] = "localhost";

/// Class which creates and holds Application components and registers components in oatpp::base::Environment
class ApplicationComponents
{
public:
   
SwaggerComponent swaggerComponent;
   
   
/// Create ConnectionProvider component which listens on the port
   
OATPP_CREATE_COMPONENT( std::shared_ptr < oatpp::network::ServerConnectionProvider >, serverConnectionProvider )([ ] {
       
constexpr const char CERT_PEM_PATH[ ] = "key.pem"; /// jeśli pliku nie znajduje to ustaw odpowiednią ścieżkę
       
constexpr const char CERT_CRT_PATH[ ] = "cert.pem"; /// jeśli pliku nie znajduje to ustaw odpowiednią ścieżkę
       
OATPP_LOGD( "oatpp::libressl::Config", "pem='%s'", CERT_PEM_PATH );
       
OATPP_LOGD( "oatpp::libressl::Config", "crt='%s'", CERT_CRT_PATH );
       
auto config = oatpp::libressl::Config::createDefaultServerConfigShared( CERT_CRT_PATH, CERT_PEM_PATH /* private key */ );
       
       
/** if you see such error:
     * oatpp::libressl::server::ConnectionProvider:Error on call to 'tls_configure'. ssl context failure
     * It might be because you have several ssl libraries installed on your machine.
     * Try to make sure you are using libtls, libssl, and libcrypto from the same package
     */
       
       
return oatpp::libressl::server::ConnectionProvider::createShared( config, { listeningAddress, listeningPortHttps, oatpp::network::Address::IP_4 } );
   
}() );
   
   
/// Create Router component
   
OATPP_CREATE_COMPONENT( std::shared_ptr < oatpp::web::server::HttpRouter >, httpRouter )([ ] {
       
return oatpp::web::server::HttpRouter::createShared();
   
}() );
   
   
/// Create ConnectionHandler component which uses Router component to route requests
   
OATPP_CREATE_COMPONENT( std::shared_ptr < oatpp::network::ConnectionHandler >, serverConnectionHandler )([ ] {
       
OATPP_COMPONENT( std::shared_ptr < oatpp::web::server::HttpRouter >, router ); // get Router component
       
return oatpp::web::server::HttpConnectionHandler::createShared( router );
   
}() );
   
   
/// Create ObjectMapper component to serialize/deserialize DTOs in Contoller's API
   
OATPP_CREATE_COMPONENT( std::shared_ptr < oatpp::data::mapping::ObjectMapper >, apiObjectMapper )([ ] {
       
auto serializerConfig = oatpp::parser::json::mapping::Serializer::Config::createShared();
       
auto deserializerConfig = oatpp::parser::json::mapping::Deserializer::Config::createShared();
       
deserializerConfig->allowUnknownFields = false;
       
auto objectMapper = oatpp::parser::json::mapping::ObjectMapper::createShared( serializerConfig, deserializerConfig );
       
return objectMapper;
   
}() );
};

Oraz należy dokonać drobnych zmian w pliku
SwaggerComponent.hpp
, zasadniczo sprowadzających się do zmiany jednej linijki (http na https oraz inny port):
addServer( "https://localhost:8443", "server on localhost" );

Oto pełny kod tego pliku:
C/C++
#pragma once

#include <oatpp-swagger/Model.hpp>
#include <oatpp-swagger/Resources.hpp>
#include <oatpp/core/macro/component.hpp>

/** Swagger ui is served at
 *  http://host:port/swagger/ui */
class SwaggerComponent
{
public:
   
/// General API docs info
   
OATPP_CREATE_COMPONENT( std::shared_ptr < oatpp::swagger::DocumentInfo >, swaggerDocumentInfo )([ ]
   
{
       
oatpp::swagger::DocumentInfo::Builder builder;
       
       
builder
        .setTitle( "Book API" )
        .
setDescription( "Example Book API with Swagger Documentation" )
        .
setVersion( "1.0" )
        .
setContactName( "Example Contact" )
        .
setContactUrl( "https://example.com" )
        .
setLicenseName( "MIT" )
        .
setLicenseUrl( "https://opensource.org/licenses/MIT" )
        .
addServer( "https://localhost:8443", "server on localhost" );
       
       
return builder.build();
   
}() );
   
   
/// Swagger-UI Resources (<oatpp-examples>/lib/oatpp-swagger/res)
   
OATPP_CREATE_COMPONENT( std::shared_ptr < oatpp::swagger::Resources >, swaggerResources )([ ]
   
{
       
// Make sure to specify correct full path to oatpp-swagger/res folder !!!
       
constexpr const char OATPP_SWAGGER_RES_PATH[ ] = "/home/agh/Desktop/tmp/vcpkg/installed/x64-linux/include/oatpp-1.3.0/bin/oatpp-swagger/res";
       
return oatpp::swagger::Resources::loadResources( OATPP_SWAGGER_RES_PATH ); // (0)
   
}() );
};

Na potrzeby "zabawy" można wygenerować certyfikat samodzielnie (komenda oparta na StackOverflow):
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -sha256 -days 3650 -nodes -subj "/C=PL/ST=malopolska/L=Krakow/O=companyName/OU=companySectionName/CN=localhost

Jednakże w profesjonalnych aplikacjach sposób pozyskania certyfikatu jest bardziej wymagający.

Klient do komunikacji z aplikacją przy użyciu szyfrowania

Nasz dotychczasowy klient nie będzie już w stanie połączyć się z szyfrowanym serwerem, dlatego musimy wprowadzić pewne zmiany. Według opisu dotyczącego przykładu z szyfrowaniem, konieczne jest dodanie kilku linijek kodu, jednak wymaga to również innych zmian. Poniżej znajduje się pełny kod klienta:
C/C++
////////// file: ClientComponent.hpp
#include <oatpp/parser/json/mapping/ObjectMapper.hpp>
#include <oatpp-libressl/Config.hpp>
#include <oatpp-libressl/client/ConnectionProvider.hpp>
#include <oatpp/core/macro/component.hpp>

constexpr const int listeningPortHttps = 8443;
constexpr const char listeningAddress[ ] = "localhost";


class ClientComponent // (1)
{
public:
   
OATPP_CREATE_COMPONENT( std::shared_ptr < oatpp::network::ClientConnectionProvider >, sslClientConnectionProvider )([ ] {
       
auto config = oatpp::libressl::Config::createShared();
       
tls_config_insecure_noverifycert( config->getTLSConfig() );
       
tls_config_insecure_noverifyname( config->getTLSConfig() );
       
return oatpp::libressl::client::ConnectionProvider::createShared( config, { listeningAddress, listeningPortHttps } );
   
}() );
};


////////// file: BooksDto.hpp:
#include <oatpp/core/macro/codegen.hpp>
#include <oatpp-swagger/Controller.hpp>


/// Begin DTO code-generation
#include OATPP_CODEGEN_BEGIN(DTO)

/// Book Data-Transfer-Object
class BookDto
    : public oatpp::DTO
{
   
DTO_INIT( BookDto, DTO /* Extends */ )
   
   
DTO_FIELD( UInt64, id );
   
DTO_FIELD( String, title, "title" );
   
DTO_FIELD( String, author, "author" );
   
public:
   
BookDto() = default;
   
   
BookDto( v_uint64 pId, const String & pTitle, const String & pAuthor )
        :
id( pId )
       
, title( pTitle )
       
, author( pAuthor )
   
{ }
}
;

/// End DTO code-generation
#include OATPP_CODEGEN_END(DTO)


////////// file: Client.hpp
// #pragma once

#include <oatpp/web/client/ApiClient.hpp>

#include OATPP_CODEGEN_BEGIN(ApiClient)  
//<- Begin Codegen

class BookClient
    : public oatpp::web::client::ApiClient
{
public:
   
API_CLIENT_INIT( BookClient )
   
   
API_CALL( "POST", "/books", createBook, BODY_DTO( Object < BookDto >, bookDto ), HEADER( String, authorization, "Authorization" ) )
   
API_CALL( "GET", "/books", getBooks )
   
API_CALL( "GET", "/books/{id}", getBookById, PATH( UInt64, id ) )
   
API_CALL( "PUT", "/books/{id}", updateBook, PATH( UInt64, id ), BODY_DTO( Object < BookDto >, bookDto ), HEADER( String, authorization, "Authorization" ) )
   
API_CALL( "DELETE", "/books/{id}", deleteBook, PATH( UInt64, id ), HEADER( String, authorization, "Authorization" ) )
};

#include OATPP_CODEGEN_END(ApiClient)  //<- End Codegen


////////// file: main.cpp
#include <iostream>
#include <oatpp/network/tcp/client/ConnectionProvider.hpp>
#include <oatpp/web/client/HttpRequestExecutor.hpp>

constexpr const char * USER = "my_user";
constexpr const char * PASSWORD = "my-secret-password";


void addBook( std::string_view basicAuthHeader, const char * title, const char * author, auto & bookApiClient );

void printBooks( auto & bookApiClient, auto & objectMapper );

void updateBook( std::string_view basicAuthHeader, v_uint64 id, const char * title, const char * author, auto & bookApiClient );

void deleteBook( std::string_view basicAuthHeader, v_uint64 id, auto & bookApiClient );

void printBookById( v_uint64 id, auto & bookApiClient, auto & objectMapper );

void runClient();

std::string base64_encode( const std::string & in ); // ignore this function

std::string createBasicAuthHeader( const std::string & user, const std::string & password )
{
   
return "Basic " + base64_encode( user + ":" + password );
}

///////////////// main()
int main()
{
   
oatpp::base::Environment::init();
   
runClient();
   
   
std::cout << "\nEnvironment:\n";
   
std::cout << "objectsCount = " << oatpp::base::Environment::getObjectsCount() << "\n";
   
std::cout << "objectsCreated = " << oatpp::base::Environment::getObjectsCreated() << "\n\n";
   
   
oatpp::base::Environment::destroy();
}


void runClient()
{
   
std::shared_ptr < oatpp::web::client::HttpRequestExecutor > httpRequestExecutor;
   
   
constexpr bool httpsConnection = true;
   
if constexpr( httpsConnection ) // (2)
   
{
       
ClientComponent clientComponent;
       
OATPP_COMPONENT( std::shared_ptr < oatpp::network::ClientConnectionProvider >, sslClientConnectionProvider );
       
httpRequestExecutor = oatpp::web::client::HttpRequestExecutor::createShared( sslClientConnectionProvider );
   
}
   
else
   
{
       
/// for HTTP:
       
auto connectionProvider = oatpp::network::tcp::client::ConnectionProvider::createShared( { "localhost", 8000, oatpp::network::Address::IP_4 } );
       
httpRequestExecutor = oatpp::web::client::HttpRequestExecutor::createShared( connectionProvider );
   
}
   
   
auto jsonObjectMapper = oatpp::parser::json::mapping::ObjectMapper::createShared();
   
   
auto bookApiClient = BookClient::createShared( httpRequestExecutor, jsonObjectMapper );
   
   
std::string basicAuthHeader = createBasicAuthHeader( USER, PASSWORD );
   
   
addBook( basicAuthHeader, "Opus magnum C++11", "Jerzy Grebosz", bookApiClient );
   
addBook( basicAuthHeader, "Skuteczny nowoczesny C++. 42 sposoby lepszego poslugiwania się jezykami C++11 i C++14", "Scott Meyers", bookApiClient );
   
addBook( basicAuthHeader, "C++17 STL. Receptury", "Jacek Galowicz", bookApiClient );
   
   
printBooks( bookApiClient, jsonObjectMapper );
   
   
std::cout << std::endl;
   
   
updateBook( basicAuthHeader, 0, "Misja w nadprzestrzen C++14/17", "Jerzy Grebosz", bookApiClient );
   
   
printBookById( 0, bookApiClient, jsonObjectMapper );
   
   
deleteBook( basicAuthHeader, 1, bookApiClient );
   
   
printBooks( bookApiClient, jsonObjectMapper );
   
   
   
auto rawData = bookApiClient->getBooks()->readBodyToString();
   
OATPP_LOGD( BookClient::TAG, "[doGet] data='%s'", rawData->c_str() );
}

void addBook( std::string_view basicAuthHeader, const char * title, const char * author, auto & bookApiClient )
{
   
auto book = BookDto::createShared();
   
book->title = title;
   
book->author = author;
   
   
auto response = bookApiClient->createBook( book, basicAuthHeader.data() );
   
   
const BookClient::Status status { response->getStatusCode(), "creation book error" };
   
OATPP_ASSERT_HTTP( response->getStatusCode() == 200, status, std::string( "Failed to create book: " ) + title );
}

void printBooks( auto & bookApiClient, auto & objectMapper )
{
   
auto response = bookApiClient->getBooks();
   
   
const BookClient::Status status { response->getStatusCode(), "getting books error" };
   
OATPP_ASSERT_HTTP( response->getStatusCode() == 200, status, "Failed to get books" );
   
   
auto books = response->template readBodyToDto < oatpp::List < oatpp::Object < BookDto >> >( objectMapper );
   
for( const auto & book: * books )
   
{
       
std::cout << "Book ID: " << book->id << std::endl;
       
std::cout << "Title: " << book->title->c_str() << std::endl;
       
std::cout << "Author: " << book->author->c_str() << std::endl;
       
std::cout << "--------" << std::endl;
   
}
}

void updateBook( std::string_view basicAuthHeader, v_uint64 id, const char * title, const char * author, auto & bookApiClient )
{
   
auto book = BookDto::createShared();
   
book->title = title;
   
book->author = author;
   
   
auto response = bookApiClient->updateBook( id, book, basicAuthHeader.data() );
   
const BookClient::Status status { response->getStatusCode(), "updating book error" };
   
OATPP_ASSERT_HTTP( response->getStatusCode() == 200, status, std::string( "Failed to update book: " ) + title );
}

void deleteBook( std::string_view basicAuthHeader, v_uint64 id, auto & bookApiClient )
{
   
auto response = bookApiClient->deleteBook( id, basicAuthHeader.data() );
   
const BookClient::Status status { response->getStatusCode(), "deleting book error" };
   
OATPP_ASSERT_HTTP( response->getStatusCode() == 200, status, std::string( "Failed to delete book with ID: " ) + std::to_string( id ) );
}

void printBookById( v_uint64 id, auto & bookApiClient, auto & objectMapper )
{
   
auto response = bookApiClient->getBookById( id );
   
const BookClient::Status status { response->getStatusCode(), "getting book by id error" };
   
OATPP_ASSERT_HTTP( response->getStatusCode() == 200, status, "Failed to get book by ID" );
   
   
auto book = response->template readBodyToDto < oatpp::Object < BookDto >>( objectMapper );
   
std::cout << "Book ID: " << book->id << std::endl;
   
std::cout << "Title: " << book->title->c_str() << std::endl;
   
std::cout << "Author: " << book->author->c_str() << std::endl;
   
std::cout << "--------" << std::endl;
}


std::string base64_encode( const std::string & in ) // ignore this function
{
   
std::string out;
   
   
int val = 0, valb = - 6;
   
for( unsigned char c: in )
   
{
       
val =( val << 8 ) + c;
       
valb += 8;
       
while( valb >= 0 )
       
{
           
out.push_back( "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"[( val >> valb ) & 0x3F ] );
           
valb -= 6;
       
}
    }
   
if( valb > - 6 )
       
 out.push_back( "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"[(( val << 8 ) >>( valb + 8 ) ) & 0x3F ] );
   
   
while( out.size() % 4 )
       
 out.push_back( '=' );
   
   
return out;
}
W powyższym kodzie znajdują się dwa fragmenty specyficzne dla połączenia HTTPS:
1. Utworzenie klasy ClientComponent, która zarządza połączeniem szyfrowanym.
2. Inny sposób połączenia, z użyciem
if constexpr( httpsConnection )
.

Obsługa baz danych przez mechanizm ORM biblioteki Oat++ oraz użycie typu wyliczeniowego

Ciekawą funkcjonalnością tej biblioteki jest moduł do mapowania obiektowo-relacyjnego (ORM) między bazą danych a obiektami C++. Na ten moment wspierane są następujące bazy danych: MongoDB, PostgreSQL oraz SQLite. Wsparcie to polega na możliwości łatwego przenoszenia obiektów transferu danych (DTO) do i z bazy danych, bez konieczności tworzenia dodatkowych obiektów pośrednich.

W niniejszej sekcji zademonstruję przykład bardzo podobny do poprzedniego, jednak zamiast używania
std::map
 wykorzystam bazę danych SQLite, obudowaną przez moduł oatpp-sqlite. Różnica z perspektywy klienta polega na dodaniu, aczkolwiek opcjonalnego, pola wyliczeniowego zawierającego informacje o typie książki. Przykład ten jest inspirowany oficjalnym przykładem CRUD, jednak jest od niego prostszy: nie zawiera testów ani mechanizmu paginacji odpowiedzi.

Oczekiwany efekt (z perspektywy klienta) - dodatkowe pole
genre
:
curl -X POST "https://localhost:8443/books" \
  --insecure \
  -H "Content-Type: application/json" \
  -H "Authorization: Basic $(echo -n 'my_user:my-secret-password' | base64)" \
  -d '{ "title": "Paradyzja", "author": "Janusz Zajdel", "genre":"fiction" }'
{"id":2,"title":"Paradyzja","author":"Janusz Zajdel","genre":"fiction"}
# Można pominąć to pole:
curl -X POST "https://localhost:8443/books" \
  --insecure \
  -H "Content-Type: application/json" \
  -H "Authorization: Basic $(echo -n 'my_user:my-secret-password' | base64)" \
  -d '{ "title": "Iskry Boże", "author": "Grzegorz Braun" }'
{"id":3,"title":"Iskry Bo\u017Ce","author":"Grzegorz Braun","genre":"unknown book genre"}
Natomiast użycie niepoprawnego wyliczenia spowoduje błąd:
curl -X POST "https://localhost:8443/books" \
  --insecure \
  -H "Content-Type: application/json" \
  -H "Authorization: Basic $(echo -n 'my_user:my-secret-password' | base64)" \
  -d '{ "title": "18 brumairea Ludwika Bonaparte", "author": "Karol Marks", "genre":"dziadostwo" }'
server=oatpp/1.3.0
code=500
description=Internal Server Error
message=[oatpp::parser::json::mapping::Deserializer::deserializeEnum()]: Error. Can't deserialize Enum.
Pozostałe komendy działają analogicznie.

Pełny kod korzystający z bazy danych SQLite oraz używający typu wyliczeniowego

Jest to ostatni przykład, dlatego został on rozbity na pliki w sposób sugerowany przez oficjalne poradniki biblioteki Oat++:
.
├── AppComponent.hpp           # Plik nagłówkowy zawierający komponenty aplikacji, takie jak konfiguracja serwera i komponenty bazy danych.
├── cert                       # Katalog zawierający pliki certyfikatu SSL do zabezpieczania połączeń HTTPS.
│   ├── cert.pem               # Certyfikat SSL do zabezpieczania połączeń HTTPS.
│   └── key.pem                # Klucz prywatny SSL do zabezpieczania połączeń HTTPS.
├── CMakeLists.txt             # Plik konfiguracyjny CMake do budowania projektu.
├── controller                 # Katalog zawierający pliki nagłówkowe kontrolerów aplikacji.
│   ├── BookController.h       # Plik nagłówkowy definiujący kontroler API dla operacji związanych z książkami (tworzenie, odczyt, aktualizacja, usuwanie książek).
│   └── StaticController.h     # Plik nagłówkowy definiujący kontroler do obsługi statycznych zasobów (np. strona powitalna).
├── DatabaseComponent.hpp      # Plik nagłówkowy zawierający komponent bazy danych, odpowiedzialny za zarządzanie połączeniami z bazą danych.
├── db                         # Katalog zawierający pliki nagłówkowe i źródłowe związane z bazą danych.
│   └── BookDb.hpp             # Plik nagłówkowy zawierający definicje zapytań do bazy danych (CRUD) dla tabeli książek.
├── dto                        # Katalog zawierający pliki nagłówkowe obiektów transferu danych (DTO).
│   └── BookDto.hpp            # Plik nagłówkowy definiujący obiekt transferu danych (DTO) dla książki.
├── main.cpp                   # Główny plik źródłowy zawierający punkt wejścia do aplikacji i konfigurację serwera.
├── service                    # Katalog zawierający pliki nagłówkowe i źródłowe serwisów aplikacji.
│   ├── BookService.cpp        # Plik źródłowy zawierający implementację serwisu do zarządzania książkami (logika biznesowa).
│   └── BookService.hpp        # Plik nagłówkowy deklarujący serwis do zarządzania książkami (logika biznesowa).
├── sql                        # Katalog zawierający skrypty SQL do inicjalizacji bazy danych.
│   └── 001_init.sql           # Skrypt SQL do inicjalizacji bazy danych (tworzenie tabeli książek).
└── SwaggerComponent.hpp       # Plik nagłówkowy zawierający konfigurację Swaggera dla automatycznej dokumentacji API.

Aby kod działał, należy zainstalować oatpp-sqlite komendą np.:
./vcpkg/vcpkg install oatpp-sqlite


Plik CMakeLists.txt
Zacznijmy od pliku
CMakeLists.txt
. Tym razem ścieżki do pewnych zasobów są umieszczone jako definicje kompilacji, analogicznie jak w oficjalnych przykładach:
cmake_minimum_required(VERSION 3.22)

project(OatText LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

add_executable(${PROJECT_NAME} main.cpp
    AppComponent.hpp
    SwaggerComponent.hpp
    DatabaseComponent.hpp
    dto/BookDto.hpp
    sql/001_init.sql
    db/BookDb.hpp
    controller/BookController.h
    service/BookService.hpp service/BookService.cpp
    controller/StaticController.h)

target_include_directories(${PROJECT_NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

find_package(oatpp CONFIG REQUIRED)
find_package(oatpp-swagger CONFIG REQUIRED)
find_package(oatpp-libressl CONFIG REQUIRED)
find_package(LibreSSL 3.0.0 REQUIRED)
find_package(oatpp-sqlite CONFIG REQUIRED)

target_link_libraries(${PROJECT_NAME}
        PUBLIC oatpp::oatpp oatpp::oatpp-swagger oatpp::oatpp-sqlite
        PUBLIC oatpp::oatpp-libressl # <-- oatpp-libressl adapter

        # LibreSSL libraries
        PUBLIC LibreSSL::TLS
        PUBLIC LibreSSL::SSL
        PUBLIC LibreSSL::Crypto
)

add_definitions(
        ## Path to swagger-ui static resources folder
        -DOATPP_SWAGGER_RES_PATH="${oatpp-swagger_INCLUDE_DIRS}/../bin/oatpp-swagger/res"

        ## SQLite database file
        -DDATABASE_FILE="${CMAKE_CURRENT_SOURCE_DIR}/books.sqlite"

        ## Path to database migration scripts
        -DDATABASE_MIGRATIONS="${CMAKE_CURRENT_SOURCE_DIR}/sql"

        ## Certificates pathes
        -DCERT_PEM_PATH="${CMAKE_CURRENT_LIST_DIR}/cert/key.pem"
        -DCERT_CRT_PATH="${CMAKE_CURRENT_LIST_DIR}/cert/cert.pem"
)

Plik z główną funkcją programu: main.cpp
Tym razem jest on krótki:
C/C++
#include <oatpp/network/Server.hpp>
#include <oatpp-swagger/Controller.hpp>

#include "AppComponent.hpp"
#include "controller/BookController.h"
#include "controller/StaticController.h"

constexpr const char USER[ ] = "my_user";
constexpr const char PASSWORD[ ] = "my-secret-password";


void runApp( const char * programName )
{
   
/// Register Components in scope of run() method
   
ApplicationComponents components;
   
   
/// Get router component
   
OATPP_COMPONENT( std::shared_ptr < oatpp::web::server::HttpRouter >, router4HttpRequestsRouting );
   
   
auto bookController = router4HttpRequestsRouting->addController( BookController::createShared( USER, PASSWORD ) );
   
auto bookEndpoints = bookController->getEndpoints();
   
   
oatpp::web::server::api::Endpoints docEndpoints;
   
docEndpoints.append( bookEndpoints );
   
   
router4HttpRequestsRouting->addController( oatpp::swagger::Controller::createShared( docEndpoints ) );
   
router4HttpRequestsRouting->addController( StaticController::createShared() );
   
   
/// Get connection handler component
   
OATPP_COMPONENT( std::shared_ptr < oatpp::network::ConnectionHandler >, connectionHandler );
   
   
/// Get connection provider component
   
OATPP_COMPONENT( std::shared_ptr < oatpp::network::ServerConnectionProvider >, connectionProvider );
   
   
/// Create server which takes provided TCP connections and passes them to HTTP connection handler
   
oatpp::network::Server server( connectionProvider, connectionHandler );
   
   
/// Print info about server port
   
OATPP_LOGI( programName, "Server running on port %s", connectionProvider->getProperty( "port" ).getData() );
   
   
server.run();
}

int main( int, char * * argv )
{
   
oatpp::base::Environment::init();
   
   
runApp( argv[ 0 ] );
   
   
oatpp::base::Environment::destroy();
}

Plik zawierający reprezentacje DTO oraz enum: dto/BookDto.hpp
Poniżej plik transferu danych do obsługi książek, wraz z enumem odpowiadającym typowi książek:
C/C++
#pragma once

#include <oatpp/core/Types.hpp>
#include <oatpp/core/macro/codegen.hpp>

#include OATPP_CODEGEN_BEGIN(DTO)

ENUM( BookGenre, v_int32,
VALUE( UNKNOWN, 0, "unkown book genre" ),
VALUE( FICTION, 1, "fiction" ),
VALUE( NONFICTION, 1, "non-fiction" ),
VALUE( FANTASY, 1, "fantasy" ),
VALUE( BIOGRAPHY, 1, "biography" ),
VALUE( SCIFI, 1, "scifi" )
)
;


class BookDto
    : public oatpp::DTO
{
   
DTO_INIT( BookDto, DTO /* Extends */ )
   
   
DTO_FIELD( UInt32, id );
   
DTO_FIELD( String, title, "title" );
   
DTO_FIELD( String, author, "author" );
   
   
DTO_FIELD( Enum < BookGenre >::AsString, genre, "genre" );
};

#include OATPP_CODEGEN_END(DTO)
Tym razem, przez zastosowanie mechanizmu ORM nie potrzebujemy klasy
Book
, gdyż możemy bezpośrednio zapisywać te obiekty do bazy danych.

Kontroler API operacji związanych z książkami: controller/BookController.h
Jest on krótszy niż wcześniej, gdyż logikę związaną z operacjami deleguje do innej klasy:
C/C++
#pragma once

#include <oatpp/web/server/api/ApiController.hpp>

#include "dto/BookDto.hpp"
#include "service/BookService.hpp"


#include OATPP_CODEGEN_BEGIN(ApiController)  
///< Begin Codegen


class BookController
    : public oatpp::web::server::api::ApiController
{
   
const oatpp::String user, password;
   
   
BookService m_bookService; // (1)
   
public:
   
static std::shared_ptr < BookController > createShared( const oatpp::String & user, const oatpp::String & password, OATPP_COMPONENT( std::shared_ptr < ObjectMapper >, objectMapper ) /* Inject objectMapper component here as default parameter */ )
   
{
       
return std::make_shared < BookController >( user, password, objectMapper );
   
}
   
   
BookController( const oatpp::String & user, const oatpp::String & password, OATPP_COMPONENT( std::shared_ptr < ObjectMapper >, objectMapper ) /* Inject object mapper */ )
        :
oatpp::web::server::api::ApiController( objectMapper )
       
, user( user )
       
, password( password )
   
{
       
setDefaultAuthorizationHandler( std::make_shared < oatpp::web::server::handler::BasicAuthorizationHandler >( "Book API" ) );
   
}
   
   
ENDPOINT_INFO( createBook )
   
{
       
info->summary = "Create new book";
       
info->addConsumes < Object < BookDto >>( "application/json" );
       
info->addResponse < Object < BookDto >>( Status::CODE_200, "application/json" );
       
info->pathParams[ "author" ].required = true;
       
info->pathParams[ "title" ].required = true;
       
info->pathParams[ "title" ].name = "Title of the book";
       
info->pathParams[ "title" ].description = "Official title of book in polish language";
       
info->pathParams[ "title" ].allowEmptyValue = false;
       
       
info->addSecurityRequirement( "basicAuth" );
   
}
   
ENDPOINT( "POST", "/books", createBook,
   
BODY_DTO( Object < BookDto >, bookDto ),
   
AUTHORIZATION( std::shared_ptr < oatpp::web::server::handler::DefaultBasicAuthorizationObject >, authObject ) )
   
{
       
OATPP_ASSERT_HTTP( userAuthorised( authObject->userId, authObject->password ), Status::CODE_401, "Unauthorized" );
       
OATPP_ASSERT_HTTP( bookDto->title, Status::CODE_400, "'title' is required!" );
       
OATPP_ASSERT_HTTP( bookDto->author, Status::CODE_400, "'author' is required!" );
       
       
if( !bookDto->genre ) {
           
bookDto->genre = BookGenre::UNKNOWN; // (2)
       
}
       
       
return createDtoResponse( Status::CODE_200, m_bookService.createBook( bookDto ) ); // (3)
   
}
   
   
ENDPOINT_INFO( putBook )
   
{
       
info->summary = "Update Book by bookId";
       
info->addConsumes < Object < BookDto >>( "application/json" );
       
info->addResponse < Object < BookDto >>( Status::CODE_200, "application/json" );
       
info->addResponse < String >( Status::CODE_404, "text/plain" );
       
info->pathParams[ "bookId" ].description = "Book Identifier";
       
       
info->addSecurityRequirement( "basicAuth" );
   
}
   
ENDPOINT( "PUT", "/books/{bookId}", putBook,
   
PATH( UInt32, bookId ),
   
BODY_DTO( Object < BookDto >, bookDto ),
   
AUTHORIZATION( std::shared_ptr < oatpp::web::server::handler::DefaultBasicAuthorizationObject >, authObject ) )
   
{
       
OATPP_ASSERT_HTTP( userAuthorised( authObject->userId, authObject->password ), Status::CODE_401, "Unauthorized" );
       
bookDto->id = bookId;
       
return createDtoResponse( Status::CODE_200, m_bookService.updateBook( bookDto ) );
   
}
   
   
ENDPOINT_INFO( getBookById )
   
{
       
info->summary = "Get one Book by bookId";
       
info->addResponse < Object < BookDto >>( Status::CODE_200, "application/json" );
       
info->addResponse < String >( Status::CODE_404, "text/plain" );
       
info->pathParams[ "bookId" ].description = "Book Identifier";
   
}
   
ENDPOINT( "GET", "/books/{bookId}", getBookById,
   
PATH( UInt32, bookId ) )
   
{
       
return createDtoResponse( Status::CODE_200, m_bookService.getBookById( bookId ) );
   
}
   
   
ENDPOINT_INFO( getBooks )
   
{
       
info->summary = "Get all stored books";
       
info->addResponse < List < Object < BookDto >> >( Status::CODE_200, "application/json" );
   
}
   
ENDPOINT( "GET", "/books", getBooks )
   
{
       
return createDtoResponse( Status::CODE_200, m_bookService.getAllBooks() );
   
}
   
   
ENDPOINT_INFO( deleteBook )
   
{
       
info->summary = "Delete Book by bookId";
       
info->addResponse < String >( Status::CODE_200, "text/plain" );
       
info->addResponse < String >( Status::CODE_404, "text/plain" );
       
info->pathParams[ "bookId" ].description = "Book Identifier";
       
       
info->addSecurityRequirement( "basicAuth" );
   
}
   
ENDPOINT( "DELETE", "/books/{bookId}", deleteBook,
   
PATH( UInt32, bookId ),
   
AUTHORIZATION( std::shared_ptr < oatpp::web::server::handler::DefaultBasicAuthorizationObject >, authObject ) )
   
{
       
OATPP_ASSERT_HTTP( userAuthorised( authObject->userId, authObject->password ), Status::CODE_401, "Unauthorized" );
       
       
if( bool bookDeletedWithSuccess = m_bookService.deleteBookById( bookId ) )
       
{
           
return createResponse( Status::CODE_200, "Book successfully deleted" );
       
}
       
else
       
{
           
return createResponse( Status::CODE_404, "Book not found" );
       
}
    }
   
private:
   
bool userAuthorised( const oatpp::String & user, const oatpp::String & password ) const
   
{
       
constexpr const char USER[ ] = "my_user";
       
constexpr const char PASSWORD[ ] = "my-secret-password";
       
       
return user == this->user && password == this->password;
   
}
}
;

#include OATPP_CODEGEN_END(ApiController)  ///< End Codegen
Wyjaśnienie kilku miejsc w programie:
1. Utworzenie instancji
BookService
, która zajmuje się wykonywaniem operacji CRUD na bazie danych i obsługą błędów.
2. Tutaj jest obsługa opcjonalnego typu wyliczeniowego - w razie niepodania ustawiamy wartość domyślną.
3. Nasz obiekt z 1. zajmuje się generowaniem odpowiedzi (zwraca obiekt DTO, który jest konwertowalny na reprezentacje tekstową).
Kolejną różnicą jest to, że login i hasło użytkownika są ustawiane w konstruktorze.

Plik odpowiadający za operacje CRUD na bazie danych: db/BookDb.hpp
W tym pliku jest zaprezentowany mechanizm ORM dostarczany przez bibliotekę, schowany za makrami preprocesora. Na uwagę zasługuje też konstruktor, który automatycznie tworzy bazę danych (ewentualnie aktualizuje do innej wersji).
C/C++
#pragma once

#include <oatpp-sqlite/orm.hpp>
#include "dto/BookDto.hpp"

#include OATPP_CODEGEN_BEGIN(DbClient)  
//<- Begin Codegen

class BookDb
    : public oatpp::orm::DbClient
{
public:
   
BookDb( const std::shared_ptr < oatpp::orm::Executor > & executor )
        :
oatpp::orm::DbClient( executor )
   
{
       
oatpp::orm::SchemaMigration migration( executor );
       
migration.addFile( 1 /* start from version 1 */, DATABASE_MIGRATIONS "/001_init.sql" );
       
migration.migrate(); // <-- run migrations. This will throw on error.
       
       
auto version = executor->getSchemaVersion();
       
OATPP_LOGD( "BookDb", "Migration - OK. Version=%lld.", version );
   
}
   
   
QUERY( createBook,
   
"INSERT INTO Book"
    "(title, author, genre) VALUES "
    "(:book.title, :book.author, :book.genre);"
,
   
PARAM( oatpp::Object < BookDto >, book ) )
   
   
QUERY( updateBook,
   
"UPDATE Book "
    "SET "
    " title=:book.title, "
    " author=:book.author, "
    " genre=:book.genre "
    "WHERE "
    " id=:book.id;"
,
   
PARAM( oatpp::Object < BookDto >, book ) )
   
   
QUERY( getBookById,
   
"SELECT * FROM Book WHERE id=:id;",
   
PARAM( oatpp::UInt32, id ) )
   
   
QUERY( getAllBooks,
   
"SELECT * FROM Book LIMIT :limit OFFSET :offset;",
   
PARAM( oatpp::UInt32, offset ),
   
PARAM( oatpp::UInt32, limit ) )
   
   
QUERY( deleteBookById,
   
"DELETE FROM Book WHERE id=:id;",
   
PARAM( oatpp::UInt32, id ) )
};

#include OATPP_CODEGEN_END(DbClient)  //<- End Codegen
Alternatywnie można dodawać kolejną wersję w formie tekstowej zamiast z pliku.

Plik tworzący schemat bazy danych: sql/001_init.sql
Jest to plik dla bazy danych SQLite3
CREATE TABLE Book (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title TEXT NOT NULL,
    author TEXT NOT NULL,
    genre VARCHAR NOT NULL DEFAULT 0
);

Klasa odpowiadająca za logikę biznesową operacji na książkach: BookService
Są to dwa pliki:
service/BookService.hpp
:
C/C++
#pragma once

#include <oatpp/web/protocol/http/Http.hpp>
#include <oatpp/core/macro/component.hpp>
#include "db/BookDb.hpp"

class BookService
{
   
typedef oatpp::web::protocol::http::Status Status;
   
   
OATPP_COMPONENT( std::shared_ptr < BookDb >, m_database ); // Inject database component
   
public:
   
oatpp::Object < BookDto > createBook( const oatpp::Object < BookDto > & dto );
   
oatpp::Object < BookDto > updateBook( const oatpp::Object < BookDto > & dto );
   
oatpp::Object < BookDto > getBookById( oatpp::UInt32 id, const oatpp::provider::ResourceHandle < oatpp::orm::Connection > & connection = nullptr );
   
oatpp::Vector < oatpp::Object < BookDto >> getAllBooks( oatpp::UInt32 offset = 0u, oatpp::UInt32 limit = 100u );
   
bool deleteBookById( oatpp::UInt32 id );
};
oraz
service/BookService.cpp
:
C/C++
#include "BookService.hpp"


oatpp::Object < BookDto > BookService::createBook( const oatpp::Object < BookDto > & dto )
{
   
auto dbResult = m_database->createBook( dto );
   
OATPP_ASSERT_HTTP( dbResult->isSuccess(), Status::CODE_500, dbResult->getErrorMessage() );
   
   
auto bookId = oatpp::sqlite::Utils::getLastInsertRowId( dbResult->getConnection() );
   
   
return getBookById(( v_int32 ) bookId );
}

oatpp::Object < BookDto > BookService::updateBook( const oatpp::Object < BookDto > & dto )
{
   
auto dbResult = m_database->updateBook( dto );
   
OATPP_ASSERT_HTTP( dbResult->isSuccess(), Status::CODE_500, dbResult->getErrorMessage() );
   
return getBookById( dto->id );
}

oatpp::Object < BookDto > BookService::getBookById( oatpp::UInt32 id, const oatpp::provider::ResourceHandle < oatpp::orm::Connection > & connection )
{
   
auto dbResult = m_database->getBookById( id, connection );
   
OATPP_ASSERT_HTTP( dbResult->isSuccess(), Status::CODE_500, dbResult->getErrorMessage() );
   
OATPP_ASSERT_HTTP( dbResult->hasMoreToFetch(), Status::CODE_404, "Book not found" );
   
   
auto result = dbResult->fetch < oatpp::Vector < oatpp::Object < BookDto >> >();
   
OATPP_ASSERT_HTTP( result->size() == 1, Status::CODE_500, "Unknown error" );
   
   
return result[ 0 ];
}

oatpp::Vector < oatpp::Object < BookDto >> BookService::getAllBooks( oatpp::UInt32 offset, oatpp::UInt32 limit )
{
   
auto dbResult = m_database->getAllBooks( offset, limit );
   
OATPP_ASSERT_HTTP( dbResult->isSuccess(), Status::CODE_500, dbResult->getErrorMessage() );
   
   
auto items = dbResult->fetch < oatpp::Vector < oatpp::Object < BookDto >> >();
   
   
return items;
}

bool BookService::deleteBookById( oatpp::UInt32 bookId )
{
   
auto dbResult = m_database->deleteBookById( bookId );
   
OATPP_ASSERT_HTTP( dbResult->isSuccess(), Status::CODE_500, dbResult->getErrorMessage() );
   
return true;
}

Plik zawierający komponent bazy danych, odpowiedzialny za zarządzanie połączeniami z bazą danych: DatabaseComponent.hpp
W tym pliku należałoby dokonać zmian, gdybyśmy chcieli zmienić typ bazy danych np. na PostgreSQL:
C/C++
#pragma once

#include <oatpp/network/tcp/server/ConnectionProvider.hpp>
#include <oatpp/core/macro/component.hpp>
#include "db/BookDb.hpp"


class DatabaseComponent
{
public:
   
/// Create database connection provider component
   
OATPP_CREATE_COMPONENT( std::shared_ptr < oatpp::provider::Provider < oatpp::sqlite::Connection >>, dbConnectionProvider )([ ] {
       
       
/// Create database-specific ConnectionProvider
       
auto connectionProvider = std::make_shared < oatpp::sqlite::ConnectionProvider >( DATABASE_FILE );
       
       
/// Create database-specific ConnectionPool
       
return oatpp::sqlite::ConnectionPool::createShared( connectionProvider,
       
10 /* max-connections */,
       
std::chrono::seconds( 5 ) /* connection TTL */ );
   
}() );
   
   
/// Create database client
   
OATPP_CREATE_COMPONENT( std::shared_ptr < BookDb >, bookDb )([ ] {
       
/// Get database ConnectionProvider component
       
OATPP_COMPONENT( std::shared_ptr < oatpp::provider::Provider < oatpp::sqlite::Connection >>, connectionProvider );
       
       
/// Create database-specific Executor
       
auto executor = std::make_shared < oatpp::sqlite::Executor >( connectionProvider );
       
       
/// Create MyClient database client
       
return std::make_shared < BookDb >( executor );
   
}() );
};

Klasa zawierająca komponenty aplikacji: AppComponent.hpp
Jedyną różnicą w stosunku do poprzedniej wersji jest dodanie komponentu bazy danych:
C/C++
#pragma once

#include <oatpp-libressl/server/ConnectionProvider.hpp>
#include <oatpp/parser/json/mapping/ObjectMapper.hpp>
#include <oatpp/core/macro/component.hpp>
#include <oatpp/network/tcp/server/ConnectionProvider.hpp>
#include <oatpp/web/server/HttpConnectionHandler.hpp>
#include <oatpp/web/server/HttpRouter.hpp>
#include "SwaggerComponent.hpp"
#include "DatabaseComponent.hpp"


constexpr const int listeningPortHttps = 8443;
constexpr const char listeningAddress[ ] = "localhost";

/// Class which creates and holds Application components and registers components in oatpp::base::Environment
class ApplicationComponents
{
public:
   
SwaggerComponent swaggerComponent;
   
   
DatabaseComponent databaseComponent; // (1)
   
    /// Create ConnectionProvider component which listens on the port
   
OATPP_CREATE_COMPONENT( std::shared_ptr < oatpp::network::ServerConnectionProvider >, serverConnectionProvider )([ ] {
       
OATPP_LOGD( "oatpp::libressl::Config", "pem='%s'", CERT_PEM_PATH );
       
OATPP_LOGD( "oatpp::libressl::Config", "crt='%s'", CERT_CRT_PATH );
       
auto config = oatpp::libressl::Config::createDefaultServerConfigShared( CERT_CRT_PATH, CERT_PEM_PATH /* private key */ );
       
       
/** if you see such error:
     * oatpp::libressl::server::ConnectionProvider:Error on call to 'tls_configure'. ssl context failure
     * It might be because you have several ssl libraries installed on your machine.
     * Try to make sure you are using libtls, libssl, and libcrypto from the same package
     */
       
       
return oatpp::libressl::server::ConnectionProvider::createShared( config, { listeningAddress, listeningPortHttps, oatpp::network::Address::IP_4 } );
   
}() );
   
   
/// Create Router component
   
OATPP_CREATE_COMPONENT( std::shared_ptr < oatpp::web::server::HttpRouter >, httpRouter )([ ] {
       
return oatpp::web::server::HttpRouter::createShared();
   
}() );
   
   
/// Create ConnectionHandler component which uses Router component to route requests
   
OATPP_CREATE_COMPONENT( std::shared_ptr < oatpp::network::ConnectionHandler >, serverConnectionHandler )([ ] {
       
OATPP_COMPONENT( std::shared_ptr < oatpp::web::server::HttpRouter >, router ); // get Router component
        /// Async ConnectionHandler for Async IO and Coroutine based endpoints
       
return oatpp::web::server::HttpConnectionHandler::createShared( router );
   
}() );
   
   
/// Create ObjectMapper component to serialize/deserialize DTOs in Contoller's API
   
OATPP_CREATE_COMPONENT( std::shared_ptr < oatpp::data::mapping::ObjectMapper >, apiObjectMapper )([ ] {
       
auto serializerConfig = oatpp::parser::json::mapping::Serializer::Config::createShared();
       
auto deserializerConfig = oatpp::parser::json::mapping::Deserializer::Config::createShared();
       
deserializerConfig->allowUnknownFields = false;
       
auto objectMapper = oatpp::parser::json::mapping::ObjectMapper::createShared( serializerConfig, deserializerConfig );
       
return objectMapper;
   
}() );
};

Pliki bez zmian: SwaggerComponent.hpp (konfiguracja dokumentacji) i controller/StaticController.h (strona główna HTML)
C/C++
#pragma once

#include <oatpp-swagger/Model.hpp>
#include <oatpp-swagger/Resources.hpp>
#include <oatpp/core/macro/component.hpp>

/** Swagger ui is served at
 *  http://host:port/swagger/ui */
class SwaggerComponent
{
public:
   
/// General API docs info
   
OATPP_CREATE_COMPONENT( std::shared_ptr < oatpp::swagger::DocumentInfo >, swaggerDocumentInfo )([ ]
   
{
       
oatpp::swagger::DocumentInfo::Builder builder;
       
       
builder
        .setTitle( "Book API" )
        .
setDescription( "Example Book API with Swagger Documentation" )
        .
setVersion( "1.0" )
        .
setContactName( "Example Contact" )
        .
setContactUrl( "https://example.com" )
        .
setLicenseName( "MIT" )
        .
setLicenseUrl( "https://opensource.org/licenses/MIT" )
        .
addServer( "https://localhost:8443", "server on localhost" );
       
       
return builder.build();
   
}() );
   
   
/// Swagger-Ui Resources (<oatpp-examples>/lib/oatpp-swagger/res)
   
OATPP_CREATE_COMPONENT( std::shared_ptr < oatpp::swagger::Resources >, swaggerResources )([ ]
   
{
       
// Make sure to specify correct full path to oatpp-swagger/res folder !!!
       
return oatpp::swagger::Resources::loadResources( OATPP_SWAGGER_RES_PATH );
   
}() );
};
i
C/C++
#pragma once

#include <oatpp/core/macro/component.hpp>
#include <oatpp/web/server/api/ApiController.hpp>

#include OATPP_CODEGEN_BEGIN(ApiController)  
///< Begin Codegen

class StaticController
    : public oatpp::web::server::api::ApiController
{
public:
   
StaticController( const std::shared_ptr < ObjectMapper > & objectMapper )
        :
oatpp::web::server::api::ApiController( objectMapper )
   
{ }
   
   
static std::shared_ptr < StaticController > createShared(
   
OATPP_COMPONENT( std::shared_ptr < ObjectMapper >, objectMapper ) /* Inject objectMapper component here as default parameter */ )
   
{
       
return std::make_shared < StaticController >( objectMapper );
   
}
   
   
ENDPOINT( "GET", "/", root ) {
       
const char * html =
       
R"(<html lang='pl'> <head> <meta charset=utf-8/> </head> <body> <p>Witaj w bazie ksiazek!</p> <a href='swagger/ui'>Zobacz dokumentacje wygenerowana przez Swagger-UI</a> </body> </html>)";
       
auto response = createResponse( Status::CODE_200, html );
       
response->putHeader( Header::CONTENT_TYPE, "text/html" );
       
return response;
   
}
}
;

#include OATPP_CODEGEN_END(ApiController)  ///< End Codegen

Kod klienta
Kod klienta jest niemalże identyczny, trzeba zastosować zaktualizowane typy DTO, oraz wpisać wartości, jedynie trzeba pokombinować aby wyświetlić wartość wyliczenia w formie tekstowej:
C/C++
////////// file: ClientComponent.hpp
#include <oatpp/parser/json/mapping/ObjectMapper.hpp>
#include <oatpp-libressl/Config.hpp>
#include <oatpp-libressl/client/ConnectionProvider.hpp>
#include <oatpp/core/macro/component.hpp>

constexpr const int listeningPortHttps = 8443;
constexpr const char listeningAddress[ ] = "localhost";


class ClientComponent // (1)
{
public:
   
OATPP_CREATE_COMPONENT( std::shared_ptr < oatpp::network::ClientConnectionProvider >, sslClientConnectionProvider )([ ] {
       
auto config = oatpp::libressl::Config::createShared();
       
tls_config_insecure_noverifycert( config->getTLSConfig() );
       
tls_config_insecure_noverifyname( config->getTLSConfig() );
       
return oatpp::libressl::client::ConnectionProvider::createShared( config, { listeningAddress, listeningPortHttps } );
   
}() );
};


////////// file: BooksDto.hpp:
#include <oatpp/core/macro/codegen.hpp>
#include <oatpp-swagger/Controller.hpp>


#include OATPP_CODEGEN_BEGIN(DTO)

ENUM( BookGenre, v_int32,
VALUE( UNKNOWN, 0, "unkown book genre" ),
VALUE( FICTION, 1, "fiction" ),
VALUE( NONFICTION, 1, "non-fiction" ),
VALUE( FANTASY, 1, "fantasy" ),
VALUE( BIOGRAPHY, 1, "biography" ),
VALUE( SCIFI, 1, "scifi" )
)
;


class BookDto
    : public oatpp::DTO
{
   
DTO_INIT( BookDto, DTO /* Extends */ )
   
   
DTO_FIELD( UInt32, id );
   
DTO_FIELD( String, title, "title" );
   
DTO_FIELD( String, author, "author" );
   
   
DTO_FIELD( Enum < BookGenre >::AsString, genre, "genre" );
};

#include OATPP_CODEGEN_END(DTO)


////////// file: Client.hpp
// #pragma once

#include <oatpp/web/client/ApiClient.hpp>

#include OATPP_CODEGEN_BEGIN(ApiClient)  
//<- Begin Codegen

class BookClient
    : public oatpp::web::client::ApiClient
{
public:
   
API_CLIENT_INIT( BookClient )
   
   
API_CALL( "POST", "/books", createBook, BODY_DTO( Object < BookDto >, bookDto ), HEADER( String, authorization, "Authorization" ) )
   
API_CALL( "GET", "/books", getBooks )
   
API_CALL( "GET", "/books/{id}", getBookById, PATH( UInt64, id ) )
   
API_CALL( "PUT", "/books/{id}", updateBook, PATH( UInt64, id ), BODY_DTO( Object < BookDto >, bookDto ), HEADER( String, authorization, "Authorization" ) )
   
API_CALL( "DELETE", "/books/{id}", deleteBook, PATH( UInt64, id ), HEADER( String, authorization, "Authorization" ) )
};

#include OATPP_CODEGEN_END(ApiClient)  //<- End Codegen


////////// file: main.cpp
#include <iostream>
#include <oatpp/network/tcp/client/ConnectionProvider.hpp>
#include <oatpp/web/client/HttpRequestExecutor.hpp>

constexpr const char * USER = "my_user";
constexpr const char * PASSWORD = "my-secret-password";


void addBook( std::string_view basicAuthHeader, const char * title, const char * author, BookGenre genre, auto & bookApiClient );

void printBooks( auto & bookApiClient, auto & objectMapper );

void updateBook( std::string_view basicAuthHeader, v_uint64 id, const char * title, const char * author, BookGenre genre, auto & bookApiClient );

void deleteBook( std::string_view basicAuthHeader, v_uint64 id, auto & bookApiClient );

void printBookById( v_uint64 id, auto & bookApiClient, auto & objectMapper );

void runClient();

std::string base64_encode( const std::string & in ); // ignore this function

std::string createBasicAuthHeader( const std::string & user, const std::string & password )
{
   
return "Basic " + base64_encode( user + ":" + password );
}

///////////////// main()
int main()
{
   
oatpp::base::Environment::init();
   
runClient();
   
   
std::cout << "\nEnvironment:\n";
   
std::cout << "objectsCount = " << oatpp::base::Environment::getObjectsCount() << "\n";
   
std::cout << "objectsCreated = " << oatpp::base::Environment::getObjectsCreated() << "\n\n";
   
   
oatpp::base::Environment::destroy();
}


void runClient()
{
   
std::shared_ptr < oatpp::web::client::HttpRequestExecutor > httpRequestExecutor;
   
   
constexpr bool httpsConnection = true;
   
if constexpr( httpsConnection )
   
{
       
ClientComponent clientComponent;
       
OATPP_COMPONENT( std::shared_ptr < oatpp::network::ClientConnectionProvider >, sslClientConnectionProvider );
       
httpRequestExecutor = oatpp::web::client::HttpRequestExecutor::createShared( sslClientConnectionProvider );
   
}
   
else
   
{
       
/// for HTTP:
       
auto connectionProvider = oatpp::network::tcp::client::ConnectionProvider::createShared( { "localhost", 8000, oatpp::network::Address::IP_4 } );
       
httpRequestExecutor = oatpp::web::client::HttpRequestExecutor::createShared( connectionProvider );
   
}
   
   
auto jsonObjectMapper = oatpp::parser::json::mapping::ObjectMapper::createShared();
   
   
auto bookApiClient = BookClient::createShared( httpRequestExecutor, jsonObjectMapper );
   
   
std::string basicAuthHeader = createBasicAuthHeader( USER, PASSWORD );
   
   
addBook( basicAuthHeader, "Opus magnum C++11", "Jerzy Grebosz", BookGenre::FICTION, bookApiClient );
   
addBook( basicAuthHeader, "Skuteczny nowoczesny C++. 42 sposoby lepszego poslugiwania się jezykami C++11 i C++14", "Scott Meyers", BookGenre::NONFICTION, bookApiClient );
   
addBook( basicAuthHeader, "C++17 STL. Receptury", "Jacek Galowicz", BookGenre::NONFICTION, bookApiClient );
   
   
printBooks( bookApiClient, jsonObjectMapper );
   
   
std::cout << std::endl;
   
   
updateBook( basicAuthHeader, 1, "Misja w nadprzestrzen C++14/17", "Jerzy Grebosz", BookGenre::FANTASY, bookApiClient );
   
   
printBookById( 1, bookApiClient, jsonObjectMapper );
   
   
deleteBook( basicAuthHeader, 1, bookApiClient );
   
   
printBooks( bookApiClient, jsonObjectMapper );
   
   
   
auto rawData = bookApiClient->getBooks()->readBodyToString();
   
OATPP_LOGD( BookClient::TAG, "[doGet] data='%s'", rawData->c_str() );
}

void addBook( std::string_view basicAuthHeader, const char * title, const char * author, BookGenre genre, auto & bookApiClient )
{
   
auto book = BookDto::createShared();
   
book->title = title;
   
book->author = author;
   
book->genre = genre;
   
   
auto response = bookApiClient->createBook( book, basicAuthHeader.data() );
   
   
const BookClient::Status status { response->getStatusCode(), "creation book error" };
   
OATPP_ASSERT_HTTP( response->getStatusCode() == 200, status, std::string( "Failed to create book: " ) + title );
}

void printBooks( auto & bookApiClient, auto & objectMapper )
{
   
auto response = bookApiClient->getBooks();
   
   
const BookClient::Status status { response->getStatusCode(), "getting books error" };
   
OATPP_ASSERT_HTTP( response->getStatusCode() == 200, status, "Failed to get books" );
   
   
auto books = response->template readBodyToDto < oatpp::List < oatpp::Object < BookDto >> >( objectMapper );
   
for( const auto & book: * books )
   
{
       
std::cout << "Book ID: " << book->id << std::endl;
       
std::cout << "Title: " << book->title->c_str() << std::endl;
       
std::cout << "Author: " << book->author->c_str() << std::endl;
       
std::cout << "Genre: " << book->genre.getEntryByValue( * book->genre ).name.std_str() << std::endl;
       
std::cout << "--------" << std::endl;
   
}
}

void updateBook( std::string_view basicAuthHeader, v_uint64 id, const char * title, const char * author, BookGenre genre, auto & bookApiClient )
{
   
auto book = BookDto::createShared();
   
book->title = title;
   
book->author = author;
   
book->genre = genre;
   
   
auto response = bookApiClient->updateBook( id, book, basicAuthHeader.data() );
   
const BookClient::Status status { response->getStatusCode(), "updating book error" };
   
OATPP_ASSERT_HTTP( response->getStatusCode() == 200, status, std::string( "Failed to update book: " ) + title );
}

void deleteBook( std::string_view basicAuthHeader, v_uint64 id, auto & bookApiClient )
{
   
auto response = bookApiClient->deleteBook( id, basicAuthHeader.data() );
   
const BookClient::Status status { response->getStatusCode(), "deleting book error" };
   
OATPP_ASSERT_HTTP( response->getStatusCode() == 200, status, std::string( "Failed to delete book with ID: " ) + std::to_string( id ) );
}

void printBookById( v_uint64 id, auto & bookApiClient, auto & objectMapper )
{
   
auto response = bookApiClient->getBookById( id );
   
const BookClient::Status status { response->getStatusCode(), "getting book by id error" };
   
OATPP_ASSERT_HTTP( response->getStatusCode() == 200, status, "Failed to get book by ID" );
   
   
auto book = response->template readBodyToDto < oatpp::Object < BookDto >>( objectMapper );
   
std::cout << "Book ID: " << book->id << std::endl;
   
std::cout << "Title: " << book->title->c_str() << std::endl;
   
std::cout << "Author: " << book->author->c_str() << std::endl;
   
std::cout << "Genre: " << book->genre.getEntryByValue( * book->genre ).name.std_str() << std::endl;
   
std::cout << "--------" << std::endl;
}


std::string base64_encode( const std::string & in ) // ignore this function
{
   
std::string out;
   
   
int val = 0, valb = - 6;
   
for( unsigned char c: in )
   
{
       
val =( val << 8 ) + c;
       
valb += 8;
       
while( valb >= 0 )
       
{
           
out.push_back( "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"[( val >> valb ) & 0x3F ] );
           
valb -= 6;
       
}
    }
   
if( valb > - 6 )
       
 out.push_back( "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"[(( val << 8 ) >>( valb + 8 ) ) & 0x3F ] );
   
   
while( out.size() % 4 )
       
 out.push_back( '=' );
   
   
return out;
}

Należy mieć na uwadze jednak to, że w SQLite3 klucz główny zaczyna od 1, a nie od 0.

Inne możliwości biblioteki Oat++

Biblioteka Oat++ jest naprawdę rozbudowana, o jej możliwościach można by napisać wiele artykułów. Postaram się jednak wymienić najważniejsze funkcjonalności, z odpowiednimi odnośnikami:

Testowanie aplikacji

Oat++ posiada funkcjonalności umożliwiające pisanie testów, które są częściowo opisane w sekcji poradnika Start krok po kroku. Testy są również obecne w oficjalnych przykładach, na przykład w przykładzie CRUD. Aby napisać testy, tworzy się klienckie API, które zostało opisane w ramach tego artykułu.

Obsługa plików binarnych

Oat++ wspiera również ładowanie plików. Oficjalny poradnik na ten temat znajduje się tutaj.

Obsługa CORS

Obsługa CORS (Cross-Origin Resource Sharing) w Oat++ realizowana jest za pomocą makr, które pozwalają na definiowanie polityki CORS dla poszczególnych endpointów. Dzięki temu aplikacje webowe mogą bezpiecznie komunikować się z serwerem Oat++ z różnych domen. Dokumentacja dotycząca konfiguracji CORS znajduje się na stronie CORS w Oat++. Poniżej znajduje się przykładowy kod, który dodaje obsługę CORS do endpointu:

C/C++
ADD_CORS( putUser )
ENDPOINT( "PUT", "/users/{userId}", putUser,
PATH( Int64, userId ),
BODY_DTO( Object < UserDto >, userDto ) )
{
   
userDto->id = userId;
   
return createDtoResponse( Status::CODE_200, m_database->updateUser( userDto ) );
}
W powyższym przykładzie makro `ADD_CORS(putUser)` dodaje obsługę CORS do endpointu `putUser`, co umożliwia przeglądarkom wykonywanie żądań typu PUT z innych domen. Dzięki temu mechanizmowi Oat++ pozwala na tworzenie bardziej elastycznych i bezpiecznych aplikacji webowych, które mogą komunikować się z serwerem bez ograniczeń związanych z polityką CORS.

Inne moduły

Oat++ zawiera wiele modułów, które umożliwiają integrację z różnymi usługami. W artykule używałem modułów: oatpp-libressl, oatpp-swagger i oatpp-sqlite, ale jest ich więcej:

  • oatpp-consul - Moduł integrujący Oat++ z HashiCorp Consul, narzędziem do rejestracji i odkrywania usług w sieci. Dzięki temu modułowi aplikacje Oat++ mogą automatycznie rejestrować swoje usługi w Consul, co pozwala innym usługom na łatwe ich odnalezienie i wykorzystanie. Dodatkowo, oatpp-consul umożliwia monitorowanie stanu zdrowia zarejestrowanych usług, co zwiększa niezawodność i skalowalność całego systemu.
  • oatpp-curl - Moduł umożliwiający wykonywanie zapytań HTTP przy użyciu libcurl.
  • oatpp-libressl - Moduł dodający wsparcie dla SSL/TLS za pomocą biblioteki LibreSSL.
  • oatpp-mbedtls - Moduł zapewniający wsparcie dla SSL/TLS przy użyciu biblioteki mbedTLS.
  • oatpp-mongo - Adapter dla MongoDB, umożliwiający integrację z bazą danych NoSQL MongoDB.
  • oatpp-postgresql - Adapter dla PostgreSQL, umożliwiający integrację z relacyjną bazą danych PostgreSQL.
  • oatpp-sqlite - Adapter SQLite dla Oat++, umożliwiający korzystanie z lekkiej, wbudowanej bazy danych SQLite.
  • oatpp-ssdp - Moduł do implementacji Simple Service Discovery Protocol (SSDP), używanego do odkrywania urządzeń w sieciach lokalnych. SSDP jest częścią standardu UPnP (Universal Plug and Play) i umożliwia urządzeniom takim jak drukarki, kamery czy inne urządzenia IoT ogłaszanie swojej obecności w sieci. Dzięki oatpp-ssdp aplikacje Oat++ mogą łatwo odnajdywać i komunikować się z innymi urządzeniami w tej samej sieci lokalnej, co jest szczególnie użyteczne w środowiskach IoT i smart home.
  • oatpp-swagger - Moduł integrujący Swagger UI z Oat++, umożliwiający generowanie i prezentowanie dokumentacji API.
  • oatpp-websocket - Moduł dodający wsparcie dla WebSocket, umożliwiający dwukierunkową komunikację w czasie rzeczywistym.
  • oatpp-zlib - Moduł dodający wsparcie dla kompresji i dekompresji danych przy użyciu biblioteki zlib.

Dostępne przykłady zastosowań biblioteki

Oat++ ma dokumentacje jaką ma, tutorial ma jeden, najwięcej można zgłębić bibliotekę analizując oficjalne przykłady (wymieniam te, których wcześniej nie wymieniałem):

Podsumowanie

Biblioteka Oat++ w wersji 1.3.0 to potężne narzędzie do tworzenia usług webowych w C++. Mimo swojej złożoności, oferuje wiele funkcji ułatwiających tworzenie skalowalnych i wydajnych aplikacji webowych. Mam nadzieję, że dzięki temu artykułowi zrozumiesz podstawy korzystania z tej biblioteki i będziesz mógł/mogła stworzyć własny serwis REST w C++.

Bibliografia


Autorzy artykułu

Autor Zakres zmian Data Afiliacja
Bazior Grzegorz Utworzenie artykułu sierpień 2024 Pracownik AGH w Krakowie