Today I am having a style question. Assume I have got a JSON serializer
class with the following interface:
class JSONBuilder {
public:
JSONBuilder& AddString(stringview key, stringview value);
JSONBuilder& AddString(stringview value);
JSONBuilder& AddNumber(stringview key, int value);
JSONBuilder& BeginObject(stringview key);
JSONBuilder& EndObject(stringview key);
JSONBuilder& BeginArray(stringview key);
JSONBuilder& EndArray(stringview key);
template<class BUILDER>
JSONBuilder& AddObject(stringview key, BUILDER objectBuilder) {
BeginObject(key);
objectBuilder(*this);
EndObject(key);
return *this;
}
template<class BUILDER>
JSONBuilder& AddArray(stringview key, BUILDER arrayBuilder) {
BeginArray(key);
arrayBuilder(*this);
EndArray(key);
return *this;
}
// ...
};
Should I use the "low-level" BeginObject/EndObject or "high-level"
AddObject? At first I thought "high-level" and structured surely must be better, with code indentation following the output structure and
automatic closing of constructs. But then I looked at the resulting code
and now I am not so convinced any more:
/// Generates a `requestBody` object for an OpenAPI document.
void GenerateRequestBody(JSONBuilder& builder, endpoint ep) {
builder
.AddObject("requestBody", [&](JSONBuilder& builder) {
builder
.AddString("required", "true")
.AddObject("content", [&](JSONBuilder& builder) {
builder
.AddObject("application/json", [&](JSONBuilder& builder) {
builder
.AddObject("schema", [&](JSONBuilder& builder) {
builder
.AddString("type", "object")
.AddObject("properties", [&](JSONBuilder& builder) {
builder
.AddObject("control", [&](JSONBuilder& builder) {
builder.AddString("type", "object");
std::vector<std::string> requiredParams;
builder.AddObject("properties", [&](JSONBuilder& builder) {
// Iterate over all API parameters and add them to the
requestBody
for (const auto& param : parameters) {
if (HasFlag(param.endpoints_, ep) && param.direction_
== direction::request) {
builder.AddObject(param.name_, [&](JSONBuilder&
builder) {
builder
.AddString("type", ToString(param.type_))
.AddString("description", param.description_);
if (param.presence_ == presence::required) {
requiredParams.push_back(std::string(param.name_));
}
});
}
}
});
builder.AddArray("required", [&](JSONBuilder& builder) {
for (const auto& name : requiredParams) {
builder.AddString(name);
}
});
builder
.AddObject("params", [&](JSONBuilder& builder) {
builder
.AddString("type", "object")
.AddString("description", "Additional parameters for
the service. The structure of this object is service-specific.");
});
});
});
});
});
});
});
}
Not sure if this reminds me more Perl or more LISP...
For comparison, the "low-level" usage would looks much more tamed:
/// Generates a `requestBody` object for an OpenAPI endpoint.
void GenerateRequestBody(JSONBuilder& builder, endpoint ep) {
builder
.BeginObject("requestBody")
.AddString("required", "true")
.BeginObject("content")
.BeginObject("application/json")
.BeginObject("schema")
.AddString("type", "object")
.BeginObject("properties")
.BeginObject("control")
.AddString("type", "object")
.BeginObject("properties");
std::vector<std::string> requiredParams;
// Iterate over all parameters and add them to the requestBody
for (const auto& param : parameters) {
if (HasFlag(param.endpoints_, ep) && param.direction_ == direction::request) {
builder
.BeginObject(param.name_)
.AddString("type", ToString(param.type_))
.AddString("description", param.description_)
.EndObject(param.name_);
if (param.presence_ == presence::required) {
requiredParams.push_back(std::string(param.name_));
}
}
}
builder.EndObject("properties");
builder.BeginArray("required");
for (const auto& name : requiredParams) {
builder.AddString(name);
}
builder
.EndArray("required")
.EndObject("control")
.BeginObject("params")
.AddString("type", "object")
.AddString("description", "Additional parameters for the service. The structure of this object is service-specific.")
.EndObject("params")
.EndObject("properties")
.EndObject("schema")
.EndObject("application/json")
.EndObject("content")
.EndObject("requestBody");
}
Here, the most challenging task is to get the end tags matching. But
this can be checked automatically by the implementation.
Any thoughts?
On 23.01.2025 23:41, Paavo Helde wrote:
Today I am having a style question. Assume I have got a JSON
serializer class with the following interface:
Meanwhile, I have figured out yet another way to do the same, inspired
by std::format(). This now looks more like PHP...
On 24/01/2025 09:16, Paavo Helde wrote:
On 23.01.2025 23:41, Paavo Helde wrote:
Today I am having a style question. Assume I have got a JSON
serializer class with the following interface:
Meanwhile, I have figured out yet another way to do the same, inspired
by std::format(). This now looks more like PHP...
I think it is going to look ugly and complicated no matter how you do it here. So your prime concern, IMHO, should be maintainability. If you
can make a code structure that closely resembles the structure of the
JSON objects, it will be far easier to change it later when the JSON
object changes.
On 24.01.2025 12:05, David Brown wrote:
On 24/01/2025 09:16, Paavo Helde wrote:
On 23.01.2025 23:41, Paavo Helde wrote:
Today I am having a style question. Assume I have got a JSON
serializer class with the following interface:
Meanwhile, I have figured out yet another way to do the same, inspired
by std::format(). This now looks more like PHP...
I think it is going to look ugly and complicated no matter how you do it
here. So your prime concern, IMHO, should be maintainability. If you
can make a code structure that closely resembles the structure of the
JSON objects, it will be far easier to change it later when the JSON
object changes.
Yes, the best approach as always would be to add another abstraction
layer and define special OpenAPI building classes instead of generic
JSON builders. But that looks as a lot of work with little benefit.
Probably there are already some libraries out there doing that, but
using them would add another dependency.
In the end, I decided to go with the simpler BeginObject+EndObject
approach. Seems to be manageable, the key insight is always to write the >EndObject line before writing any of the content between.
On Sat, 25 Jan 2025 00:07:29 +0200
Paavo Helde <[email protected]> gabbled:
On 24.01.2025 12:05, David Brown wrote:
On 24/01/2025 09:16, Paavo Helde wrote:
On 23.01.2025 23:41, Paavo Helde wrote:
Today I am having a style question. Assume I have got a JSON
serializer class with the following interface:
Meanwhile, I have figured out yet another way to do the same,
inspired by std::format(). This now looks more like PHP...
I think it is going to look ugly and complicated no matter how you do
it here. So your prime concern, IMHO, should be maintainability. If
you can make a code structure that closely resembles the structure of
the JSON objects, it will be far easier to change it later when the
JSON object changes.
Yes, the best approach as always would be to add another abstraction
layer and define special OpenAPI building classes instead of generic
JSON builders. But that looks as a lot of work with little benefit.
Probably there are already some libraries out there doing that, but
using them would add another dependency.
In the end, I decided to go with the simpler BeginObject+EndObject
approach. Seems to be manageable, the key insight is always to write
the EndObject line before writing any of the content between.
Personal preference but I prefer just to create a std::string containing
json (or XML) and convert it to an object at the end. That way you can not only see directly what you're going to get but you can use standard C++ functions to modify it if required.
On 25.01.2025 19:04, [email protected] wrote:
Personal preference but I prefer just to create a std::string containing
json (or XML) and convert it to an object at the end. That way you can not >> only see directly what you're going to get but you can use standard C++
functions to modify it if required.
For once, I agree with you here. My JSONBuilder also builds a
std::string directly, that's the simplest and fastest approach. No need
to convert it to anything as it will be just served as an HTTP packet
anyway. Still, I parse the result with a JSON parser in Debug build,
just to make sure I have generated valid JSON.
On 23.01.2025 23:41, Paavo Helde wrote:
Today I am having a style question. Assume I have got a JSON serializer
class with the following interface:
Meanwhile, I have figured out yet another way to do the same, inspired
by std::format(). This now looks more like PHP...
void GenerateRequestBody(JSONBuilder& builder, endpoint ep) {
// Build the variable pieces beforehand
JSONBuilder properties, required;
properties.BeginObject("properties");
required.BeginArray("required");
// Iterate over all parameters and add them to the
for (const auto& param : parameters) {
if (HasFlag(param.endpoints_, ep) && param.direction_ == direction::request) {
properties
.BeginObject(param.name_)
.AddString("type", ToString(param.type_))
.AddString("description", param.description_)
.EndObject(param.name_);
if (param.presence_ == presence::required) {
required.AddString(param.name_);
}
}
}
// Inject the variable pieces as {0} and {1} in the constant
// template piece of the openapi document:
builder.AddFormatted(R"__(
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"control": {
"type": "object",
{0},
{1}
},
"params": {
"type": "object",
"description": "Parameters for the command"
}
}
}
}
}
})__", properties.str(), required.str());
}
In all three cases, the produced output result ought to be the same
(starting from `requestBody` in this example):
{
"openapi": "3.1.0",
"info": {
"title": "{redacted}",
"description": "{reducted}",
"version": "1.0.0"
},
"paths": {
"/basepath/service/test1/start": {
"post": {
"summary": "Start a new test1 session and return immediately.",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"control": {
"type": "object",
"properties": {
"authorization": {
"type": "string",
"description": "Bearer token for authorization"
},
"user": {
"type": "string",
"description": "Username"
},
"client": {
"type": "string",
"description": "Client identifier"
},
// ... ... ...
},
"required": [
"user",
]
},
"params": {
"type": "object",
"description": "Parameters for the command"
}
}
}
}
}
},
// ... ... ...
class JSONBuilder {
public:
JSONBuilder& AddString(stringview key, stringview value);
JSONBuilder& AddString(stringview value);
JSONBuilder& AddNumber(stringview key, int value);
JSONBuilder& BeginObject(stringview key);
JSONBuilder& EndObject(stringview key);
JSONBuilder& BeginArray(stringview key);
JSONBuilder& EndArray(stringview key);
template<class BUILDER>
JSONBuilder& AddObject(stringview key, BUILDER objectBuilder) {
BeginObject(key);
objectBuilder(*this);
EndObject(key);
return *this;
}
template<class BUILDER>
JSONBuilder& AddArray(stringview key, BUILDER arrayBuilder) {
BeginArray(key);
arrayBuilder(*this);
EndArray(key);
return *this;
}
// ...
};
Should I use the "low-level" BeginObject/EndObject or "high-level"
AddObject? At first I thought "high-level" and structured surely must be
better, with code indentation following the output structure and
automatic closing of constructs. But then I looked at the resulting code
and now I am not so convinced any more:
/// Generates a `requestBody` object for an OpenAPI document.
void GenerateRequestBody(JSONBuilder& builder, endpoint ep) {
builder
.AddObject("requestBody", [&](JSONBuilder& builder) {
builder
.AddString("required", "true")
.AddObject("content", [&](JSONBuilder& builder) {
builder
.AddObject("application/json", [&](JSONBuilder& builder) {
builder
.AddObject("schema", [&](JSONBuilder& builder) {
builder
.AddString("type", "object")
.AddObject("properties", [&](JSONBuilder& builder) { >> builder
.AddObject("control", [&](JSONBuilder& builder) { >> builder.AddString("type", "object");
std::vector<std::string> requiredParams;
builder.AddObject("properties", [&](JSONBuilder& builder) {
// Iterate over all API parameters and add them to the
requestBody
for (const auto& param : parameters) {
if (HasFlag(param.endpoints_, ep) && param.direction_
== direction::request) {
builder.AddObject(param.name_, [&](JSONBuilder&
builder) {
builder
.AddString("type", ToString(param.type_))
.AddString("description", param.description_);
if (param.presence_ == presence::required) {
requiredParams.push_back(std::string(param.name_));
}
});
}
}
});
builder.AddArray("required", [&](JSONBuilder& builder) {
for (const auto& name : requiredParams) {
builder.AddString(name);
}
});
builder
.AddObject("params", [&](JSONBuilder& builder) {
builder
.AddString("type", "object")
.AddString("description", "Additional parameters for
the service. The structure of this object is service-specific.");
});
});
});
});
});
});
});
}
Not sure if this reminds me more Perl or more LISP...
For comparison, the "low-level" usage would looks much more tamed:
/// Generates a `requestBody` object for an OpenAPI endpoint.
void GenerateRequestBody(JSONBuilder& builder, endpoint ep) {
builder
.BeginObject("requestBody")
.AddString("required", "true")
.BeginObject("content")
.BeginObject("application/json")
.BeginObject("schema")
.AddString("type", "object")
.BeginObject("properties")
.BeginObject("control")
.AddString("type", "object")
.BeginObject("properties");
std::vector<std::string> requiredParams;
// Iterate over all parameters and add them to the requestBody
for (const auto& param : parameters) {
if (HasFlag(param.endpoints_, ep) && param.direction_ ==
direction::request) {
builder
.BeginObject(param.name_)
.AddString("type", ToString(param.type_))
.AddString("description", param.description_)
.EndObject(param.name_);
if (param.presence_ == presence::required) {
requiredParams.push_back(std::string(param.name_));
}
}
}
builder.EndObject("properties");
builder.BeginArray("required");
for (const auto& name : requiredParams) {
builder.AddString(name);
}
builder
.EndArray("required")
.EndObject("control")
.BeginObject("params")
.AddString("type", "object")
.AddString("description", "Additional parameters for the service. >> The structure of this object is service-specific.")
.EndObject("params")
.EndObject("properties")
.EndObject("schema")
.EndObject("application/json")
.EndObject("content")
.EndObject("requestBody");
}
Here, the most challenging task is to get the end tags matching. But
this can be checked automatically by the implementation.
Any thoughts?
| Sysop: | Keyop |
|---|---|
| Location: | Huddersfield, West Yorkshire, UK |
| Users: | 715 |
| Nodes: | 16 (2 / 14) |
| Uptime: | 27:39:18 |
| Calls: | 12,106 |
| Calls today: | 6 |
| Files: | 15,006 |
| Messages: | 6,518,216 |