diff --git a/docs/usage.md b/docs/usage.md index 7b970b5b..26a47952 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -37,6 +37,21 @@ sonic_json::WriteBuffer wb; doc.Serialize(wb); std::cout << wb.ToString() << std::endl; ``` + +#### Pretty serialize to an indented string +Use `PrettySerialize()` when human-readable output is preferred. It uses two +spaces per indentation level by default; pass a different width as the second +argument when needed. +```c++ +#include "sonic/sonic.h" +// ... +sonic_json::WriteBuffer wb; +doc.PrettySerialize(wb); // two spaces per level +std::cout << wb.ToString() << std::endl; + +doc.PrettySerialize(wb, 4); // four spaces per level +std::string json = doc.PrettyDump(4); +``` ### Node Node is the present for JSON value and supports all JSON value manipulation. diff --git a/include/sonic/dom/dynamicnode.h b/include/sonic/dom/dynamicnode.h index 26786567..ad08ce8b 100644 --- a/include/sonic/dom/dynamicnode.h +++ b/include/sonic/dom/dynamicnode.h @@ -980,6 +980,11 @@ class DNode : public GenericNode> { return internal::SerializeImpl(this, wb); } + template + SonicError prettySerializeImpl(WriteBuffer& wb, size_t indentSize) const { + return internal::PrettySerializeImpl(this, wb, indentSize); + } + sonic_force_inline DNode* nextImpl() { return this + 1; } sonic_force_inline const DNode* cnextImpl() const { return this + 1; } diff --git a/include/sonic/dom/genericnode.h b/include/sonic/dom/genericnode.h index 7b3c4e9d..cda5a242 100644 --- a/include/sonic/dom/genericnode.h +++ b/include/sonic/dom/genericnode.h @@ -1069,6 +1069,19 @@ class GenericNode { return downCast()->template serializeImpl(wb); } + /** + * @brief serialize this node as an indented JSON string. + * @param serializeFlags combination of different SerializeFlag. + * @param wb write buffer where you want to store the JSON string. + * @param indentSize number of spaces to use for each indentation level. + * @return EndcodeError + */ + template + SonicError PrettySerialize(WriteBuffer& wb, size_t indentSize = 2) const { + return downCast()->template prettySerializeImpl(wb, + indentSize); + } + /** * @brief dump this node as json string. * @param serializeFlags combination of different SerializeFlag. @@ -1085,6 +1098,23 @@ class GenericNode { return std::string(sv.data(), sv.size()); } + /** + * @brief dump this node as an indented JSON string. + * @param serializeFlags combination of different SerializeFlag. + * @param indentSize number of spaces to use for each indentation level. + * @return empty string if there are errors when serializing. + */ + template + std::string PrettyDump(size_t indentSize = 2) const { + WriteBuffer wb; + SonicError err = PrettySerialize(wb, indentSize); + if (err != kErrorNone) { + return ""; + } + auto sv = wb.ToStringView(); + return std::string(sv.data(), sv.size()); + } + protected: sonic_force_inline NodeType* next() noexcept { return downCast()->nextImpl(); diff --git a/include/sonic/dom/serialize.h b/include/sonic/dom/serialize.h index d89327db..32deafa8 100644 --- a/include/sonic/dom/serialize.h +++ b/include/sonic/dom/serialize.h @@ -227,5 +227,102 @@ sonic_force_inline SonicError SerializeImpl(const NodeType* node, return kSerErrorInvalidObjKey; } +inline void PushPrettyLine(WriteBuffer& wb, size_t depth, size_t indent_size) { + wb.Push('\n'); + for (size_t i = 0; i < depth; ++i) { + for (size_t j = 0; j < indent_size; ++j) { + wb.Push(' '); + } + } +} + +template +SonicError PrettySerializeImpl(const NodeType* node, WriteBuffer& wb, + size_t indent_size) { + struct ParentCtx { + const NodeType* node; + size_t index; + }; + + constexpr SerializeFlags append_flags = + serializeFlags | SerializeFlags::kSerializeAppendBuffer; + internal::Stack parents; + size_t depth = 0; + SonicError error = kErrorNone; + + if constexpr ((serializeFlags & SerializeFlags::kSerializeAppendBuffer) == + 0) { + wb.Clear(); + } + wb.Reserve(wb.Size() + 64); + +value_begin: + if (!node->IsContainer() || node->Empty()) { + error = SerializeImpl(node, wb); + if (sonic_unlikely(error != kErrorNone)) { + return error; + } + goto value_end; + } + + wb.Push(node->IsObject() ? '{' : '['); + parents.Push(ParentCtx{node, 0}); + ++depth; + PushPrettyLine(wb, depth, indent_size); + + if (node->IsObject()) { + auto member = node->MemberBegin(); + if (sonic_unlikely(!member->name.IsString())) { + return kSerErrorInvalidObjKey; + } + error = SerializeImpl(&(member->name), wb); + if (sonic_unlikely(error != kErrorNone)) { + return error; + } + wb.Push(':'); + wb.Push(' '); + node = &(member->value); + } else { + node = &(*(node->Begin())); + } + goto value_begin; + +value_end: + if (parents.Empty()) { + return kErrorNone; + } + + { + ParentCtx* parent = parents.Top(); + ++parent->index; + if (parent->index < parent->node->Size()) { + wb.Push(','); + PushPrettyLine(wb, depth, indent_size); + if (parent->node->IsObject()) { + auto member = parent->node->MemberBegin() + parent->index; + if (sonic_unlikely(!member->name.IsString())) { + return kSerErrorInvalidObjKey; + } + error = SerializeImpl(&(member->name), wb); + if (sonic_unlikely(error != kErrorNone)) { + return error; + } + wb.Push(':'); + wb.Push(' '); + node = &(member->value); + } else { + node = &(*(parent->node->Begin() + parent->index)); + } + goto value_begin; + } + + --depth; + PushPrettyLine(wb, depth, indent_size); + wb.Push(parent->node->IsObject() ? '}' : ']'); + parents.Pop(1); + } + goto value_end; +} + } // namespace internal } // namespace sonic_json diff --git a/tests/document_test.cpp b/tests/document_test.cpp index 5db2398b..a36353e9 100644 --- a/tests/document_test.cpp +++ b/tests/document_test.cpp @@ -577,6 +577,70 @@ TYPED_TEST(DocumentTest, SerializeOK) { } } +TYPED_TEST(DocumentTest, PrettySerialize) { + TypeParam doc; + const std::string json = + R"({"name":"sonic","items":[1,true,null,{"nested":"value"}],"empty":[]})"; + doc.Parse(json); + ASSERT_FALSE(doc.HasParseError()); + + const std::string expected = + "{\n" + " \"name\": \"sonic\",\n" + " \"items\": [\n" + " 1,\n" + " true,\n" + " null,\n" + " {\n" + " \"nested\": \"value\"\n" + " }\n" + " ],\n" + " \"empty\": []\n" + "}"; + + WriteBuffer wb; + EXPECT_EQ(doc.PrettySerialize(wb), kErrorNone); + EXPECT_EQ(wb.ToStringView(), expected); + EXPECT_EQ(doc.PrettyDump(), expected); + + const std::string four_space_expected = + "{\n" + " \"value\": 1\n" + "}"; + TypeParam small_doc; + small_doc.Parse(R"({"value":1})"); + ASSERT_FALSE(small_doc.HasParseError()); + EXPECT_EQ(small_doc.PrettyDump(4), four_space_expected); +} + +TYPED_TEST(DocumentTest, PrettySerializeFlagsAndErrors) { + TypeParam doc; + doc.Parse(R"([1,2])"); + ASSERT_FALSE(doc.HasParseError()); + + WriteBuffer wb; + wb.Push("prefix:", 7); + EXPECT_EQ( + doc.template PrettySerialize(wb, + 0), + kErrorNone); + EXPECT_STREQ(wb.ToString(), "prefix:[\n1,\n2\n]"); + + doc.SetDouble(std::numeric_limits::infinity()); + EXPECT_EQ(doc.PrettySerialize(wb), kSerErrorInfinity); + EXPECT_TRUE(doc.PrettyDump().empty()); + EXPECT_EQ(doc.template PrettyDump(), + "\"Infinity\""); + + doc.Parse(R"({"key":1})"); + ASSERT_FALSE(doc.HasParseError()); + using DNode = typename TypeParam::NodeType; + auto member = doc.MemberBegin(); + const_cast(&(member->name))->SetNull(); + EXPECT_EQ(doc.PrettySerialize(wb), kSerErrorInvalidObjKey); + EXPECT_TRUE(doc.PrettyDump().empty()); +} + TYPED_TEST(DocumentTest, SonicErrorInvalidKey) { using DNode = typename TypeParam::NodeType; auto iter = this->doc_.MemberBegin();