oatpp/changelog/1.3.0.md
Leonid Stryzhevskyi 97d7597112
Update 1.3.0.md
2021-11-15 04:47:16 +02:00

16 KiB

Oat++ 1.3.0

Previous release - 1.2.5

Feel free to ask questions - Chat on Gitter!

Contents:

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

#408

{

  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

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

#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.

/* 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, #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.

#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.