Signed-off-by: Ferry Huberts <ferry.huberts@pelagic.nl>
16 KiB
Oat++ 1.3.0
Previous release - 1.2.5
Feel free to ask questions - Chat on Gitter!
Contents:
- The New oatpp::String
- ConnectionPool::get() Timeout
- JSON Serializer Escape Flags
- Headers Stored In unordered_multimap
- QueryParameters Stored In unordered_multimap
- Polymorphic DTO_FIELD
- ConnectionMonitor
- Request Data Bundle
- ConnectionProviderSwitch
- Proper Server Stoppage
- TemporaryFile
- Better Multipart
- Response::getBody()
- data::stream::FIFOStream
- data::stream::BufferedInputStream
- oatpp::parser::json::mapping::Serializer::Config::alwaysIncludeRequired
The New oatpp::String
Now it's much easier to use oatpp::String
since oatpp::String
is now wrapper over std::string
{
std::string s1 = Hello;
oatpp::String s2 = s1;
}
{
oatpp::String s1 = "Hello";
std::string s2 = *s1; // *s1 returns a reference to the internal std::string object
}
{
oatpp::String s1 = "Hello";
std::string s2 = s1; // implicit cast
}
{
oatpp::String s1 = nullptr;
std::string s2 = s1; // implicit cast from null-value throws runtime_error
}
{
oatpp::String s1 = "Hello";
bool b = s1 == "Hello"; // compare s1 with const char*
assert(b);
}
{
oatpp::String s1 = "Hello";
std::stringg s2 = "Hello";
bool b = s1 == s2; // compare s1 with std::string
assert(b);
}
{
oatpp::String s1 = "Hello";
std::string s2 = "World";
oatpp::String s3 = s1 + " " + s2; // concat oatpp::String with const char* and std::string directly
OATPP_LOGD("TEST", "str='%s'", s3->c_str()) // prints 'Hello World'
}
{
oatpp::String s1 = nullptr;
oatpp::String s2 = "hello";
OATPP_ASSERT(s1.getValue("default") == "default")
OATPP_ASSERT(s2.getValue("default") == "hello")
}
ConnectionPool::get() Timeout
{
auto connectionProvider = oatpp::network::tcp::client::ConnectionProvider::createShared({"httpbin.org", 80});
auto pool = oatpp::network::ClientConnectionPool::createShared(connectionProvider,
1,
std::chrono::seconds(10),
std::chrono::seconds(5));
OATPP_LOGD("TEST", "start")
auto c1 = pool->get(); //<--- this one will succeed
OATPP_LOGD("TEST", "c1=%llu", c1.get())
auto c2 = pool->get(); //<--- this one will fail in 5 sec. Since Max-Resources is 1, Pool timeout is 5 sec. And c1 is not freed.
OATPP_LOGD("TEST", "c2=%llu", c2.get())
}
Output:
D |2021-08-04 01:32:56 1628029976986744| TEST:start
D |2021-08-04 01:32:57 1628029977126940| TEST:c1=140716915331208
D |2021-08-04 01:33:02 1628029982128324| TEST:c2=0
JSON Serializer Escape Flags
Now you can control if solidus is escaped or not.
Default Behavior
oatpp::parser::json::mapping::ObjectMapper mapper;
// mapper.getSerializer()->getConfig()->escapeFlags = 0; // by default FLAG_ESCAPE_SOLIDUS is ON
auto res = mapper.writeToString(oatpp::String("https://oatpp.io/"));
OATPP_LOGD("TEST", "res='%s'", res->c_str())
Output:
res='"https:\/\/oatpp.io\/"' # by default, solidus is escaped
Clear Escape Flags
oatpp::parser::json::mapping::ObjectMapper mapper;
mapper.getSerializer()->getConfig()->escapeFlags = 0;
auto res = mapper.writeToString(oatpp::String("https://oatpp.io/"));
OATPP_LOGD("TEST", "res='%s'", res->c_str())
Output:
res='"https://oatpp.io/"' # solidus isn't escaped
Headers Stored In unordered_multimap
Headers are now stored using std::unordered_multimap.
Put multiple headers:
auto response = createResponse(Status::CODE_200, "");
response->putHeader("Set-Cookie", "...");
response->putHeader("Set-Cookie", "...");
return response;
Log all "Set-Cookie" headers:
const auto& map = headers.getAll();
auto bucket = map.bucket("Set-Cookie");
auto bucketBegin = map.begin(bucket);
auto bucketEnd = map.end(bucket);
for(auto it = bucketBegin; it != bucketEnd; it ++) {
oatpp::String value = it->second.toString();
OATPP_LOGD("Header", "Set-Cookie: %s", value->c_str())
}
QueryParameters Stored In unordered_multimap
QueryParameters are now stored using std::unordered_multimap.
Log all entries of "userId" query parameter:
const auto& map = request->getQueryParameters().getAll();
auto bucket = map.bucket("userId");
auto bucketBegin = map.begin(bucket);
auto bucketEnd = map.end(bucket);
for(auto it = bucketBegin; it != bucketEnd; it ++) {
oatpp::String value = it->second.toString();
OATPP_LOGD("QueryParameter", "userId: %s", value->c_str())
}
Polymorphic DTO_FIELD
Now, when used inside of a DTO, we can specify exact types that oatpp::Any
can store by specifying DTO_FIELD_TYPE_SELECTOR
:
/* Possible type of a DTO_FIELD */
class ClassA : public oatpp::DTO {
DTO_INIT(ClassA, DTO)
DTO_FIELD(String, value);
};
/* Possible type of a DTO_FIELD */
class ClassB : public oatpp::DTO {
DTO_INIT(ClassB, DTO)
DTO_FIELD(Vector<String>, values);
};
/* enum of possible DTO_FIELD types */
ENUM(ClassType, v_int32,
VALUE(CLASS_TYPE_A, 0),
VALUE(CLASS_TYPE_B, 1)
)
/* our DTO */
class ResponseDto : public oatpp::DTO {
DTO_INIT(ResponseDto, DTO)
/* type control field */
DTO_FIELD(Enum<ClassType>::AsString, payloadType);
/* polymorphic field */
DTO_FIELD(Any, payload);
/* type selector */
DTO_FIELD_TYPE_SELECTOR(payload) {
if(!payloadType) return Void::Class::getType();
switch (*payloadType) {
case ClassType::CLASS_TYPE_A: return Object<ClassA>::Class::getType();
case ClassType::CLASS_TYPE_B: return Object<ClassB>::Class::getType();
}
}
};
...
/* send polymorphic payload to client */
ENDPOINT("GET", "payload", getPayload) {
auto payload = ClassB::createShared();
payload->values = {"value1", "value2", "value3"};
auto r = ResponseDto::createShared();
r->payloadType = ClassType::CLASS_TYPE_B;
r->payload = payload;
return createDtoResponse(Status::CODE_200, r);
}
/* receive polymorphic payload from client */
ENDPOINT("POST", "payload", postPayload,
BODY_DTO(oatpp::Object<ResponseDto>, r))
{
/* check type-control field and retrieve value of the corresponding type */
if(r->payloadType == ClassType::CLASS_TYPE_B) {
auto payload = r->payload.retrieve<oatpp::Object<ClassB>>();
for(auto& value : *payload->values) {
OATPP_LOGD("VALUE", "%s", value->c_str())
}
}
return createResponse(Status::CODE_200, "OK");
}
ConnectionMonitor
oatpp::network::monitor::ConnectionMonitor
is a middleman who's able to monitor provided connections and close those ones that not satisfy selected rules.
OATPP_CREATE_COMPONENT(std::shared_ptr<oatpp::network::ServerConnectionProvider>, serverConnectionProvider)([] {
auto connectionProvider = oatpp::network::tcp::server::ConnectionProvider::createShared({"0.0.0.0", 8000, oatpp::network::Address::IP_4});
auto monitor = std::make_shared<oatpp::network::monitor::ConnectionMonitor>(connectionProvider);
/* close all connections that stay opened for more than 120 seconds */
monitor->addMetricsChecker(
std::make_shared<oatpp::network::monitor::ConnectionMaxAgeChecker>(
std::chrono::seconds(120)
)
);
/* close all connections that have had no successful reads and writes for longer than 5 seconds */
monitor->addMetricsChecker(
std::make_shared<oatpp::network::monitor::ConnectionInactivityChecker>(
std::chrono::seconds(5),
std::chrono::seconds(5),
)
);
return monitor;
}());
Note: ConnectionMonitor
also works with ClientConnectionProvider
as well.
Request Data Bundle
Now there is a data bundle associated with the Request and the Response which makes it easy to pass data through middleware interceptors and endpoints.
Example:
class MyAuthInterceptor : public oatpp::web::server::interceptor::RequestInterceptor {
public:
std::shared_ptr<OutgoingResponse> intercept(const std::shared_ptr<IncomingRequest>& request) override {
/* authorize request and get auth data */
oatpp::Object<AuthDto> authData = authorize(request);
if(!authData) {
return OutgoingResponse::createShared(Status::CODE_401, nullptr);
}
/* put auth data to bundle for later use at an endpoint */
request->putBundleData("auth", authData);
return nullptr; // continue processing
}
};
...
ENDPOINT("GET", "videos/{videoId}", getVideoById,
PATH(String, videoId),
BUNDLE(oatpp::Object<AuthDto>, authData, "auth"))
{
...
}
ConnectionProviderSwitch
oatpp::network::ConnectionProviderSwitch
can be used to change connection providers on the go, ex.: when you want to reload an SSL certificate without stopping the server.
/* create server connection provider component */
/* use ConnectionProviderSwitch instead of a regular ServerConnectionProvider */
OATPP_CREATE_COMPONENT(std::shared_ptr<oatpp::network::ConnectionProviderSwitch>, serverConnectionProvider)([this] {
/* create SSL provider */
auto sslProvider = oatpp::libressl::server::ConnectionProvider::createShared(...);
/* create oatpp::network::ConnectionProviderSwitch*/
return std::make_shared<oatpp::network::ConnectionProviderSwitch>(sslProvider /* current provider */);
}());
...
void reloadCert() {
/* get server connection provider component */
OATPP_COMPONENT(std::shared_ptr<oatpp::network::ConnectionProviderSwitch>, providerSwitch);
/* create new SSL provider with new cert */
auto sslProvider = oatpp::libressl::server::ConnectionProvider::createShared(...);
/* set new provider */
providerSwitch->resetProvider(sslProvider);
}
Additionally, resource invalidation is no longer supported by ConnectionProvider. Please use either ResourceHandleTemplate::invalidate() or Invalidator::invalidate(resource) directly.
Proper Server Stoppage
Now call to HttpConnectionHandler::stop()
, AsyncHttpConnectionHandler::stop()
will shutdown all opened connections and will wait until all request handlers exit.
TemporaryFile
Introduce oatpp::data::resource::TemporaryFile
.
Use-case:
Temporary file resolves concurrency issues during file uploads. Also, a temporary file ensures that partially uploaded (due to errors/exceptions) resources will be automatically deleted at the end of the block.
#include "oatpp/core/data/resource/TemporaryFile.hpp"
...
ENDPOINT("POST", "/upload", upload,
REQUEST(std::shared_ptr<IncomingRequest>, request))
{
/* create random file in '/tmp' folder */
oatpp::data::resource::TemporaryFile tmp("/tmp");
/* transfer body to temporary file */
request->transferBody(tmp.openOutputStream());
/* move file to permanent storage */
OATPP_ASSERT_HTTP(tmp.moveFile("/path/to/permanent/storage/avatar.png"), Status::CODE_500, "Failed to save file")
/* return 200 */
return createResponse(Status::CODE_200, "OK");
}
Better Multipart
Multipart API has been changed and improved.
Now it's possible to upload multiple files using TemporaryFile
and keep track of
all parts and their corresponding data resources.
#include "oatpp/web/mime/multipart/TemporaryFileProvider.hpp"
#include "oatpp/web/mime/multipart/Reader.hpp"
#include "oatpp/web/mime/multipart/PartList.hpp"
...
namespace multipart = oatpp::web::mime::multipart;
...
ENDPOINT("POST", "upload", upload,
REQUEST(std::shared_ptr<IncomingRequest>, request))
{
/* create multipart object */
multipart::PartList multipart(request->getHeaders());
/* create multipart reader */
multipart::Reader multipartReader(&multipart);
/* setup reader to stream parts to a temporary files by default */
multipartReader.setDefaultPartReader(multipart::createTemporaryFilePartReader("/tmp" /* /tmp directory */));
/* upload multipart data */
request->transferBody(&multipartReader);
/* list all parts and locations to corresponding temporary files */
auto parts = multipart.getAllParts();
for(auto& p : parts) {
OATPP_LOGD("part", "name=%s, location=%s", p->getName()->c_str(), p->getPayload()->getLocation()->c_str())
}
/* return 200 */
return createResponse(Status::CODE_200, "OK");
}
Response::getBody()
oatpp::web::protocol::http::outgoing::Response
has a new method getBody()
to retrieve the body of the response.
This is handy for response interceptors.
data::stream::FIFOStream
The new FIFOStream
stream is a buffered
InputStream
with an
WriteCallback
.
Check the corresponding documentation on how to use these interfaces.
Instead of using a static buffer like BufferInputStream
it is build upon data::buffer::FIFOBuffer
and is able to
dynamically grow when data is written to it that would surpass its capacity.
It is especially useful if you need to buffer data from a stream upfront or have multiple data sources that should be
buffered in a single stream.
However, it is not synchronized, so be careful when using FIFOStream
in a multithreaded manner.
You need to implement your own locking.
data::stream::BufferedInputStream
FIFOStream
also introduced a new interface
BufferedInputStream
which unifies
the buffered-stream-interface all existing buffered streams (InputStreamBufferedProxy
, BufferInputStream
,
FIFOStream
) to allow for generalisation.
oatpp::parser::json::mapping::Serializer::Config::alwaysIncludeRequired
If oatpp::parser::json::mapping::Serializer::Config::includeNullFields == false
there might still be the requirement
to include some fields even if they are nullptr
, because they are required by the deserializing end.
Consider the following DTO and endpoint-snippet.
class StatusDto : public oatpp::DTO {
DTO_INIT(StatusDto, DTO)
DTO_FIELD_INFO(status) {
info->required = true;
}
DTO_FIELD(String, status);
DTO_FIELD(Int32, code);
DTO_FIELD(String, message);
};
// endpoint code:
ENDPOINT("GET", "/status", status) {
auto dto = StatusDto::createShared();
dto->code = 200;
return createDtoResponse(Status::CODE_200, dto);
}
With a serializer with its config set to Serializer::Config::includeNullFields = false
, the snippet would just yield {"code":200}
.
However, status
is a required field.
Now, one can set Serializer::Config::alwaysIncludeRequired = true
.
With alwaysIncludeRequired == true
, the same snippet would yield {"status":null,"code":200}
, even with includeNullFields == false
.