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
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
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:
Przykłady użycia tych komend w kontekście konkretnego przykładu, opisywanego w ramach tego artykułu - aplikacji do zarządzania książkami:
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
#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 {
public:
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();
router4HttpRequestsRouting->route( "GET", routeSubaddress, std::make_shared < CustomRequestHandler >() ); auto httpConnectionHandler = oatpp::web::server::HttpConnectionHandler::createShared( router4HttpRequestsRouting );
auto tcpConnectionProvider = oatpp::network::tcp::server::ConnectionProvider::createShared( { listeningAddress, listeningPort, oatpp::network::Address::IP_4 } ); oatpp::network::Server server( tcpConnectionProvider, httpConnectionHandler );
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
#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> constexpr const int listeningPort = 8000;
constexpr const char listeningAddress[ ] = "localhost";
constexpr const char routeSubaddress[ ] = "/hello";
#include OATPP_CODEGEN_BEGIN(DTO)
class MessageDto
: public oatpp::DTO {
DTO_INIT( MessageDto, DTO )
DTO_FIELD( Int32, statusCode ); DTO_FIELD( String, message ); };
#include OATPP_CODEGEN_END(DTO)
class CustomRequestHandler2
: public oatpp::web::server::HttpRequestHandler
{
private:
std::shared_ptr < oatpp::data::mapping::ObjectMapper > m_objectMapper;
public:
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(); auto router4HttpRequestsRouting = oatpp::web::server::HttpRouter::createShared();
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 } );
oatpp::network::Server server( tcpConnectionProvider, httpConnectionHandler );
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ć:
#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.:
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.:
#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 ApplicationComponents
{
public:
OATPP_CREATE_COMPONENT( std::shared_ptr
< oatpp::network::ServerConnectionProvider >, serverConnectionProvider )([ ] {
return oatpp::network::tcp::server::ConnectionProvider::createShared( { listeningAddress, listeningPort, oatpp::network::Address::IP_4 } );
}() );
OATPP_CREATE_COMPONENT( std::shared_ptr < oatpp::web::server::HttpRouter >, httpRouter )([ ] {
return oatpp::web::server::HttpRouter::createShared();
}() );
OATPP_CREATE_COMPONENT( std::shared_ptr < oatpp::network::ConnectionHandler >, serverConnectionHandler )([ ] {
OATPP_COMPONENT( std::shared_ptr < oatpp::web::server::HttpRouter >, router ); return oatpp::web::server::HttpConnectionHandler::createShared( router );
}() );
OATPP_CREATE_COMPONENT( std::shared_ptr < oatpp::data::mapping::ObjectMapper >, apiObjectMapper )([ ] {
return oatpp::parser::json::mapping::ObjectMapper::createShared();
}() );
};
Cały pozostały kod:
#include <oatpp/network/Server.hpp>
#include <oatpp/core/macro/codegen.hpp>
#include <oatpp/web/server/api/ApiController.hpp>
#include "AppComponent.hpp"
#include OATPP_CODEGEN_BEGIN(DTO)
class MessageDto
: public oatpp::DTO
{
DTO_INIT( MessageDto, DTO )
DTO_FIELD( Int32, statusCode ); DTO_FIELD( String, message ); };
#include OATPP_CODEGEN_END(DTO)
#include OATPP_CODEGEN_BEGIN(ApiController) class MyApiController
: public oatpp::web::server::api::ApiController
{
public:
MyApiController( OATPP_COMPONENT( std::shared_ptr < ObjectMapper >, objectMapper ) )
: oatpp::web::server::api::ApiController( objectMapper )
{ }
public:
ENDPOINT( "GET", "/hello", root ) {
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) 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 )
{
ApplicationComponents components;
OATPP_COMPONENT( std::shared_ptr < oatpp::web::server::HttpRouter >, router4HttpRequestsRouting );
auto myApiController = std::make_shared < MyApiController >(); router4HttpRequestsRouting->addController( myApiController );
OATPP_COMPONENT( std::shared_ptr < oatpp::network::ConnectionHandler >, connectionHandler );
OATPP_COMPONENT( std::shared_ptr < oatpp::network::ServerConnectionProvider >, connectionProvider );
oatpp::network::Server server( connectionProvider, connectionHandler );
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:
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):
#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"
#include OATPP_CODEGEN_BEGIN(DTO)
class BookDto
: public oatpp::DTO {
DTO_INIT( BookDto, DTO )
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 )
{ }
};
#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) class BookController
: public oatpp::web::server::api::ApiController
{
public:
BookController( OATPP_COMPONENT( std::shared_ptr < ObjectMapper >, objectMapper ) )
: oatpp::web::server::api::ApiController( objectMapper )
{ }
ENDPOINT( "POST", "/books", createBook,
BODY_DTO( Object < BookDto >, bookDto ) ) {
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 ), 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(); 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) 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 )
{
ApplicationComponents components;
OATPP_COMPONENT( std::shared_ptr < oatpp::web::server::HttpRouter >, router4HttpRequestsRouting );
auto myApiController = std::make_shared < BookController >();
router4HttpRequestsRouting->addController( myApiController );
OATPP_COMPONENT( std::shared_ptr < oatpp::network::ConnectionHandler >, connectionHandler );
OATPP_COMPONENT( std::shared_ptr < oatpp::network::ServerConnectionProvider >, connectionProvider );
oatpp::network::Server server( connectionProvider, connectionHandler );
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:
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:
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ść:
#pragma once
#include <oatpp-swagger/Model.hpp>
#include <oatpp-swagger/Resources.hpp>
#include <oatpp/core/macro/component.hpp>
class SwaggerComponent
{
public:
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();
}() );
OATPP_CREATE_COMPONENT( std::shared_ptr < oatpp::swagger::Resources >, swaggerResources )([ ]
{
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 ); }() );
};
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ć:
#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" constexpr const int listeningPort = 8000;
constexpr const char listeningAddress[ ] = "localhost";
constexpr const char routeSubaddress[ ] = "/hello";
class ApplicationComponents
{
public:
SwaggerComponent swaggerComponent; OATPP_CREATE_COMPONENT( std::shared_ptr < oatpp::network::ServerConnectionProvider >, serverConnectionProvider )([ ] {
return oatpp::network::tcp::server::ConnectionProvider::createShared( { listeningAddress, listeningPort, oatpp::network::Address::IP_4 } );
}() );
OATPP_CREATE_COMPONENT( std::shared_ptr < oatpp::web::server::HttpRouter >, httpRouter )([ ] {
return oatpp::web::server::HttpRouter::createShared();
}() );
OATPP_CREATE_COMPONENT( std::shared_ptr < oatpp::network::ConnectionHandler >, serverConnectionHandler )([ ] {
OATPP_COMPONENT( std::shared_ptr < oatpp::web::server::HttpRouter >, router ); return oatpp::web::server::HttpConnectionHandler::createShared( router );
}() );
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):
#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"
#include OATPP_CODEGEN_BEGIN(DTO)
class BookDto
: public oatpp::DTO
{
DTO_INIT( BookDto, DTO )
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 )
{ }
};
#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) class BookController
: public oatpp::web::server::api::ApiController
{
public:
static std::shared_ptr < BookController > createShared(
OATPP_COMPONENT( std::shared_ptr < ObjectMapper >, objectMapper ) ) {
return std::make_shared < BookController >( objectMapper );
}
BookController( OATPP_COMPONENT( std::shared_ptr < ObjectMapper >, objectMapper ) )
: oatpp::web::server::api::ApiController( objectMapper )
{ }
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;
}
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" );
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" );
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 )
{
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";
}
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 {
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 ) )
{
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) 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 )
{
ApplicationComponents components;
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() ); auto myApiController = std::make_shared < BookController >();
router4HttpRequestsRouting->addController( myApiController );
OATPP_COMPONENT( std::shared_ptr < oatpp::network::ConnectionHandler >, connectionHandler );
OATPP_COMPONENT( std::shared_ptr < oatpp::network::ServerConnectionProvider >, connectionProvider );
oatpp::network::Server server( connectionProvider, connectionHandler );
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.
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:
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:
#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";
#include OATPP_CODEGEN_BEGIN(DTO)
class BookDto
: public oatpp::DTO
{
DTO_INIT( BookDto, DTO )
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 )
{ }
};
#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) class BookController
: public oatpp::web::server::api::ApiController
{
public:
static std::shared_ptr < BookController > createShared( OATPP_COMPONENT( std::shared_ptr < ObjectMapper >, objectMapper ) )
{
return std::make_shared < BookController >( objectMapper );
}
BookController( OATPP_COMPONENT( std::shared_ptr < ObjectMapper >, objectMapper ) )
: oatpp::web::server::api::ApiController( objectMapper )
{
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!" );
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" );
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" );
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 )
{
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( 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 ) )
{
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) 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 )
{
ApplicationComponents components;
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() );
auto myApiController = std::make_shared < BookController >();
router4HttpRequestsRouting->addController( myApiController );
OATPP_COMPONENT( std::shared_ptr < oatpp::network::ConnectionHandler >, connectionHandler );
OATPP_COMPONENT( std::shared_ptr < oatpp::network::ServerConnectionProvider >, connectionProvider );
oatpp::network::Server server( connectionProvider, connectionHandler );
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
#include <oatpp/core/macro/codegen.hpp>
#include <oatpp-swagger/Controller.hpp>
#include OATPP_CODEGEN_BEGIN(DTO)
class BookDto
: public oatpp::DTO
{
DTO_INIT( BookDto, DTO )
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 )
{ }
};
#include OATPP_CODEGEN_END(DTO)
#include <oatpp/web/client/ApiClient.hpp>
#include OATPP_CODEGEN_BEGIN(ApiClient) 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) #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 ); std::string createBasicAuthHeader( const std::string & user, const std::string & password )
{
return "Basic " + base64_encode( user + ":" + password );
}
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 ) {
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:
#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 ApplicationComponents
{
public:
SwaggerComponent swaggerComponent;
OATPP_CREATE_COMPONENT( std::shared_ptr < oatpp::network::ServerConnectionProvider >, serverConnectionProvider )([ ] {
constexpr const char CERT_PEM_PATH[ ] = "key.pem"; constexpr const char CERT_CRT_PATH[ ] = "cert.pem"; 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 );
return oatpp::libressl::server::ConnectionProvider::createShared( config, { listeningAddress, listeningPortHttps, oatpp::network::Address::IP_4 } );
}() );
OATPP_CREATE_COMPONENT( std::shared_ptr < oatpp::web::server::HttpRouter >, httpRouter )([ ] {
return oatpp::web::server::HttpRouter::createShared();
}() );
OATPP_CREATE_COMPONENT( std::shared_ptr < oatpp::network::ConnectionHandler >, serverConnectionHandler )([ ] {
OATPP_COMPONENT( std::shared_ptr < oatpp::web::server::HttpRouter >, router ); return oatpp::web::server::HttpConnectionHandler::createShared( router );
}() );
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:
#pragma once
#include <oatpp-swagger/Model.hpp>
#include <oatpp-swagger/Resources.hpp>
#include <oatpp/core/macro/component.hpp>
class SwaggerComponent
{
public:
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();
}() );
OATPP_CREATE_COMPONENT( std::shared_ptr < oatpp::swagger::Resources >, swaggerResources )([ ]
{
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 ); }() );
};
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:
#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 {
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 } );
}() );
};
#include <oatpp/core/macro/codegen.hpp>
#include <oatpp-swagger/Controller.hpp>
#include OATPP_CODEGEN_BEGIN(DTO)
class BookDto
: public oatpp::DTO
{
DTO_INIT( BookDto, DTO )
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 )
{ }
};
#include OATPP_CODEGEN_END(DTO)
#include <oatpp/web/client/ApiClient.hpp>
#include OATPP_CODEGEN_BEGIN(ApiClient) 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) #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 ); std::string createBasicAuthHeader( const std::string & user, const std::string & password )
{
return "Basic " + base64_encode( user + ":" + password );
}
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
{
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 ) {
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:
#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 )
{
ApplicationComponents components;
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() );
OATPP_COMPONENT( std::shared_ptr < oatpp::network::ConnectionHandler >, connectionHandler );
OATPP_COMPONENT( std::shared_ptr < oatpp::network::ServerConnectionProvider >, connectionProvider );
oatpp::network::Server server( connectionProvider, connectionHandler );
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:
#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 )
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:
#pragma once
#include <oatpp/web/server/api/ApiController.hpp>
#include "dto/BookDto.hpp"
#include "service/BookService.hpp"
#include OATPP_CODEGEN_BEGIN(ApiController) class BookController
: public oatpp::web::server::api::ApiController
{
const oatpp::String user, password;
BookService m_bookService; public:
static std::shared_ptr < BookController > createShared( const oatpp::String & user, const oatpp::String & password, OATPP_COMPONENT( std::shared_ptr < ObjectMapper >, objectMapper ) )
{
return std::make_shared < BookController >( user, password, objectMapper );
}
BookController( const oatpp::String & user, const oatpp::String & password, OATPP_COMPONENT( std::shared_ptr < ObjectMapper >, objectMapper ) )
: 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; }
return createDtoResponse( Status::CODE_200, m_bookService.createBook( 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" );
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)
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).
#pragma once
#include <oatpp-sqlite/orm.hpp>
#include "dto/BookDto.hpp"
#include OATPP_CODEGEN_BEGIN(DbClient) 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 , DATABASE_MIGRATIONS "/001_init.sql" );
migration.migrate(); 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)
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
:
#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 ); 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
:
#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:
#pragma once
#include <oatpp/network/tcp/server/ConnectionProvider.hpp>
#include <oatpp/core/macro/component.hpp>
#include "db/BookDb.hpp"
class DatabaseComponent
{
public:
OATPP_CREATE_COMPONENT( std::shared_ptr < oatpp::provider::Provider < oatpp::sqlite::Connection >>, dbConnectionProvider )([ ] {
auto connectionProvider = std::make_shared < oatpp::sqlite::ConnectionProvider >( DATABASE_FILE );
return oatpp::sqlite::ConnectionPool::createShared( connectionProvider,
10 ,
std::chrono::seconds( 5 ) );
}() );
OATPP_CREATE_COMPONENT( std::shared_ptr < BookDb >, bookDb )([ ] {
OATPP_COMPONENT( std::shared_ptr < oatpp::provider::Provider < oatpp::sqlite::Connection >>, connectionProvider );
auto executor = std::make_shared < oatpp::sqlite::Executor >( connectionProvider );
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:
#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 ApplicationComponents
{
public:
SwaggerComponent swaggerComponent;
DatabaseComponent databaseComponent; 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 );
return oatpp::libressl::server::ConnectionProvider::createShared( config, { listeningAddress, listeningPortHttps, oatpp::network::Address::IP_4 } );
}() );
OATPP_CREATE_COMPONENT( std::shared_ptr < oatpp::web::server::HttpRouter >, httpRouter )([ ] {
return oatpp::web::server::HttpRouter::createShared();
}() );
OATPP_CREATE_COMPONENT( std::shared_ptr < oatpp::network::ConnectionHandler >, serverConnectionHandler )([ ] {
OATPP_COMPONENT( std::shared_ptr < oatpp::web::server::HttpRouter >, router ); return oatpp::web::server::HttpConnectionHandler::createShared( router );
}() );
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)
#pragma once
#include <oatpp-swagger/Model.hpp>
#include <oatpp-swagger/Resources.hpp>
#include <oatpp/core/macro/component.hpp>
class SwaggerComponent
{
public:
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();
}() );
OATPP_CREATE_COMPONENT( std::shared_ptr < oatpp::swagger::Resources >, swaggerResources )([ ]
{
return oatpp::swagger::Resources::loadResources( OATPP_SWAGGER_RES_PATH );
}() );
};
i
#pragma once
#include <oatpp/core/macro/component.hpp>
#include <oatpp/web/server/api/ApiController.hpp>
#include OATPP_CODEGEN_BEGIN(ApiController) 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 ) )
{
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)
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:
#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 {
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 } );
}() );
};
#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 )
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)
#include <oatpp/web/client/ApiClient.hpp>
#include OATPP_CODEGEN_BEGIN(ApiClient) 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) #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 ); std::string createBasicAuthHeader( const std::string & user, const std::string & password )
{
return "Basic " + base64_encode( user + ":" + password );
}
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
{
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 ) {
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:
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:
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