mirror of
https://gitee.com/zyjblog/oatpp.git
synced 2024-12-22 22:16:37 +08:00
aa02f4bb3f
Signed-off-by: Ferry Huberts <ferry.huberts@pelagic.nl>
527 lines
16 KiB
Markdown
527 lines
16 KiB
Markdown
# Oat++ 1.3.0
|
|
|
|
Previous release - [1.2.5](1.2.5.md)
|
|
|
|
Feel free to ask questions - [Chat on Gitter!](https://gitter.im/oatpp-framework/Lobby)
|
|
|
|
Contents:
|
|
|
|
- [The New oatpp::String](#the-new-oatppstring)
|
|
- [ConnectionPool::get() Timeout](#connectionpoolget-timeout)
|
|
- [JSON Serializer Escape Flags](#json-serializer-escape-flags)
|
|
- [Headers Stored In unordered_multimap](#headers-stored-in-unordered_multimap)
|
|
- [QueryParameters Stored In unordered_multimap](#queryparameters-stored-in-unordered_multimap)
|
|
- [Polymorphic DTO_FIELD](#polymorphic-dto_field)
|
|
- [ConnectionMonitor](#connectionmonitor)
|
|
- [Request Data Bundle](#request-data-bundle)
|
|
- [ConnectionProviderSwitch](#connectionproviderswitch)
|
|
- [Proper Server Stoppage](#proper-server-stoppage)
|
|
- [TemporaryFile](#temporaryfile)
|
|
- [Better Multipart](#better-multipart)
|
|
- [Response::getBody()](#responsegetbody)
|
|
- [data::stream::FIFOStream](#datastreamfifostream)
|
|
- [data::stream::BufferedInputStream](#datastreambufferedinputstream)
|
|
- [oatpp::parser::json::mapping::Serializer::Config::alwaysIncludeRequired](#oatppparserjsonmappingserializerconfigalwaysincluderequired)
|
|
|
|
|
|
## The New oatpp::String
|
|
|
|
Now it's much easier to use `oatpp::String` since `oatpp::String` is now wrapper over `std::string`
|
|
|
|
```cpp
|
|
{
|
|
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
|
|
|
|
[#408](https://github.com/oatpp/oatpp/issues/408)
|
|
|
|
```cpp
|
|
{
|
|
|
|
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
|
|
|
|
[#381](https://github.com/oatpp/oatpp/issues/381)
|
|
|
|
Now you can control if solidus is escaped or not.
|
|
|
|
### Default Behavior
|
|
|
|
```cpp
|
|
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
|
|
|
|
```cpp
|
|
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](https://en.cppreference.com/w/cpp/container/unordered_multimap).
|
|
|
|
Put multiple headers:
|
|
|
|
```cpp
|
|
auto response = createResponse(Status::CODE_200, "");
|
|
response->putHeader("Set-Cookie", "...");
|
|
response->putHeader("Set-Cookie", "...");
|
|
return response;
|
|
```
|
|
|
|
Log all "Set-Cookie" headers:
|
|
|
|
```cpp
|
|
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](https://en.cppreference.com/w/cpp/container/unordered_multimap).
|
|
|
|
Log all entries of "userId" query parameter:
|
|
|
|
```cpp
|
|
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`:
|
|
|
|
```cpp
|
|
/* 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.
|
|
|
|
```cpp
|
|
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:
|
|
|
|
```cpp
|
|
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
|
|
|
|
[#483](https://github.com/oatpp/oatpp/issues/483)
|
|
|
|
`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.
|
|
|
|
```cpp
|
|
/* 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
|
|
|
|
Fix to [#476](https://github.com/oatpp/oatpp/issues/476), [#269](https://github.com/oatpp/oatpp/issues/269)
|
|
|
|
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.
|
|
|
|
```cpp
|
|
#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.
|
|
|
|
```cpp
|
|
#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`](https://oatpp.io/api/latest/oatpp/core/data/stream/Stream/#inputstream) with an
|
|
[`WriteCallback`](https://oatpp.io/api/latest/oatpp/core/data/stream/Stream/#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`](https://oatpp.io/api/latest/oatpp/core/data/stream/Stream/#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.
|
|
```c++
|
|
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`.
|