diff --git a/src/script/C4PropList.cpp b/src/script/C4PropList.cpp index f9cab1c77..c8139800d 100644 --- a/src/script/C4PropList.cpp +++ b/src/script/C4PropList.cpp @@ -512,6 +512,26 @@ void C4PropList::AppendDataString(StdStrBuf * out, const char * delim, int depth } } +StdStrBuf C4PropList::ToJSON(int depth, bool ignore_reference_parent) const +{ + if (depth <= 0 && Properties.GetSize()) + { + throw new C4JSONSerializationError("maximum depth reached"); + } + StdStrBuf DataString; + DataString = "{"; + std::list sorted_props = Properties.GetSortedListOfElementPointers(); + for (std::list::const_iterator p = sorted_props.begin(); p != sorted_props.end(); ++p) + { + if (p != sorted_props.begin()) DataString.Append(","); + DataString.Append(C4Value((*p)->Key).ToJSON()); + DataString.Append(":"); + DataString.Append((*p)->Value.ToJSON(depth - 1, ignore_reference_parent ? IsStatic() : nullptr)); + } + DataString.Append("}"); + return DataString; +} + std::vector< C4String * > C4PropList::GetSortedLocalProperties(bool add_prototype) const { // return property list without descending into prototype diff --git a/src/script/C4PropList.h b/src/script/C4PropList.h index dd7b3a9e3..45d5fda39 100644 --- a/src/script/C4PropList.h +++ b/src/script/C4PropList.h @@ -141,6 +141,7 @@ public: void CompileFunc(StdCompiler *pComp, C4ValueNumbers *); void AppendDataString(StdStrBuf * out, const char * delim, int depth = 3, bool ignore_reference_parent = false) const; + StdStrBuf ToJSON(int depth = 10, bool ignore_reference_parent = false) const; std::vector< C4String * > GetSortedLocalProperties(bool add_prototype=true) const; std::vector< C4String * > GetSortedLocalProperties(const char *prefix, const C4PropList *ignore_overridden) const; std::vector< C4String * > GetUnsortedProperties(const char *prefix, C4PropList *ignore_parent = nullptr) const; diff --git a/src/script/C4Value.cpp b/src/script/C4Value.cpp index decbb7564..a93f9198e 100644 --- a/src/script/C4Value.cpp +++ b/src/script/C4Value.cpp @@ -184,6 +184,64 @@ StdStrBuf C4Value::GetDataString(int depth, const C4PropListStatic *ignore_refer } } +// JSON serialization. +// Only plain data values can be serialized. Throws a C4JSONSerializationError +// when encountering values that cannot be represented in JSON or when the +// maximum depth is reached. +StdStrBuf C4Value::ToJSON(int depth, const C4PropListStatic *ignore_reference_parent) const +{ + // ouput by type info + switch (GetType()) + { + case C4V_Int: + return FormatString("%ld", static_cast(Data.Int)); + case C4V_Bool: + return StdStrBuf(Data ? "true" : "false"); + case C4V_PropList: + { + const C4PropListStatic * Def = Data.PropList->IsStatic(); + if (Def) + if (!ignore_reference_parent || Def->GetParent() != ignore_reference_parent) + return Def->ToJSON(); + return Data.PropList->ToJSON(depth, Def && ignore_reference_parent); + } + case C4V_String: + if (Data.Str && Data.Str->GetCStr()) + { + StdStrBuf str = Data.Str->GetData(); + str.EscapeString(); + str.Replace("\n", "\\n"); + return FormatString("\"%s\"", str.getData()); + } + else + { + return StdStrBuf("null"); + } + case C4V_Array: + { + if (depth <= 0 && Data.Array->GetSize()) + { + throw C4JSONSerializationError("maximum depth reached"); + } + StdStrBuf DataString; + DataString = "["; + for (int32_t i = 0; i < Data.Array->GetSize(); i++) + { + if (i) DataString.Append(","); + DataString.Append(std::move(Data.Array->GetItem(i).GetDataString(depth - 1))); + } + DataString.AppendChar(']'); + return DataString; + } + case C4V_Function: + throw C4JSONSerializationError("cannot serialize function"); + case C4V_Nil: + return StdStrBuf("null"); + default: + throw C4JSONSerializationError("unknown type"); + } +} + const C4Value & C4ValueNumbers::GetValue(uint32_t n) { if (n <= LoadedValues.size()) diff --git a/src/script/C4Value.h b/src/script/C4Value.h index 9a8a5726b..b0b2f5558 100644 --- a/src/script/C4Value.h +++ b/src/script/C4Value.h @@ -61,6 +61,14 @@ union C4V_Data C4V_Data &operator = (void *p) { assert(!p); Ptr = p; return *this; } }; +class C4JSONSerializationError : public std::exception +{ + std::string msg; +public: + C4JSONSerializationError(const std::string& msg) : msg(msg) {} + virtual const char* what() const noexcept override { return msg.c_str(); } +}; + class C4Value { public: @@ -158,6 +166,7 @@ public: void Denumerate(C4ValueNumbers *); StdStrBuf GetDataString(int depth = 10, const class C4PropListStatic *ignore_reference_parent = nullptr) const; + StdStrBuf ToJSON(int depth = 10, const class C4PropListStatic *ignore_reference_parent = nullptr) const; ALWAYS_INLINE bool CheckParConversion(C4V_Type vtToType) const // convert to dest type { diff --git a/tests/C4ValueTest.cpp b/tests/C4ValueTest.cpp index 14f49de43..e82ad186a 100644 --- a/tests/C4ValueTest.cpp +++ b/tests/C4ValueTest.cpp @@ -34,3 +34,41 @@ TEST(C4ValueTest, SanityTests) EXPECT_TRUE(C4Value(true)); EXPECT_FALSE(C4Value(false)); } + +TEST(C4ValueTest, ToJSON) +{ + // Wrapping in std::string makes GTest print something useful in case of failure. +#define EXPECT_STDSTRBUF_EQ(a, b) EXPECT_EQ(std::string((a).getData()), std::string(b)); + + // simple values + EXPECT_STDSTRBUF_EQ(C4Value(42).ToJSON(), "42"); + EXPECT_STDSTRBUF_EQ(C4Value(-42).ToJSON(), "-42"); + EXPECT_STDSTRBUF_EQ(C4Value("foobar").ToJSON(), R"#("foobar")#"); + EXPECT_STDSTRBUF_EQ(C4Value("es\"caping").ToJSON(), R"#("es\"caping")#"); + EXPECT_STDSTRBUF_EQ(C4Value("es\\caping").ToJSON(), R"#("es\\caping")#"); + EXPECT_STDSTRBUF_EQ(C4Value("new\nline").ToJSON(), R"#("new\nline")#"); + EXPECT_STDSTRBUF_EQ(C4Value(true).ToJSON(), R"#(true)#"); + EXPECT_STDSTRBUF_EQ(C4Value(false).ToJSON(), R"#(false)#"); + EXPECT_STDSTRBUF_EQ(C4Value().ToJSON(), R"#(null)#"); + + // proplists + auto proplist = C4PropList::NewStatic(nullptr, nullptr, nullptr); + proplist->SetProperty(P_Options, C4Value("options")); + proplist->SetProperty(P_Min, C4Value(13)); + auto nested = C4PropList::NewStatic(nullptr, nullptr, nullptr); + nested->SetProperty(P_Description, C4Value(true)); + proplist->SetProperty(P_Storage, C4Value(nested)); + EXPECT_STDSTRBUF_EQ(C4Value(proplist).ToJSON(), R"#({"Min":13,"Options":"options","Storage":{"Description":true}})#"); + + auto crazy_key = C4PropList::NewStatic(nullptr, nullptr, nullptr); + auto key = Strings.RegString("foo\"bar"); + proplist->SetPropertyByS(key, C4Value(42)); + EXPECT_STDSTRBUF_EQ(C4Value(proplist).ToJSON(), R"#({"foo\"bar":42})#"); + + // arrays + auto array = new C4ValueArray(3); + array->SetItem(0, C4Value(1)); + array->SetItem(1, C4Value(2)); + array->SetItem(2, C4Value(3)); + EXPECT_STDSTRBUF_EQ(C4Value(array).ToJSON(), R"#([1,2,3])#"); +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 6fb919b4b..0bac3577a 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -130,6 +130,15 @@ if (GTEST_FOUND AND GMOCK_FOUND) LIBRARIES libmisc libc4script) + # This is included in "tests" (above) as well, but that executable currently doesn't compile. + create_test(c4value_test + SOURCES + C4ValueTest.cpp + ../src/script/C4ScriptStandaloneStubs.cpp + ../src/script/C4ScriptStandalone.cpp + LIBRARIES + libmisc + libc4script) else() set(_gtest_missing "") if (NOT GTEST_INCLUDE_DIR)