[C++][Pistache] Generate API generalization interface (#15279)

* [C++][Pistache] Refactor setupSupportingFiles

Supporting files are set up in the CppPistacheServerCodegen()
constructor as well as in processOpts(). Refactor the code and extract a
method setupSupportingFiles().

* [C++][Pistache] Refactor: Simplify isQueryParam condition

Both branches of the if/else do the same steps. Refactor this out and
invert logic.

* [C++][Pistache] Refactor: Add injectImplInFilename

Both branches of the if/else if do the similar steps and are dependent
on the suffix. Make this obvious by introducing a new method
injectImplInFilename(String result, String suffix).

* [C++][Pistache] Refactor: injectImplInFilename: remove index search

We do not need the separatorChar index to inject the "Impl" string.
Simply truncate the whole string.

Also rename the parameter from 'result' to' filename'.

* CppPistacheServer: Refactor postProcessOperationsWithModels

Pull out the post-processing for a single operation, and also pull out
post-processing for parameters.

Introduce boolean expressions for supported parsing per parameter, and
consumption of JSON.

Reorder code to make locality more explicit i.e. how consumeJSON and
isParsingSupported is generated and used.

* CppPistacheServer: Refactor to use functional matching

Functional matching like anyMatch() directly state what boolean value is
 searched.

 However, the Predicates deserve to heave names themselves.

* CppPistacheServer: Add base class for Api

Looking at the generated main-api-server.cpp code it gets obvious that
the API classes are self similar with a similar interface.
Only the construction and teh initialization is called in the main()
function. Leverage this fact to create a generalization ApiBase.

Introduce ApiBase as a pure virtual base class to the concrete API
classes and declare init() as virtual.

Pull the route member into the base class.

With this change we could have a container hold all the ApiImpl
objects later and call init() on all of them using a for_each loop.

* CppPistacheServer: Use ApiBase for ApiImpl storage

Refactor the main-api-server template to use a vector for ApiImpl
storage instead of separate objects. This leverages the previously
added ApiBase generalization.

 We push all concrete ApiImpl objects into a vector and call init() on
  each of them.

* [C++][Pistache]: Update generated sample

Due to teh addition of ApiBase class update the generated sample.

* [C++][Pistache] Add comment for postProcessSingleParam

* [C++][Pistache] Rename and comment implFilenameFromApiFilename

While writing the comment, I realized that the method name could be more
precise. Thus rename injectImplInFilename to implFilenameFromApiFilename
and add comment.
This commit is contained in:
CTerasa-ep
2023-04-26 14:01:58 +02:00
committed by GitHub
parent b8ccd25a79
commit b847140834
14 changed files with 190 additions and 95 deletions
@@ -34,6 +34,7 @@ import org.slf4j.LoggerFactory;
import java.io.File;
import java.util.*;
import java.util.function.Predicate;
import static org.openapitools.codegen.utils.StringUtils.underscore;
@@ -113,11 +114,7 @@ public class CppPistacheServerCodegen extends AbstractCppCodegen {
VARIABLE_NAME_FIRST_CHARACTER_UPPERCASE_DESC,
Boolean.toString(this.variableNameFirstCharacterUppercase));
supportingFiles.add(new SupportingFile("helpers-header.mustache", "model", modelNamePrefix + "Helpers.h"));
supportingFiles.add(new SupportingFile("helpers-source.mustache", "model", modelNamePrefix + "Helpers.cpp"));
supportingFiles.add(new SupportingFile("main-api-server.mustache", "", modelNamePrefix + "main-api-server.cpp"));
supportingFiles.add(new SupportingFile("cmake.mustache", "", "CMakeLists.txt"));
supportingFiles.add(new SupportingFile("README.mustache", "", "README.md"));
setupSupportingFiles();
languageSpecificPrimitives = new HashSet<>(
Arrays.asList("int", "char", "bool", "long", "float", "double", "int32_t", "int64_t"));
@@ -149,6 +146,16 @@ public class CppPistacheServerCodegen extends AbstractCppCodegen {
importMapping.put("nlohmann::json", "#include <nlohmann/json.hpp>");
}
private void setupSupportingFiles() {
supportingFiles.clear();
supportingFiles.add(new SupportingFile("api-base-header.mustache", "api", "ApiBase.h"));
supportingFiles.add(new SupportingFile("helpers-header.mustache", "model", modelNamePrefix + "Helpers.h"));
supportingFiles.add(new SupportingFile("helpers-source.mustache", "model", modelNamePrefix + "Helpers.cpp"));
supportingFiles.add(new SupportingFile("main-api-server.mustache", "", modelNamePrefix + "main-api-server.cpp"));
supportingFiles.add(new SupportingFile("cmake.mustache", "", "CMakeLists.txt"));
supportingFiles.add(new SupportingFile("README.mustache", "", "README.md"));
}
@Override
public void processOpts() {
super.processOpts();
@@ -157,12 +164,7 @@ public class CppPistacheServerCodegen extends AbstractCppCodegen {
}
if (additionalProperties.containsKey("modelNamePrefix")) {
additionalProperties().put("prefix", modelNamePrefix);
supportingFiles.clear();
supportingFiles.add(new SupportingFile("helpers-header.mustache", "model", modelNamePrefix + "Helpers.h"));
supportingFiles.add(new SupportingFile("helpers-source.mustache", "model", modelNamePrefix + "Helpers.cpp"));
supportingFiles.add(new SupportingFile("main-api-server.mustache", "", modelNamePrefix + "main-api-server.cpp"));
supportingFiles.add(new SupportingFile("cmake.mustache", "", "CMakeLists.txt"));
supportingFiles.add(new SupportingFile("README.mustache", "", "README.md"));
setupSupportingFiles();
}
if (additionalProperties.containsKey(RESERVED_WORD_PREFIX_OPTION)) {
reservedWordPrefix = (String) additionalProperties.get(RESERVED_WORD_PREFIX_OPTION);
@@ -267,63 +269,69 @@ public class CppPistacheServerCodegen extends AbstractCppCodegen {
operations.put("classnameSnakeLowerCase", underscore(classname).toLowerCase(Locale.ROOT));
List<CodegenOperation> operationList = operations.getOperation();
for (CodegenOperation op : operationList) {
boolean consumeJson = false;
boolean isParsingSupported = true;
if (op.bodyParam != null) {
if (op.bodyParam.vendorExtensions == null) {
op.bodyParam.vendorExtensions = new HashMap<>();
}
boolean isStringOrDate = op.bodyParam.isString || op.bodyParam.isDate;
op.bodyParam.vendorExtensions.put("x-codegen-pistache-is-string-or-date", isStringOrDate);
}
if (op.consumes != null) {
for (Map<String, String> consume : op.consumes) {
if (consume.get("mediaType") != null && consume.get("mediaType").equals("application/json")) {
consumeJson = true;
}
}
}
op.httpMethod = op.httpMethod.substring(0, 1).toUpperCase(Locale.ROOT) + op.httpMethod.substring(1).toLowerCase(Locale.ROOT);
for (CodegenParameter param : op.allParams) {
if (param.isFormParam) isParsingSupported = false;
if (param.isFile) isParsingSupported = false;
if (param.isCookieParam) isParsingSupported = false;
//TODO: This changes the info about the real type but it is needed to parse the header params
if (param.isHeaderParam) {
param.dataType = "std::optional<Pistache::Http::Header::Raw>";
param.baseType = "std::optional<Pistache::Http::Header::Raw>";
} else if (param.isQueryParam) {
if (param.isPrimitiveType) {
param.dataType = "std::optional<" + param.dataType + ">";
} else {
param.dataType = "std::optional<" + param.dataType + ">";
param.baseType = "std::optional<" + param.baseType + ">";
}
}
}
if (op.vendorExtensions == null) {
op.vendorExtensions = new HashMap<>();
}
op.vendorExtensions.put("x-codegen-pistache-consumes-json", consumeJson);
op.vendorExtensions.put("x-codegen-pistache-is-parsing-supported", isParsingSupported);
// Check if any one of the operations needs a model, then at API file level, at least one model has to be included.
for (String hdr : op.imports) {
if (importMapping.containsKey(hdr)) {
continue;
}
operations.put("hasModelImport", true);
}
postProcessSingleOperation(operations, op);
}
return objs;
}
private void postProcessSingleOperation(OperationMap operations, CodegenOperation op) {
if (op.vendorExtensions == null) {
op.vendorExtensions = new HashMap<>();
}
if (op.bodyParam != null) {
if (op.bodyParam.vendorExtensions == null) {
op.bodyParam.vendorExtensions = new HashMap<>();
}
boolean isStringOrDate = op.bodyParam.isString || op.bodyParam.isDate;
op.bodyParam.vendorExtensions.put("x-codegen-pistache-is-string-or-date", isStringOrDate);
}
boolean consumeJson = false;
if (op.consumes != null) {
Predicate<Map<String,String>> isMediaTypeJson = consume -> (consume.get("mediaType") != null && consume.get("mediaType").equals("application/json"));
consumeJson = op.consumes.stream().anyMatch(isMediaTypeJson);
}
op.vendorExtensions.put("x-codegen-pistache-consumes-json", consumeJson);
op.httpMethod = op.httpMethod.substring(0, 1).toUpperCase(Locale.ROOT) + op.httpMethod.substring(1).toLowerCase(Locale.ROOT);
boolean isParsingSupported = true;
for (CodegenParameter param : op.allParams) {
boolean paramSupportsParsing = (!param.isFormParam && !param.isFile && !param.isCookieParam);
isParsingSupported = isParsingSupported && paramSupportsParsing;
postProcessSingleParam(param);
}
op.vendorExtensions.put("x-codegen-pistache-is-parsing-supported", isParsingSupported);
// Check if any one of the operations needs a model, then at API file level, at least one model has to be included.
Predicate<String> importNotInImportMapping = hdr -> !importMapping.containsKey(hdr);
if (op.imports.stream().anyMatch(importNotInImportMapping)) {
operations.put("hasModelImport", true);
}
}
/**
* postProcessSingleParam - Modifies a single parameter, adjusting generated
* data types for Header and Query parameters.
* @param param CodegenParameter to be modified.
*/
private static void postProcessSingleParam(CodegenParameter param) {
//TODO: This changes the info about the real type but it is needed to parse the header params
if (param.isHeaderParam) {
param.dataType = "std::optional<Pistache::Http::Header::Raw>";
param.baseType = "std::optional<Pistache::Http::Header::Raw>";
} else if (param.isQueryParam) {
param.dataType = "std::optional<" + param.dataType + ">";
if (!param.isPrimitiveType) {
param.baseType = "std::optional<" + param.baseType + ">";
}
}
}
@Override
public String toModelFilename(String name) {
return toModelName(name);
@@ -334,17 +342,27 @@ public class CppPistacheServerCodegen extends AbstractCppCodegen {
String result = super.apiFilename(templateName, tag);
if (templateName.endsWith("impl-header.mustache")) {
int ix = result.lastIndexOf(File.separatorChar);
result = result.substring(0, ix) + result.substring(ix, result.length() - 2) + "Impl.h";
result = result.replace(apiFileFolder(), implFileFolder());
result = implFilenameFromApiFilename(result, ".h");
} else if (templateName.endsWith("impl-source.mustache")) {
int ix = result.lastIndexOf(File.separatorChar);
result = result.substring(0, ix) + result.substring(ix, result.length() - 4) + "Impl.cpp";
result = result.replace(apiFileFolder(), implFileFolder());
result = implFilenameFromApiFilename(result, ".cpp");
}
return result;
}
/**
* implFilenameFromApiFilename - Inserts the string "Impl" in front of the
* suffix and replace "api" with "impl" directory prefix.
*
* @param filename Filename of the api-file to be modified
* @param suffix Suffix of the file (usually ".cpp" or ".h")
* @return a filename string of impl file.
*/
private String implFilenameFromApiFilename(String filename, String suffix) {
String result = filename.substring(0, filename.length() - suffix.length()) + "Impl" + suffix;
result = result.replace(apiFileFolder(), implFileFolder());
return result;
}
@Override
public String toApiFilename(String name) {
return toApiName(name);
@@ -0,0 +1,29 @@
{{>licenseInfo}}
/*
* ApiBase.h
*
* Generalization of the Api classes
*/
#ifndef ApiBase_H_
#define ApiBase_H_
#include <pistache/router.h>
#include <memory>
namespace {{apiNamespace}}
{
class ApiBase {
public:
explicit ApiBase(const std::shared_ptr<Pistache::Rest::Router>& rtr) : router(rtr) {};
virtual ~ApiBase() = default;
virtual void init() = 0;
protected:
const std::shared_ptr<Pistache::Rest::Router> router;
};
} // namespace {{apiNamespace}}
#endif /* ApiBase_H_ */
@@ -9,6 +9,8 @@
#define {{classname}}_H_
{{{defaultInclude}}}
#include "ApiBase.h"
#include <pistache/http.h>
#include <pistache/router.h>
#include <pistache/http_headers.h>
@@ -22,7 +24,7 @@
namespace {{apiNamespace}}
{
class {{declspec}} {{classname}} {
class {{declspec}} {{classname}} : public ApiBase {
public:
explicit {{classname}}(const std::shared_ptr<Pistache::Rest::Router>& rtr);
virtual ~{{classname}}() = default;
@@ -38,8 +40,6 @@ private:
{{/operation}}
void {{classnameSnakeLowerCase}}_default_handler(const Pistache::Rest::Request &request, Pistache::Http::ResponseWriter response);
const std::shared_ptr<Pistache::Rest::Router> router;
/// <summary>
/// Helper function to handle unexpected Exceptions during Parameter parsing and validation.
/// May be overridden to return custom error formats. This is called inside a catch block.
@@ -14,7 +14,7 @@ using namespace {{modelNamespace}};{{/hasModelImport}}
const std::string {{classname}}::base = "{{basePathWithoutHost}}";
{{classname}}::{{classname}}(const std::shared_ptr<Pistache::Rest::Router>& rtr)
: router(rtr)
: ApiBase(rtr)
{
}
@@ -9,6 +9,8 @@
#include <signal.h>
#include <unistd.h>
#endif
#include "ApiBase.h"
{{#apiInfo}}{{#apis}}{{#operations}}
#include "{{classname}}Impl.h"{{/operations}}{{/apis}}{{/apiInfo}}
@@ -66,13 +68,16 @@ int main() {
opts.maxResponseSize(PISTACHE_SERVER_MAX_RESPONSE_SIZE);
httpEndpoint->init(opts);
auto apiImpls = std::vector<std::shared_ptr<ApiBase>>();
{{#apiInfo}}{{#apis}}{{#operations}}
{{classname}}Impl {{classname}}server(router);
{{classname}}server.init();{{/operations}}{{/apis}}{{/apiInfo}}
apiImpls.push_back(std::make_shared<{{classname}}Impl>(router));{{/operations}}{{/apis}}{{/apiInfo}}
for (auto api : apiImpls) {
api->init();
}
httpEndpoint->setHandler(router->handler());
httpEndpoint->serve();
httpEndpoint->shutdown();
}
@@ -1,5 +1,6 @@
CMakeLists.txt
README.md
api/ApiBase.h
api/PetApi.cpp
api/PetApi.h
api/StoreApi.cpp
@@ -0,0 +1,39 @@
/**
* OpenAPI Petstore
* This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
*
* The version of the OpenAPI document: 1.0.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
/*
* ApiBase.h
*
* Generalization of the Api classes
*/
#ifndef ApiBase_H_
#define ApiBase_H_
#include <pistache/router.h>
#include <memory>
namespace org::openapitools::server::api
{
class ApiBase {
public:
explicit ApiBase(const std::shared_ptr<Pistache::Rest::Router>& rtr) : router(rtr) {};
virtual ~ApiBase() = default;
virtual void init() = 0;
protected:
const std::shared_ptr<Pistache::Rest::Router> router;
};
} // namespace org::openapitools::server::api
#endif /* ApiBase_H_ */
@@ -22,7 +22,7 @@ using namespace org::openapitools::server::model;
const std::string PetApi::base = "/v2";
PetApi::PetApi(const std::shared_ptr<Pistache::Rest::Router>& rtr)
: router(rtr)
: ApiBase(rtr)
{
}
@@ -19,6 +19,8 @@
#define PetApi_H_
#include "ApiBase.h"
#include <pistache/http.h>
#include <pistache/router.h>
#include <pistache/http_headers.h>
@@ -33,7 +35,7 @@
namespace org::openapitools::server::api
{
class PetApi {
class PetApi : public ApiBase {
public:
explicit PetApi(const std::shared_ptr<Pistache::Rest::Router>& rtr);
virtual ~PetApi() = default;
@@ -54,8 +56,6 @@ private:
void upload_file_handler(const Pistache::Rest::Request &request, Pistache::Http::ResponseWriter response);
void pet_api_default_handler(const Pistache::Rest::Request &request, Pistache::Http::ResponseWriter response);
const std::shared_ptr<Pistache::Rest::Router> router;
/// <summary>
/// Helper function to handle unexpected Exceptions during Parameter parsing and validation.
/// May be overridden to return custom error formats. This is called inside a catch block.
@@ -22,7 +22,7 @@ using namespace org::openapitools::server::model;
const std::string StoreApi::base = "/v2";
StoreApi::StoreApi(const std::shared_ptr<Pistache::Rest::Router>& rtr)
: router(rtr)
: ApiBase(rtr)
{
}
@@ -19,6 +19,8 @@
#define StoreApi_H_
#include "ApiBase.h"
#include <pistache/http.h>
#include <pistache/router.h>
#include <pistache/http_headers.h>
@@ -33,7 +35,7 @@
namespace org::openapitools::server::api
{
class StoreApi {
class StoreApi : public ApiBase {
public:
explicit StoreApi(const std::shared_ptr<Pistache::Rest::Router>& rtr);
virtual ~StoreApi() = default;
@@ -50,8 +52,6 @@ private:
void place_order_handler(const Pistache::Rest::Request &request, Pistache::Http::ResponseWriter response);
void store_api_default_handler(const Pistache::Rest::Request &request, Pistache::Http::ResponseWriter response);
const std::shared_ptr<Pistache::Rest::Router> router;
/// <summary>
/// Helper function to handle unexpected Exceptions during Parameter parsing and validation.
/// May be overridden to return custom error formats. This is called inside a catch block.
@@ -22,7 +22,7 @@ using namespace org::openapitools::server::model;
const std::string UserApi::base = "/v2";
UserApi::UserApi(const std::shared_ptr<Pistache::Rest::Router>& rtr)
: router(rtr)
: ApiBase(rtr)
{
}
@@ -19,6 +19,8 @@
#define UserApi_H_
#include "ApiBase.h"
#include <pistache/http.h>
#include <pistache/router.h>
#include <pistache/http_headers.h>
@@ -33,7 +35,7 @@
namespace org::openapitools::server::api
{
class UserApi {
class UserApi : public ApiBase {
public:
explicit UserApi(const std::shared_ptr<Pistache::Rest::Router>& rtr);
virtual ~UserApi() = default;
@@ -54,8 +56,6 @@ private:
void update_user_handler(const Pistache::Rest::Request &request, Pistache::Http::ResponseWriter response);
void user_api_default_handler(const Pistache::Rest::Request &request, Pistache::Http::ResponseWriter response);
const std::shared_ptr<Pistache::Rest::Router> router;
/// <summary>
/// Helper function to handle unexpected Exceptions during Parameter parsing and validation.
/// May be overridden to return custom error formats. This is called inside a catch block.
@@ -20,6 +20,8 @@
#include <unistd.h>
#endif
#include "ApiBase.h"
#include "PetApiImpl.h"
#include "StoreApiImpl.h"
#include "UserApiImpl.h"
@@ -78,17 +80,18 @@ int main() {
opts.maxResponseSize(PISTACHE_SERVER_MAX_RESPONSE_SIZE);
httpEndpoint->init(opts);
auto apiImpls = std::vector<std::shared_ptr<ApiBase>>();
PetApiImpl PetApiserver(router);
PetApiserver.init();
StoreApiImpl StoreApiserver(router);
StoreApiserver.init();
UserApiImpl UserApiserver(router);
UserApiserver.init();
apiImpls.push_back(std::make_shared<PetApiImpl>(router));
apiImpls.push_back(std::make_shared<StoreApiImpl>(router));
apiImpls.push_back(std::make_shared<UserApiImpl>(router));
for (auto api : apiImpls) {
api->init();
}
httpEndpoint->setHandler(router->handler());
httpEndpoint->serve();
httpEndpoint->shutdown();
}