[FEATURE]: better separation of types and serialization / deserialilzation code in generated C++ output
beat-schaer opened this issue · 0 comments
I would like to use the generated C++ code in a Clean Architecture project. While the generated types itself are perfect candidates to end in the Entity Layer right in the core of the project, I want to put the serialization / deserialization code in one of the outer layers, preferably the Frameworks & Drivers Layer. However, that requires that the code is clearly separated according to these 2 different concerns.
Context (Input, Language)
Input Format: JSON Schema, draft-07
Output Language: C++
Description
The generated C++ content should be clearly separated without having any dependencies to the JSON serialization / deserialization code in the core types. Additionally it would be nice to also get an option of generating those 2 aspects of the code into separate directories (if using in multi-source
mode. However, this is mandatory and can be easily fixed with a script after generation.
For the example used in this feature request, I used the following schema:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$ref": "#/definitions/TestObject",
"definitions": {
"TestObject": {
"type": "object",
"properties": {
"some_string": {
"type": "string",
"pattern": "[0-9A-Za-z]+"
}
},
"required": [ "some_string" ]
}
}
}
Current Behaviour / Output
This is what is currently generated, using Pascal case style for types and Underscore case for members.
// helper.hpp
// To parse this JSON data, first install
//
// json.hpp https://github.com/nlohmann/json
//
// Then include this file, and then do
//
// helper.hpp data = nlohmann::json::parse(jsonString);
#pragma once
#include "json.hpp"
#include <optional>
#include <stdexcept>
#include <regex>
#include <sstream>
namespace data {
using nlohmann::json;
class ClassMemberConstraints {
private:
std::optional<int64_t> min_int_value;
std::optional<int64_t> max_int_value;
std::optional<double> min_double_value;
std::optional<double> max_double_value;
std::optional<size_t> min_length;
std::optional<size_t> max_length;
std::optional<std::string> pattern;
public:
ClassMemberConstraints(
std::optional<int64_t> min_int_value,
std::optional<int64_t> max_int_value,
std::optional<double> min_double_value,
std::optional<double> max_double_value,
std::optional<size_t> min_length,
std::optional<size_t> max_length,
std::optional<std::string> pattern
) : min_int_value(min_int_value), max_int_value(max_int_value), min_double_value(min_double_value), max_double_value(max_double_value), min_length(min_length), max_length(max_length), pattern(pattern) {}
ClassMemberConstraints() = default;
virtual ~ClassMemberConstraints() = default;
void set_min_int_value(int64_t min_int_value) { this->min_int_value = min_int_value; }
auto get_min_int_value() const { return min_int_value; }
void set_max_int_value(int64_t max_int_value) { this->max_int_value = max_int_value; }
auto get_max_int_value() const { return max_int_value; }
void set_min_double_value(double min_double_value) { this->min_double_value = min_double_value; }
auto get_min_double_value() const { return min_double_value; }
void set_max_double_value(double max_double_value) { this->max_double_value = max_double_value; }
auto get_max_double_value() const { return max_double_value; }
void set_min_length(size_t min_length) { this->min_length = min_length; }
auto get_min_length() const { return min_length; }
void set_max_length(size_t max_length) { this->max_length = max_length; }
auto get_max_length() const { return max_length; }
void set_pattern(const std::string & pattern) { this->pattern = pattern; }
auto get_pattern() const { return pattern; }
};
class ClassMemberConstraintException : public std::runtime_error {
public:
ClassMemberConstraintException(const std::string & msg) : std::runtime_error(msg) {}
};
class ValueTooLowException : public ClassMemberConstraintException {
public:
ValueTooLowException(const std::string & msg) : ClassMemberConstraintException(msg) {}
};
class ValueTooHighException : public ClassMemberConstraintException {
public:
ValueTooHighException(const std::string & msg) : ClassMemberConstraintException(msg) {}
};
class ValueTooShortException : public ClassMemberConstraintException {
public:
ValueTooShortException(const std::string & msg) : ClassMemberConstraintException(msg) {}
};
class ValueTooLongException : public ClassMemberConstraintException {
public:
ValueTooLongException(const std::string & msg) : ClassMemberConstraintException(msg) {}
};
class InvalidPatternException : public ClassMemberConstraintException {
public:
InvalidPatternException(const std::string & msg) : ClassMemberConstraintException(msg) {}
};
inline void CheckConstraint(const std::string & name, const ClassMemberConstraints & c, int64_t value) {
if (c.get_min_int_value() != std::nullopt && value < *c.get_min_int_value()) {
throw ValueTooLowException ("Value too low for " + name + " (" + std::to_string(value) + "<" + std::to_string(*c.get_min_int_value()) + ")");
}
if (c.get_max_int_value() != std::nullopt && value > *c.get_max_int_value()) {
throw ValueTooHighException ("Value too high for " + name + " (" + std::to_string(value) + ">" + std::to_string(*c.get_max_int_value()) + ")");
}
}
inline void CheckConstraint(const std::string & name, const ClassMemberConstraints & c, double value) {
if (c.get_min_double_value() != std::nullopt && value < *c.get_min_double_value()) {
throw ValueTooLowException ("Value too low for " + name + " (" + std::to_string(value) + "<" + std::to_string(*c.get_min_double_value()) + ")");
}
if (c.get_max_double_value() != std::nullopt && value > *c.get_max_double_value()) {
throw ValueTooHighException ("Value too high for " + name + " (" + std::to_string(value) + ">" + std::to_string(*c.get_max_double_value()) + ")");
}
}
inline void CheckConstraint(const std::string & name, const ClassMemberConstraints & c, const std::string & value) {
if (c.get_min_length() != std::nullopt && value.length() < *c.get_min_length()) {
throw ValueTooShortException ("Value too short for " + name + " (" + std::to_string(value.length()) + "<" + std::to_string(*c.get_min_length()) + ")");
}
if (c.get_max_length() != std::nullopt && value.length() > *c.get_max_length()) {
throw ValueTooLongException ("Value too long for " + name + " (" + std::to_string(value.length()) + ">" + std::to_string(*c.get_max_length()) + ")");
}
if (c.get_pattern() != std::nullopt) {
std::smatch result;
std::regex_search(value, result, std::regex( *c.get_pattern() ));
if (result.empty()) {
throw InvalidPatternException ("Value doesn't match pattern for " + name + " (" + value +" != " + *c.get_pattern() + ")");
}
}
}
#ifndef NLOHMANN_UNTYPED_data_HELPER
#define NLOHMANN_UNTYPED_data_HELPER
inline json get_untyped(const json & j, const char * property) {
if (j.find(property) != j.end()) {
return j.at(property).get<json>();
}
return json();
}
inline json get_untyped(const json & j, std::string property) {
return get_untyped(j, property.data());
}
#endif
}
// Generators.hpp
// To parse this JSON data, first install
//
// json.hpp https://github.com/nlohmann/json
//
// Then include this file, and then do
//
// Generators.hpp data = nlohmann::json::parse(jsonString);
#pragma once
#include "json.hpp"
#include "helper.hpp"
#include "TestObject.hpp"
namespace data {
void from_json(const json & j, TestObject & x);
void to_json(json & j, const TestObject & x);
inline void from_json(const json & j, TestObject& x) {
x.set_some_string(j.at("some_string").get<std::string>());
}
inline void to_json(json & j, const TestObject & x) {
j = json::object();
j["some_string"] = x.get_some_string();
}
}
// TestObject.hpp
// To parse this JSON data, first install
//
// json.hpp https://github.com/nlohmann/json
//
// Then include this file, and then do
//
// TestObject.hpp data = nlohmann::json::parse(jsonString);
#pragma once
#include "json.hpp"
#include "helper.hpp"
namespace data {
using nlohmann::json;
class TestObject {
public:
TestObject() :
some_string_constraint(std::nullopt, std::nullopt, std::nullopt, std::nullopt, std::nullopt, std::nullopt, std::string("[0-9A-Za-z]+"))
{}
virtual ~TestObject() = default;
private:
std::string some_string;
ClassMemberConstraints some_string_constraint;
public:
const std::string & get_some_string() const { return some_string; }
std::string & get_mutable_some_string() { return some_string; }
void set_some_string(const std::string & value) { CheckConstraint("some_string", some_string_constraint, value); this->some_string = value; }
};
}
// stdout
// To parse this JSON data, first install
//
// json.hpp https://github.com/nlohmann/json
//
// Then include this file, and then do
//
// stdout data = nlohmann::json::parse(jsonString);
#pragma once
#include "json.hpp"
#include "helper.hpp"
#include "TestObject.hpp"
namespace data {
}
Proposed Behaviour / Output
This is my proposed new output. The following aspects should be changed from the current output:
- The class
ClassMemberConstraints
and all its corresponding exceptions have been put into its own fileClassMemberConstraints.hpp
. The class has no relation to JSON code. - All includes / references to the JSON parser / serializer have been completely removed from
TestObject.hpp
allowing to use this class as a type in the Entity Layer.
// helper.hpp
// To parse this JSON data, first install
//
// json.hpp https://github.com/nlohmann/json
//
// Then include this file, and then do
//
// helper.hpp data = nlohmann::json::parse(jsonString);
#pragma once
#include "json.hpp"
namespace data {
#ifndef NLOHMANN_UNTYPED_data_HELPER
#define NLOHMANN_UNTYPED_data_HELPER
inline json get_untyped(const json & j, const char * property) {
if (j.find(property) != j.end()) {
return j.at(property).get<json>();
}
return json();
}
inline json get_untyped(const json & j, std::string property) {
return get_untyped(j, property.data());
}
#endif
}
// ClassMemberContraints.hpp
#pragma once
#include <optional>
#include <stdexcept>
#include <regex>
#include <sstream>
namespace data {
class ClassMemberConstraints {
private:
std::optional<int64_t> min_int_value;
std::optional<int64_t> max_int_value;
std::optional<double> min_double_value;
std::optional<double> max_double_value;
std::optional<size_t> min_length;
std::optional<size_t> max_length;
std::optional<std::string> pattern;
public:
ClassMemberConstraints(
std::optional<int64_t> min_int_value,
std::optional<int64_t> max_int_value,
std::optional<double> min_double_value,
std::optional<double> max_double_value,
std::optional<size_t> min_length,
std::optional<size_t> max_length,
std::optional<std::string> pattern
) : min_int_value(min_int_value), max_int_value(max_int_value), min_double_value(min_double_value), max_double_value(max_double_value), min_length(min_length), max_length(max_length), pattern(pattern) {}
ClassMemberConstraints() = default;
virtual ~ClassMemberConstraints() = default;
void set_min_int_value(int64_t min_int_value) { this->min_int_value = min_int_value; }
auto get_min_int_value() const { return min_int_value; }
void set_max_int_value(int64_t max_int_value) { this->max_int_value = max_int_value; }
auto get_max_int_value() const { return max_int_value; }
void set_min_double_value(double min_double_value) { this->min_double_value = min_double_value; }
auto get_min_double_value() const { return min_double_value; }
void set_max_double_value(double max_double_value) { this->max_double_value = max_double_value; }
auto get_max_double_value() const { return max_double_value; }
void set_min_length(size_t min_length) { this->min_length = min_length; }
auto get_min_length() const { return min_length; }
void set_max_length(size_t max_length) { this->max_length = max_length; }
auto get_max_length() const { return max_length; }
void set_pattern(const std::string & pattern) { this->pattern = pattern; }
auto get_pattern() const { return pattern; }
};
class ClassMemberConstraintException : public std::runtime_error {
public:
ClassMemberConstraintException(const std::string & msg) : std::runtime_error(msg) {}
};
class ValueTooLowException : public ClassMemberConstraintException {
public:
ValueTooLowException(const std::string & msg) : ClassMemberConstraintException(msg) {}
};
class ValueTooHighException : public ClassMemberConstraintException {
public:
ValueTooHighException(const std::string & msg) : ClassMemberConstraintException(msg) {}
};
class ValueTooShortException : public ClassMemberConstraintException {
public:
ValueTooShortException(const std::string & msg) : ClassMemberConstraintException(msg) {}
};
class ValueTooLongException : public ClassMemberConstraintException {
public:
ValueTooLongException(const std::string & msg) : ClassMemberConstraintException(msg) {}
};
class InvalidPatternException : public ClassMemberConstraintException {
public:
InvalidPatternException(const std::string & msg) : ClassMemberConstraintException(msg) {}
};
inline void CheckConstraint(const std::string & name, const ClassMemberConstraints & c, int64_t value) {
if (c.get_min_int_value() != std::nullopt && value < *c.get_min_int_value()) {
throw ValueTooLowException ("Value too low for " + name + " (" + std::to_string(value) + "<" + std::to_string(*c.get_min_int_value()) + ")");
}
if (c.get_max_int_value() != std::nullopt && value > *c.get_max_int_value()) {
throw ValueTooHighException ("Value too high for " + name + " (" + std::to_string(value) + ">" + std::to_string(*c.get_max_int_value()) + ")");
}
}
inline void CheckConstraint(const std::string & name, const ClassMemberConstraints & c, double value) {
if (c.get_min_double_value() != std::nullopt && value < *c.get_min_double_value()) {
throw ValueTooLowException ("Value too low for " + name + " (" + std::to_string(value) + "<" + std::to_string(*c.get_min_double_value()) + ")");
}
if (c.get_max_double_value() != std::nullopt && value > *c.get_max_double_value()) {
throw ValueTooHighException ("Value too high for " + name + " (" + std::to_string(value) + ">" + std::to_string(*c.get_max_double_value()) + ")");
}
}
inline void CheckConstraint(const std::string & name, const ClassMemberConstraints & c, const std::string & value) {
if (c.get_min_length() != std::nullopt && value.length() < *c.get_min_length()) {
throw ValueTooShortException ("Value too short for " + name + " (" + std::to_string(value.length()) + "<" + std::to_string(*c.get_min_length()) + ")");
}
if (c.get_max_length() != std::nullopt && value.length() > *c.get_max_length()) {
throw ValueTooLongException ("Value too long for " + name + " (" + std::to_string(value.length()) + ">" + std::to_string(*c.get_max_length()) + ")");
}
if (c.get_pattern() != std::nullopt) {
std::smatch result;
std::regex_search(value, result, std::regex( *c.get_pattern() ));
if (result.empty()) {
throw InvalidPatternException ("Value doesn't match pattern for " + name + " (" + value +" != " + *c.get_pattern() + ")");
}
}
}
}
// Generators.hpp
// To parse this JSON data, first install
//
// json.hpp https://github.com/nlohmann/json
//
// Then include this file, and then do
//
// Generators.hpp data = nlohmann::json::parse(jsonString);
#pragma once
#include "json.hpp"
#include "helper.hpp"
#include "TestObject.hpp"
namespace data {
void from_json(const json & j, TestObject & x);
void to_json(json & j, const TestObject & x);
inline void from_json(const json & j, TestObject& x) {
x.set_some_string(j.at("some_string").get<std::string>());
}
inline void to_json(json & j, const TestObject & x) {
j = json::object();
j["some_string"] = x.get_some_string();
}
}
// TestObject.hpp
#pragma once
#include "ClassMemberConstraints.hpp"
namespace data {
class TestObject {
public:
TestObject() :
some_string_constraint(std::nullopt, std::nullopt, std::nullopt, std::nullopt, std::nullopt, std::nullopt, std::string("[0-9A-Za-z]+"))
{}
virtual ~TestObject() = default;
private:
std::string some_string;
ClassMemberConstraints some_string_constraint;
public:
const std::string & get_some_string() const { return some_string; }
std::string & get_mutable_some_string() { return some_string; }
void set_some_string(const std::string & value) { CheckConstraint("some_string", some_string_constraint, value); this->some_string = value; }
};
}
// stdout
// To parse this JSON data, first install
//
// json.hpp https://github.com/nlohmann/json
//
// Then include this file, and then do
//
// stdout data = nlohmann::json::parse(jsonString);
#pragma once
#include "json.hpp"
#include "helper.hpp"
#include "Generators.hpp"
#include "TestObject.hpp"
namespace data {
}
Solution
Either change the existing C++ generator or add a new option which allows this proposed separation.
Optionally add an option to already allow different directories, where the files with and without JSON-relation can be generated to.
Alternatives
The alternative is to highly modify the generated code after the quicktype generation step. However, this is cumbersome and error-prone.