Skip to content

Commit 1e3f9d6

Browse files
committed
Introduce JsonEncoder class
1 parent 59b7dfb commit 1e3f9d6

File tree

2 files changed

+238
-0
lines changed

2 files changed

+238
-0
lines changed

lib/base/json.cpp

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,104 @@
1818

1919
using namespace icinga;
2020

21+
constexpr uint8_t JsonEncoder::m_IndentSize;
22+
23+
/**
24+
* Encodes the given value into JSON and writes it to the configured output stream.
25+
*
26+
* This function is specialized for the @c Value type and recursively encodes each
27+
* and every concrete type it represents.
28+
*
29+
* @param value The value to be JSON serialized.
30+
* @param currentIndent The current indentation level. Defaults to 0.
31+
*/
32+
void JsonEncoder::Encode(const Value& value, unsigned currentIndent)
33+
{
34+
switch (value.GetType()) {
35+
case ValueEmpty:
36+
m_Writer->write_characters("null", 4);
37+
return;
38+
case ValueBoolean:
39+
if (value.ToBool()) {
40+
m_Writer->write_characters("true", 4);
41+
} else {
42+
m_Writer->write_characters("false", 5);
43+
}
44+
return;
45+
case ValueString:
46+
EncodeValidatedJson(Utility::ValidateUTF8(value.Get<String>()));
47+
return;
48+
case ValueNumber:
49+
if (auto ll(static_cast<long long>(value)); ll == value) {
50+
EncodeValidatedJson(ll);
51+
} else {
52+
EncodeValidatedJson(value.Get<double>());
53+
}
54+
return;
55+
case ValueObject: {
56+
const Object::Ptr& obj = value.Get<Object::Ptr>();
57+
if (obj->GetReflectionType() == Namespace::TypeInstance) {
58+
if (auto ns(static_pointer_cast<Namespace>(obj)); ns->GetLength() == 0) {
59+
m_Writer->write_characters("{}", 2);
60+
} else {
61+
EncodeObj(ns, currentIndent, [](const NamespaceValue& v) { return v.Val; });
62+
}
63+
} else if (obj->GetReflectionType() == Dictionary::TypeInstance) {
64+
if (auto dict(static_pointer_cast<Dictionary>(obj)); dict->GetLength() == 0) {
65+
m_Writer->write_characters("{}", 2);
66+
} else {
67+
EncodeObj(dict, currentIndent, [](const Value& v) { return v; });
68+
}
69+
} else if (obj->GetReflectionType() == Array::TypeInstance) {
70+
auto arr(static_pointer_cast<Array>(obj));
71+
ObjectLock olock(arr);
72+
auto begin(arr->Begin());
73+
auto end(arr->End());
74+
// Release the object lock if we're writing to an Asio stream, i.e. every write operation
75+
// is asynchronous which may cause the coroutine to yield and later resume on another thread.
76+
RELEASE_OLOCK_IF_ASYNC_WRITER(m_Writer, olock);
77+
78+
if (arr->GetLength() == 0) {
79+
m_Writer->write_characters("[]", 2);
80+
} else if (m_Pretty) {
81+
m_Writer->write_characters("[\n", 2);
82+
83+
auto newIndent = currentIndent + m_IndentSize;
84+
if (m_IndentStr.size() < newIndent) {
85+
m_IndentStr.resize(m_IndentStr.size() * 2, ' ');
86+
}
87+
88+
for (auto it(begin); it != end; ++it) {
89+
if (it != begin) {
90+
m_Writer->write_characters(",\n", 2);
91+
}
92+
m_Writer->write_characters(m_IndentStr.c_str(), newIndent);
93+
Encode(*it, newIndent);
94+
}
95+
m_Writer->write_character('\n');
96+
m_Writer->write_characters(m_IndentStr.c_str(), currentIndent);
97+
m_Writer->write_character(']');
98+
} else {
99+
m_Writer->write_character('[');
100+
for (auto it(begin); it != end; ++it) {
101+
if (it != begin) {
102+
m_Writer->write_character(',');
103+
}
104+
Encode(*it, currentIndent);
105+
}
106+
m_Writer->write_character(']');
107+
}
108+
} else {
109+
// Some other non-serializable object type!
110+
EncodeValidatedJson(Utility::ValidateUTF8(obj->ToString()));
111+
}
112+
return;
113+
}
114+
default:
115+
VERIFY(!"Invalid variant type.");
116+
}
117+
}
118+
21119
class JsonSax : public nlohmann::json_sax<nlohmann::json>
22120
{
23121
public:

lib/base/json.hpp

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44
#define JSON_H
55

66
#include "base/i2-base.hpp"
7+
#include "base/dictionary.hpp"
8+
#include "base/namespace.hpp"
9+
#include "base/objectlock.hpp"
710
#include "base/tlsstream.hpp"
11+
#include "base/utility.hpp"
812
#include <json.hpp>
913

1014
namespace icinga
@@ -77,6 +81,142 @@ class AsioStreamAdapter : public AsyncJsonWriter
7781
class String;
7882
class Value;
7983

84+
/**
85+
* JSON encoder.
86+
*
87+
* This class can be used to encode Icinga Value types into JSON format and write them to an output stream.
88+
* The supported stream types include std::ostream and AsioStreamAdapter. The nlohmann/json library already
89+
* provides a full support for the former stream type, while the latter is fully implemented by our own and
90+
* satisfies the @c nlohmann::detail::output_adapter_protocol<> interface. It is used for writing the produced
91+
* JSON directly to an Asio either TCP or TLS stream and doesn't require any additional buffering other than
92+
* the one used by the Asio buffered_stream<> class internally.
93+
*
94+
* The JSON encoder generates most of the low level JSON tokens, but it still relies on the already existing
95+
* @c nlohmann::detail::serializer<> class to dump numbers and ascii validated JSON strings. This means that the
96+
* encoder doesn't perform any kind of JSON validation or escaping on its own, but simply delegates all this kind
97+
* of work to serializer<>. However, Strings are UTF-8 validated beforehand using the @c Utility::ValidateUTF8()
98+
* function and only the validated (copy of the original) String is passed to the serializer.
99+
*
100+
* The generated JSON can be either prettified or compact, depending on your needs. The prettified JSON object
101+
* is indented with 4 spaces and grows linearly with the depth of the object tree.
102+
*
103+
* @note The JSON serialization logic of this encoder is heavily inspired by the @c nlohmann::detail::serializer<>.
104+
*
105+
* @ingroup base
106+
*/
107+
class JsonEncoder
108+
{
109+
#define RELEASE_OLOCK_IF_ASYNC_WRITER(writer, olock) \
110+
if (std::is_base_of<AsyncJsonWriter, decltype(writer)>::value) { \
111+
olock.Unlock(); \
112+
}
113+
114+
public:
115+
explicit JsonEncoder(std::basic_ostream<char>& stream, bool prettify = false)
116+
: JsonEncoder{nlohmann::detail::output_adapter<char>(stream), prettify}
117+
{
118+
}
119+
120+
explicit JsonEncoder(const std::shared_ptr<AsioStreamAdapter<AsioTlsStream>>& writer, bool prettify = false)
121+
: JsonEncoder{nlohmann::detail::output_adapter_t<char>(writer), prettify}
122+
{
123+
}
124+
125+
void Encode(const Value& value, unsigned currentIndent = 0);
126+
127+
private:
128+
JsonEncoder(nlohmann::detail::output_adapter_t<char> stream, bool pretty)
129+
: m_Pretty(pretty), m_IndentStr(32, ' '), m_Writer(std::move(stream))
130+
{}
131+
132+
/**
133+
* Serializes a value into JSON using the serializer and writes it directly to @c m_Writer.
134+
*
135+
* @tparam T Type of the value to encode via the serializer.
136+
* @param value The value to validate and encode using the serializer.
137+
*/
138+
template<typename T>
139+
void EncodeValidatedJson(T&& value)
140+
{
141+
nlohmann::detail::serializer<nlohmann::json> s(m_Writer, ' ', nlohmann::json::error_handler_t::strict);
142+
s.dump(nlohmann::json(std::forward<T>(value)), m_Pretty, true, 0, 0);
143+
}
144+
145+
/**
146+
* Encodes an Icinga 2 object (Namespace or Dictionary) into JSON and writes it to @c m_Writer.
147+
*
148+
* @tparam Iterable Type of the container (Namespace or Dictionary).
149+
* @tparam ValExtractor Type of the value extractor function used to extract values from the container's iterator.
150+
*
151+
* @param container The container to JSON serialize.
152+
* @param currentIndent The current indentation level.
153+
* @param extractor The value extractor function used to extract values from the container's iterator.
154+
*/
155+
template<typename Iterable, typename ValExtractor>
156+
void EncodeObj(const Iterable& container, unsigned currentIndent, ValExtractor extractor)
157+
{
158+
static_assert(std::__is_any_of<Iterable, Namespace::Ptr, Dictionary::Ptr>::value, "Container must be a Namespace or Dictionary");
159+
160+
ObjectLock olock(container);
161+
auto begin(container->Begin());
162+
auto end(container->End());
163+
// Release the object lock if we're writing to an Asio stream, i.e. every write operation
164+
// is asynchronous which may cause the coroutine to yield and later resume on another thread.
165+
RELEASE_OLOCK_IF_ASYNC_WRITER(*m_Writer, olock);
166+
167+
if (m_Pretty) {
168+
m_Writer->write_characters("{\n", 2);
169+
170+
auto newIndent = currentIndent + m_IndentSize;
171+
if (m_IndentStr.size() < newIndent) {
172+
m_IndentStr.resize(m_IndentStr.size() * 2, ' ');
173+
}
174+
175+
for (auto it(begin); it != end; ++it) {
176+
// Add a comma before the next key-value pair, but only if it's not the first one.
177+
if (it != begin) {
178+
m_Writer->write_characters(",\n", 2);
179+
}
180+
m_Writer->write_characters(m_IndentStr.c_str(), newIndent);
181+
EncodeValidatedJson(Utility::ValidateUTF8(it->first));
182+
m_Writer->write_characters(": ", 2);
183+
Encode(extractor(it->second), newIndent);
184+
}
185+
186+
m_Writer->write_character('\n');
187+
m_Writer->write_characters(m_IndentStr.c_str(), currentIndent);
188+
m_Writer->write_character('}');
189+
} else {
190+
m_Writer->write_character('{');
191+
for (auto it(begin); it != end; ++it) {
192+
// Add a comma before the next key-value pair, but not before the first one.
193+
if (it != begin) {
194+
m_Writer->write_character(',');
195+
}
196+
EncodeValidatedJson(Utility::ValidateUTF8(it->first));
197+
m_Writer->write_character(':');
198+
Encode(extractor(it->second), currentIndent);
199+
}
200+
m_Writer->write_character('}');
201+
}
202+
}
203+
204+
private:
205+
// The number of spaces to use for indentation in prettified JSON.
206+
static constexpr uint8_t m_IndentSize = 4;
207+
208+
bool m_Pretty; // Whether to pretty-print the JSON output.
209+
// The pre-allocated indent characters for pretty-printing.
210+
// By default, 32 ' ' characters (bytes) are allocated which can be used to indent JSON object trees up to
211+
// 8 levels deep (4 spaces per level) without reallocation. Otherwise, when encountering a deeper JSON tree,
212+
// this will be resized to the required size on the fly and doubled each time.
213+
std::string m_IndentStr;
214+
215+
// The output stream adapter for writing JSON data.
216+
// This can be either a std::ostream or an Asio stream adapter.
217+
nlohmann::detail::output_adapter_t<char> m_Writer;
218+
};
219+
80220
String JsonEncode(const Value& value, bool pretty_print = false);
81221
Value JsonDecode(const String& data);
82222

0 commit comments

Comments
 (0)