aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorStefan Boberg <[email protected]>2024-03-26 13:31:10 +0100
committerGitHub Enterprise <[email protected]>2024-03-26 13:31:10 +0100
commit59e51bf811a5907983b84f3e2ed14e47a7210e75 (patch)
treea8418dec9b8184ad76d6922da23cebe9db290897 /src
parentadd filter to projectstore entries request (#25) (diff)
downloadzen-59e51bf811a5907983b84f3e2ed14e47a7210e75.tar.xz
zen-59e51bf811a5907983b84f3e2ed14e47a7210e75.zip
add yaml serialization support (#3)
this change adds serialization of payloads as YAML, but not parsing. The implementation is somewhat based on the JSON path, and may be collapsed eventually as it is possible to serialize JSON format using the same code it also separates out the JSON serialization into a separate file for ease of maintenance any HTTP request response may be formatted as yaml by using a `.yaml` suffix or an `Accept: text/yaml` header
Diffstat (limited to 'src')
-rw-r--r--src/zencore/compactbinary.cpp781
-rw-r--r--src/zencore/compactbinaryjson.cpp805
-rw-r--r--src/zencore/compactbinaryyaml.cpp352
-rw-r--r--src/zencore/include/zencore/compactbinary.h32
-rw-r--r--src/zencore/xmake.lua2
-rw-r--r--src/zencore/zencore.cpp2
-rw-r--r--src/zenhttp/httpserver.cpp10
7 files changed, 1201 insertions, 783 deletions
diff --git a/src/zencore/compactbinary.cpp b/src/zencore/compactbinary.cpp
index 6677b5a61..0907f8a2e 100644
--- a/src/zencore/compactbinary.cpp
+++ b/src/zencore/compactbinary.cpp
@@ -24,10 +24,6 @@
# include <time.h>
#endif
-ZEN_THIRD_PARTY_INCLUDES_START
-#include <json11.hpp>
-ZEN_THIRD_PARTY_INCLUDES_END
-
namespace zen {
const int DaysToMonth[] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365};
@@ -1468,339 +1464,6 @@ SaveCompactBinary(BinaryWriter& Ar, const CbObjectView& Object)
//////////////////////////////////////////////////////////////////////////
-class CbJsonWriter
-{
-public:
- explicit CbJsonWriter(StringBuilderBase& InBuilder) : Builder(InBuilder) { NewLineAndIndent << LINE_TERMINATOR_ANSI; }
-
- void BeginObject()
- {
- Builder << '{';
- NewLineAndIndent << '\t';
- NeedsNewLine = true;
- }
-
- void EndObject()
- {
- NewLineAndIndent.RemoveSuffix(1);
- if (NeedsComma)
- {
- WriteOptionalNewLine();
- }
- Builder << '}';
- }
-
- void BeginArray()
- {
- Builder << '[';
- NewLineAndIndent << '\t';
- NeedsNewLine = true;
- }
-
- void EndArray()
- {
- NewLineAndIndent.RemoveSuffix(1);
- if (NeedsComma)
- {
- WriteOptionalNewLine();
- }
- Builder << ']';
- }
-
- void WriteField(CbFieldView Field)
- {
- using namespace std::literals;
-
- WriteOptionalComma();
- WriteOptionalNewLine();
-
- if (std::u8string_view Name = Field.GetU8Name(); !Name.empty())
- {
- AppendQuotedString(Name);
- Builder << ": "sv;
- }
-
- switch (CbValue Accessor = Field.GetValue(); Accessor.GetType())
- {
- case CbFieldType::Null:
- Builder << "null"sv;
- break;
- case CbFieldType::Object:
- case CbFieldType::UniformObject:
- {
- BeginObject();
- for (CbFieldView It : Field)
- {
- WriteField(It);
- }
- EndObject();
- }
- break;
- case CbFieldType::Array:
- case CbFieldType::UniformArray:
- {
- BeginArray();
- for (CbFieldView It : Field)
- {
- WriteField(It);
- }
- EndArray();
- }
- break;
- case CbFieldType::Binary:
- AppendBase64String(Accessor.AsBinary());
- break;
- case CbFieldType::String:
- AppendQuotedString(Accessor.AsU8String());
- break;
- case CbFieldType::IntegerPositive:
- Builder << Accessor.AsIntegerPositive();
- break;
- case CbFieldType::IntegerNegative:
- Builder << Accessor.AsIntegerNegative();
- break;
- case CbFieldType::Float32:
- {
- const float Value = Accessor.AsFloat32();
- if (std::isfinite(Value))
- {
- Builder.Append(fmt::format("{:.9g}", Value));
- }
- else
- {
- Builder << "null"sv;
- }
- }
- break;
- case CbFieldType::Float64:
- {
- const double Value = Accessor.AsFloat64();
- if (std::isfinite(Value))
- {
- Builder.Append(fmt::format("{:.17g}", Value));
- }
- else
- {
- Builder << "null"sv;
- }
- }
- break;
- case CbFieldType::BoolFalse:
- Builder << "false"sv;
- break;
- case CbFieldType::BoolTrue:
- Builder << "true"sv;
- break;
- case CbFieldType::ObjectAttachment:
- case CbFieldType::BinaryAttachment:
- {
- Builder << '"';
- Accessor.AsAttachment().ToHexString(Builder);
- Builder << '"';
- }
- break;
- case CbFieldType::Hash:
- {
- Builder << '"';
- Accessor.AsHash().ToHexString(Builder);
- Builder << '"';
- }
- break;
- case CbFieldType::Uuid:
- {
- Builder << '"';
- Accessor.AsUuid().ToString(Builder);
- Builder << '"';
- }
- break;
- case CbFieldType::DateTime:
- Builder << '"' << DateTime(Accessor.AsDateTimeTicks()).ToIso8601() << '"';
- break;
- case CbFieldType::TimeSpan:
- {
- const TimeSpan Span(Accessor.AsTimeSpanTicks());
- if (Span.GetDays() == 0)
- {
- Builder << '"' << Span.ToString("%h:%m:%s.%n") << '"';
- }
- else
- {
- Builder << '"' << Span.ToString("%d.%h:%m:%s.%n") << '"';
- }
- break;
- }
- case CbFieldType::ObjectId:
- Builder << '"';
- Accessor.AsObjectId().ToString(Builder);
- Builder << '"';
- break;
- case CbFieldType::CustomById:
- {
- CbCustomById Custom = Accessor.AsCustomById();
- Builder << "{ \"Id\": ";
- Builder << Custom.Id;
- Builder << ", \"Data\": ";
- AppendBase64String(Custom.Data);
- Builder << " }";
- break;
- }
- case CbFieldType::CustomByName:
- {
- CbCustomByName Custom = Accessor.AsCustomByName();
- Builder << "{ \"Name\": ";
- AppendQuotedString(Custom.Name);
- Builder << ", \"Data\": ";
- AppendBase64String(Custom.Data);
- Builder << " }";
- break;
- }
- default:
- ZEN_ASSERT_FORMAT(false, "invalid field type: {}", uint8_t(Accessor.GetType()));
- break;
- }
-
- NeedsComma = true;
- NeedsNewLine = true;
- }
-
-private:
- void WriteOptionalComma()
- {
- if (NeedsComma)
- {
- NeedsComma = false;
- Builder << ',';
- }
- }
-
- void WriteOptionalNewLine()
- {
- if (NeedsNewLine)
- {
- NeedsNewLine = false;
- Builder << NewLineAndIndent;
- }
- }
-
- void AppendQuotedString(std::u8string_view Value)
- {
- using namespace std::literals;
-
- const AsciiSet EscapeSet(
- "\\\"\b\f\n\r\t"
- "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
- "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f");
-
- Builder << '\"';
- while (!Value.empty())
- {
- std::u8string_view Verbatim = AsciiSet::FindPrefixWithout(Value, EscapeSet);
- Builder << Verbatim;
-
- Value = Value.substr(Verbatim.size());
-
- std::u8string_view Escape = AsciiSet::FindPrefixWith(Value, EscapeSet);
- for (char Char : Escape)
- {
- switch (Char)
- {
- case '\\':
- Builder << "\\\\"sv;
- break;
- case '\"':
- Builder << "\\\""sv;
- break;
- case '\b':
- Builder << "\\b"sv;
- break;
- case '\f':
- Builder << "\\f"sv;
- break;
- case '\n':
- Builder << "\\n"sv;
- break;
- case '\r':
- Builder << "\\r"sv;
- break;
- case '\t':
- Builder << "\\t"sv;
- break;
- default:
- Builder << Char;
- break;
- }
- }
- Value = Value.substr(Escape.size());
- }
- Builder << '\"';
- }
-
- void AppendBase64String(MemoryView Value)
- {
- Builder << '"';
- ZEN_ASSERT(Value.GetSize() <= 512 * 1024 * 1024);
- const uint32_t EncodedSize = Base64::GetEncodedDataSize(uint32_t(Value.GetSize()));
- const size_t EncodedIndex = Builder.AddUninitialized(size_t(EncodedSize));
- Base64::Encode(static_cast<const uint8_t*>(Value.GetData()), uint32_t(Value.GetSize()), Builder.Data() + EncodedIndex);
- }
-
-private:
- StringBuilderBase& Builder;
- ExtendableStringBuilder<32> NewLineAndIndent;
- bool NeedsComma{false};
- bool NeedsNewLine{false};
-};
-
-void
-CompactBinaryToJson(const CbObjectView& Object, StringBuilderBase& Builder)
-{
- CbJsonWriter Writer(Builder);
- Writer.WriteField(Object.AsFieldView());
-}
-
-void
-CompactBinaryToJson(const CbArrayView& Array, StringBuilderBase& Builder)
-{
- CbJsonWriter Writer(Builder);
- Writer.WriteField(Array.AsFieldView());
-}
-
-void
-CompactBinaryToJson(MemoryView Data, StringBuilderBase& InBuilder)
-{
- std::vector<CbFieldView> Fields = ReadCompactBinaryStream(Data);
- CbJsonWriter Writer(InBuilder);
- if (!Fields.empty())
- {
- if (Fields.size() == 1)
- {
- Writer.WriteField(Fields[0]);
- return;
- }
- bool UseTopLevelObject = Fields[0].HasName();
- if (UseTopLevelObject)
- {
- Writer.BeginObject();
- }
- else
- {
- Writer.BeginArray();
- }
- for (const CbFieldView& Field : Fields)
- {
- Writer.WriteField(Field);
- }
- if (UseTopLevelObject)
- {
- Writer.EndObject();
- }
- else
- {
- Writer.EndArray();
- }
- }
-}
-
std::vector<CbFieldView>
ReadCompactBinaryStream(MemoryView Data)
{
@@ -1823,225 +1486,6 @@ ReadCompactBinaryStream(MemoryView Data)
//////////////////////////////////////////////////////////////////////////
-class CbJsonReader
-{
-public:
- static CbFieldIterator Read(std::string_view JsonText, std::string& Error)
- {
- using namespace json11;
-
- const Json Json = Json::parse(std::string(JsonText), Error);
-
- if (Error.empty())
- {
- CbWriter Writer;
- if (ReadField(Writer, Json, std::string_view(), Error))
- {
- return Writer.Save();
- }
- }
-
- return CbFieldIterator();
- }
-
-private:
- static bool ReadField(CbWriter& Writer, const json11::Json& Json, const std::string_view FieldName, std::string& Error)
- {
- using namespace json11;
-
- switch (Json.type())
- {
- case Json::Type::OBJECT:
- {
- if (FieldName.empty())
- {
- Writer.BeginObject();
- }
- else
- {
- Writer.BeginObject(FieldName);
- }
-
- for (const auto& Kv : Json.object_items())
- {
- const std::string& Name = Kv.first;
- const json11::Json& Item = Kv.second;
-
- if (ReadField(Writer, Item, Name, Error) == false)
- {
- return false;
- }
- }
-
- Writer.EndObject();
- }
- break;
- case Json::Type::ARRAY:
- {
- if (FieldName.empty())
- {
- Writer.BeginArray();
- }
- else
- {
- Writer.BeginArray(FieldName);
- }
-
- for (const json11::Json& Item : Json.array_items())
- {
- if (ReadField(Writer, Item, std::string_view(), Error) == false)
- {
- return false;
- }
- }
-
- Writer.EndArray();
- }
- break;
- case Json::Type::NUL:
- {
- if (FieldName.empty())
- {
- Writer.AddNull();
- }
- else
- {
- Writer.AddNull(FieldName);
- }
- }
- break;
- case Json::Type::BOOL:
- {
- if (FieldName.empty())
- {
- Writer.AddBool(Json.bool_value());
- }
- else
- {
- Writer.AddBool(FieldName, Json.bool_value());
- }
- }
- break;
- case Json::Type::NUMBER:
- {
- if (FieldName.empty())
- {
- Writer.AddFloat(Json.number_value());
- }
- else
- {
- Writer.AddFloat(FieldName, Json.number_value());
- }
- }
- break;
- case Json::Type::STRING:
- {
- Oid Id;
- if (TryParseObjectId(Json.string_value(), Id))
- {
- if (FieldName.empty())
- {
- Writer.AddObjectId(Id);
- }
- else
- {
- Writer.AddObjectId(FieldName, Id);
- }
-
- return true;
- }
-
- IoHash Hash;
- if (TryParseIoHash(Json.string_value(), Hash))
- {
- if (FieldName.empty())
- {
- Writer.AddHash(Hash);
- }
- else
- {
- Writer.AddHash(FieldName, Hash);
- }
-
- return true;
- }
-
- if (FieldName.empty())
- {
- Writer.AddString(Json.string_value());
- }
- else
- {
- Writer.AddString(FieldName, Json.string_value());
- }
- }
- break;
- default:
- break;
- }
-
- return true;
- }
-
- static constexpr AsciiSet HexCharSet = AsciiSet("0123456789abcdefABCDEF");
-
- static bool TryParseObjectId(std::string_view Str, Oid& Id)
- {
- using namespace std::literals;
-
- if (Str.size() == Oid::StringLength && AsciiSet::HasOnly(Str, HexCharSet))
- {
- Id = Oid::FromHexString(Str);
- return true;
- }
-
- if (Str.starts_with("0x"sv))
- {
- return TryParseObjectId(Str.substr(2), Id);
- }
-
- return false;
- }
-
- static bool TryParseIoHash(std::string_view Str, IoHash& Hash)
- {
- using namespace std::literals;
-
- if (Str.size() == IoHash::StringLength && AsciiSet::HasOnly(Str, HexCharSet))
- {
- Hash = IoHash::FromHexString(Str);
- return true;
- }
-
- if (Str.starts_with("0x"sv))
- {
- return TryParseIoHash(Str.substr(2), Hash);
- }
-
- return false;
- }
-};
-
-CbFieldIterator
-LoadCompactBinaryFromJson(std::string_view Json, std::string& Error)
-{
- if (Json.empty() == false)
- {
- return CbJsonReader::Read(Json, Error);
- }
-
- return CbFieldIterator();
-}
-
-CbFieldIterator
-LoadCompactBinaryFromJson(std::string_view Json)
-{
- std::string Error;
- return LoadCompactBinaryFromJson(Json, Error);
-}
-
-//////////////////////////////////////////////////////////////////////////
-
#if ZEN_WITH_TESTS
void
uson_forcelink()
@@ -2211,130 +1655,6 @@ TEST_CASE("uson.null")
}
}
-TEST_CASE("uson.json")
-{
- using namespace std::literals;
-
- SUBCASE("string")
- {
- CbObjectWriter Writer;
- Writer << "KeyOne"
- << "ValueOne";
- Writer << "KeyTwo"
- << "ValueTwo";
- CbObject Obj = Writer.Save();
-
- StringBuilder<128> Sb;
- const char* JsonText = Obj.ToJson(Sb).Data();
-
- std::string JsonError;
- json11::Json Json = json11::Json::parse(JsonText, JsonError);
-
- const std::string ValueOne = Json["KeyOne"].string_value();
- const std::string ValueTwo = Json["KeyTwo"].string_value();
-
- CHECK(JsonError.empty());
- CHECK(ValueOne == "ValueOne");
- CHECK(ValueTwo == "ValueTwo");
- }
-
- SUBCASE("number")
- {
- const float ExpectedFloatValue = 21.21f;
- const double ExpectedDoubleValue = 42.42;
-
- CbObjectWriter Writer;
- Writer << "Float" << ExpectedFloatValue;
- Writer << "Double" << ExpectedDoubleValue;
-
- CbObject Obj = Writer.Save();
-
- StringBuilder<128> Sb;
- const char* JsonText = Obj.ToJson(Sb).Data();
-
- std::string JsonError;
- json11::Json Json = json11::Json::parse(JsonText, JsonError);
-
- const float FloatValue = float(Json["Float"].number_value());
- const double DoubleValue = Json["Double"].number_value();
-
- CHECK(JsonError.empty());
- CHECK(FloatValue == Approx(ExpectedFloatValue));
- CHECK(DoubleValue == Approx(ExpectedDoubleValue));
- }
-
- SUBCASE("number.nan")
- {
- constexpr float FloatNan = std::numeric_limits<float>::quiet_NaN();
- constexpr double DoubleNan = std::numeric_limits<double>::quiet_NaN();
-
- CbObjectWriter Writer;
- Writer << "FloatNan" << FloatNan;
- Writer << "DoubleNan" << DoubleNan;
-
- CbObject Obj = Writer.Save();
-
- StringBuilder<128> Sb;
- const char* JsonText = Obj.ToJson(Sb).Data();
-
- std::string JsonError;
- json11::Json Json = json11::Json::parse(JsonText, JsonError);
-
- const double FloatValue = Json["FloatNan"].number_value();
- const double DoubleValue = Json["DoubleNan"].number_value();
-
- CHECK(JsonError.empty());
- CHECK(FloatValue == 0);
- CHECK(DoubleValue == 0);
- }
-
- SUBCASE("stream")
- {
- const auto MakeObject = [&](std::string_view Name, const std::vector<int>& Fields) -> CbObject {
- CbWriter Writer;
- Writer.SetName(Name);
- Writer.BeginObject();
- for (const auto& Field : Fields)
- {
- Writer.AddInteger(fmt::format("{}", Field), Field);
- }
- Writer.EndObject();
- return Writer.Save().AsObject();
- };
-
- std::vector<uint8_t> Buffer;
-
- auto AppendToBuffer = [&](const void* Data, size_t Count) {
- const uint8_t* AppendBytes = reinterpret_cast<const uint8_t*>(Data);
- Buffer.insert(Buffer.end(), AppendBytes, AppendBytes + Count);
- };
-
- auto Append = [&](const CbFieldView& Field) {
- Field.WriteToStream([&](const void* Data, size_t Count) {
- const uint8_t* AppendBytes = reinterpret_cast<const uint8_t*>(Data);
- Buffer.insert(Buffer.end(), AppendBytes, AppendBytes + Count);
- });
- };
-
- CbObject DataObjects[] = {MakeObject("Empty object"sv, {}),
- MakeObject("OneField object"sv, {5}),
- MakeObject("TwoField object"sv, {-5, 999}),
- MakeObject("ThreeField object"sv, {1, 2, -129})};
- for (const CbObject& Object : DataObjects)
- {
- Object.AsField().WriteToStream(AppendToBuffer);
- }
-
- ExtendableStringBuilder<128> Sb;
- CompactBinaryToJson(MemoryView(Buffer.data(), Buffer.size()), Sb);
- std::string JsonText = Sb.ToString().c_str();
- std::string JsonError;
- json11::Json Json = json11::Json::parse(JsonText, JsonError);
- std::string ParsedJsonString = Json.dump();
- CHECK(!ParsedJsonString.empty());
- }
-}
-
TEST_CASE("uson.datetime")
{
using namespace std::literals;
@@ -2362,107 +1682,6 @@ TEST_CASE("uson.datetime")
}
}
-TEST_CASE("json.uson")
-{
- using namespace std::literals;
- using namespace json11;
-
- SUBCASE("empty")
- {
- CbFieldIterator It = LoadCompactBinaryFromJson(""sv);
- CHECK(It.HasValue() == false);
- }
-
- SUBCASE("object")
- {
- const Json JsonObject = Json::object{{"Null", nullptr},
- {"String", "Value1"},
- {"Bool", true},
- {"Number", 46.2},
- {"Array", Json::array{1, 2, 3}},
- {"Object",
- Json::object{
- {"String", "Value2"},
- }}};
-
- CbObject Cb = LoadCompactBinaryFromJson(JsonObject.dump()).AsObject();
-
- CHECK(Cb["Null"].IsNull());
- CHECK(Cb["String"].AsString() == "Value1"sv);
- CHECK(Cb["Bool"].AsBool());
- CHECK(Cb["Number"].AsDouble() == 46.2);
- CHECK(Cb["Object"].IsObject());
- CbObjectView Object = Cb["Object"].AsObjectView();
- CHECK(Object["String"].AsString() == "Value2"sv);
- }
-
- SUBCASE("array")
- {
- const Json JsonArray = Json::array{42, 43, 44};
- CbArray Cb = LoadCompactBinaryFromJson(JsonArray.dump()).AsArray();
-
- auto It = Cb.CreateIterator();
- CHECK((*It).AsDouble() == 42);
- It++;
- CHECK((*It).AsDouble() == 43);
- It++;
- CHECK((*It).AsDouble() == 44);
- }
-
- SUBCASE("objectid")
- {
- const Oid& Id = Oid::NewOid();
-
- StringBuilder<64> Sb;
- Id.ToString(Sb);
-
- Json JsonObject = Json::object{{"value", Sb.ToString()}};
- CbObject Cb = LoadCompactBinaryFromJson(JsonObject.dump()).AsObject();
-
- CHECK(Cb["value"sv].IsObjectId());
- CHECK(Cb["value"sv].AsObjectId() == Id);
-
- Sb.Reset();
- Sb << "0x";
- Id.ToString(Sb);
-
- JsonObject = Json::object{{"value", Sb.ToString()}};
- Cb = LoadCompactBinaryFromJson(JsonObject.dump()).AsObject();
-
- CHECK(Cb["value"sv].IsObjectId());
- CHECK(Cb["value"sv].AsObjectId() == Id);
- }
-
- SUBCASE("iohash")
- {
- const uint8_t Data[] = {
- 1,
- 2,
- 3,
- 4,
- 5,
- 6,
- 7,
- 8,
- 9,
- };
-
- const IoHash Hash = IoHash::HashBuffer(Data, sizeof(Data));
-
- Json JsonObject = Json::object{{"value", Hash.ToHexString()}};
- CbObject Cb = LoadCompactBinaryFromJson(JsonObject.dump()).AsObject();
-
- CHECK(Cb["value"sv].IsHash());
- CHECK(Cb["value"sv].AsHash() == Hash);
-
- JsonObject = Json::object{{"value", "0x" + Hash.ToHexString()}};
- Cb = LoadCompactBinaryFromJson(JsonObject.dump()).AsObject();
-
- CHECK(Cb["value"sv].IsHash());
- CHECK(Cb["value"sv].AsHash() == Hash);
- }
-}
-
//////////////////////////////////////////////////////////////////////////
TEST_SUITE_BEGIN("core.datetime");
diff --git a/src/zencore/compactbinaryjson.cpp b/src/zencore/compactbinaryjson.cpp
new file mode 100644
index 000000000..58a91c731
--- /dev/null
+++ b/src/zencore/compactbinaryjson.cpp
@@ -0,0 +1,805 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#include "zencore/compactbinary.h"
+#include "zencore/compactbinarybuilder.h"
+#include "zencore/compactbinaryvalue.h"
+
+#include <zencore/assertfmt.h>
+#include <zencore/base64.h>
+#include <zencore/fmtutils.h>
+#include <zencore/string.h>
+#include <zencore/testing.h>
+
+#include <fmt/format.h>
+#include <vector>
+
+ZEN_THIRD_PARTY_INCLUDES_START
+#include <json11.hpp>
+ZEN_THIRD_PARTY_INCLUDES_END
+
+namespace zen {
+
+class CbJsonWriter
+{
+public:
+ explicit CbJsonWriter(StringBuilderBase& InBuilder) : Builder(InBuilder) { NewLineAndIndent << LINE_TERMINATOR_ANSI; }
+
+ void BeginObject()
+ {
+ Builder << '{';
+ NewLineAndIndent << '\t';
+ NeedsNewLine = true;
+ }
+
+ void EndObject()
+ {
+ NewLineAndIndent.RemoveSuffix(1);
+ if (NeedsComma)
+ {
+ WriteOptionalNewLine();
+ }
+ Builder << '}';
+ }
+
+ void BeginArray()
+ {
+ Builder << '[';
+ NewLineAndIndent << '\t';
+ NeedsNewLine = true;
+ }
+
+ void EndArray()
+ {
+ NewLineAndIndent.RemoveSuffix(1);
+ if (NeedsComma)
+ {
+ WriteOptionalNewLine();
+ }
+ Builder << ']';
+ }
+
+ void WriteField(CbFieldView Field)
+ {
+ using namespace std::literals;
+
+ WriteOptionalComma();
+ WriteOptionalNewLine();
+
+ if (std::u8string_view Name = Field.GetU8Name(); !Name.empty())
+ {
+ AppendQuotedString(Name);
+ Builder << ": "sv;
+ }
+
+ switch (CbValue Accessor = Field.GetValue(); Accessor.GetType())
+ {
+ case CbFieldType::Null:
+ Builder << "null"sv;
+ break;
+ case CbFieldType::Object:
+ case CbFieldType::UniformObject:
+ {
+ BeginObject();
+ for (CbFieldView It : Field)
+ {
+ WriteField(It);
+ }
+ EndObject();
+ }
+ break;
+ case CbFieldType::Array:
+ case CbFieldType::UniformArray:
+ {
+ BeginArray();
+ for (CbFieldView It : Field)
+ {
+ WriteField(It);
+ }
+ EndArray();
+ }
+ break;
+ case CbFieldType::Binary:
+ AppendBase64String(Accessor.AsBinary());
+ break;
+ case CbFieldType::String:
+ AppendQuotedString(Accessor.AsU8String());
+ break;
+ case CbFieldType::IntegerPositive:
+ Builder << Accessor.AsIntegerPositive();
+ break;
+ case CbFieldType::IntegerNegative:
+ Builder << Accessor.AsIntegerNegative();
+ break;
+ case CbFieldType::Float32:
+ {
+ const float Value = Accessor.AsFloat32();
+ if (std::isfinite(Value))
+ {
+ Builder.Append(fmt::format("{:.9g}", Value));
+ }
+ else
+ {
+ Builder << "null"sv;
+ }
+ }
+ break;
+ case CbFieldType::Float64:
+ {
+ const double Value = Accessor.AsFloat64();
+ if (std::isfinite(Value))
+ {
+ Builder.Append(fmt::format("{:.17g}", Value));
+ }
+ else
+ {
+ Builder << "null"sv;
+ }
+ }
+ break;
+ case CbFieldType::BoolFalse:
+ Builder << "false"sv;
+ break;
+ case CbFieldType::BoolTrue:
+ Builder << "true"sv;
+ break;
+ case CbFieldType::ObjectAttachment:
+ case CbFieldType::BinaryAttachment:
+ {
+ Builder << '"';
+ Accessor.AsAttachment().ToHexString(Builder);
+ Builder << '"';
+ }
+ break;
+ case CbFieldType::Hash:
+ {
+ Builder << '"';
+ Accessor.AsHash().ToHexString(Builder);
+ Builder << '"';
+ }
+ break;
+ case CbFieldType::Uuid:
+ {
+ Builder << '"';
+ Accessor.AsUuid().ToString(Builder);
+ Builder << '"';
+ }
+ break;
+ case CbFieldType::DateTime:
+ Builder << '"' << DateTime(Accessor.AsDateTimeTicks()).ToIso8601() << '"';
+ break;
+ case CbFieldType::TimeSpan:
+ {
+ const TimeSpan Span(Accessor.AsTimeSpanTicks());
+ if (Span.GetDays() == 0)
+ {
+ Builder << '"' << Span.ToString("%h:%m:%s.%n") << '"';
+ }
+ else
+ {
+ Builder << '"' << Span.ToString("%d.%h:%m:%s.%n") << '"';
+ }
+ break;
+ }
+ case CbFieldType::ObjectId:
+ Builder << '"';
+ Accessor.AsObjectId().ToString(Builder);
+ Builder << '"';
+ break;
+ case CbFieldType::CustomById:
+ {
+ CbCustomById Custom = Accessor.AsCustomById();
+ Builder << "{ \"Id\": ";
+ Builder << Custom.Id;
+ Builder << ", \"Data\": ";
+ AppendBase64String(Custom.Data);
+ Builder << " }";
+ break;
+ }
+ case CbFieldType::CustomByName:
+ {
+ CbCustomByName Custom = Accessor.AsCustomByName();
+ Builder << "{ \"Name\": ";
+ AppendQuotedString(Custom.Name);
+ Builder << ", \"Data\": ";
+ AppendBase64String(Custom.Data);
+ Builder << " }";
+ break;
+ }
+ default:
+ ZEN_ASSERT_FORMAT(false, "invalid field type: {}", uint8_t(Accessor.GetType()));
+ break;
+ }
+
+ NeedsComma = true;
+ NeedsNewLine = true;
+ }
+
+private:
+ void WriteOptionalComma()
+ {
+ if (NeedsComma)
+ {
+ NeedsComma = false;
+ Builder << ',';
+ }
+ }
+
+ void WriteOptionalNewLine()
+ {
+ if (NeedsNewLine)
+ {
+ NeedsNewLine = false;
+ Builder << NewLineAndIndent;
+ }
+ }
+
+ void AppendQuotedString(std::u8string_view Value)
+ {
+ using namespace std::literals;
+
+ const AsciiSet EscapeSet(
+ "\\\"\b\f\n\r\t"
+ "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f"
+ "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f");
+
+ Builder << '\"';
+ while (!Value.empty())
+ {
+ std::u8string_view Verbatim = AsciiSet::FindPrefixWithout(Value, EscapeSet);
+ Builder << Verbatim;
+
+ Value = Value.substr(Verbatim.size());
+
+ std::u8string_view Escape = AsciiSet::FindPrefixWith(Value, EscapeSet);
+ for (char Char : Escape)
+ {
+ switch (Char)
+ {
+ case '\\':
+ Builder << "\\\\"sv;
+ break;
+ case '\"':
+ Builder << "\\\""sv;
+ break;
+ case '\b':
+ Builder << "\\b"sv;
+ break;
+ case '\f':
+ Builder << "\\f"sv;
+ break;
+ case '\n':
+ Builder << "\\n"sv;
+ break;
+ case '\r':
+ Builder << "\\r"sv;
+ break;
+ case '\t':
+ Builder << "\\t"sv;
+ break;
+ default:
+ Builder << Char;
+ break;
+ }
+ }
+ Value = Value.substr(Escape.size());
+ }
+ Builder << '\"';
+ }
+
+ void AppendBase64String(MemoryView Value)
+ {
+ Builder << '"';
+ ZEN_ASSERT(Value.GetSize() <= 512 * 1024 * 1024);
+ const uint32_t EncodedSize = Base64::GetEncodedDataSize(uint32_t(Value.GetSize()));
+ const size_t EncodedIndex = Builder.AddUninitialized(size_t(EncodedSize));
+ Base64::Encode(static_cast<const uint8_t*>(Value.GetData()), uint32_t(Value.GetSize()), Builder.Data() + EncodedIndex);
+ }
+
+private:
+ StringBuilderBase& Builder;
+ ExtendableStringBuilder<32> NewLineAndIndent;
+ bool NeedsComma{false};
+ bool NeedsNewLine{false};
+};
+
+void
+CompactBinaryToJson(const CbObjectView& Object, StringBuilderBase& Builder)
+{
+ CbJsonWriter Writer(Builder);
+ Writer.WriteField(Object.AsFieldView());
+}
+
+void
+CompactBinaryToJson(const CbArrayView& Array, StringBuilderBase& Builder)
+{
+ CbJsonWriter Writer(Builder);
+ Writer.WriteField(Array.AsFieldView());
+}
+
+void
+CompactBinaryToJson(MemoryView Data, StringBuilderBase& InBuilder)
+{
+ std::vector<CbFieldView> Fields = ReadCompactBinaryStream(Data);
+ CbJsonWriter Writer(InBuilder);
+ if (!Fields.empty())
+ {
+ if (Fields.size() == 1)
+ {
+ Writer.WriteField(Fields[0]);
+ return;
+ }
+ bool UseTopLevelObject = Fields[0].HasName();
+ if (UseTopLevelObject)
+ {
+ Writer.BeginObject();
+ }
+ else
+ {
+ Writer.BeginArray();
+ }
+ for (const CbFieldView& Field : Fields)
+ {
+ Writer.WriteField(Field);
+ }
+ if (UseTopLevelObject)
+ {
+ Writer.EndObject();
+ }
+ else
+ {
+ Writer.EndArray();
+ }
+ }
+}
+
+class CbJsonReader
+{
+public:
+ static CbFieldIterator Read(std::string_view JsonText, std::string& Error)
+ {
+ using namespace json11;
+
+ const Json Json = Json::parse(std::string(JsonText), Error);
+
+ if (Error.empty())
+ {
+ CbWriter Writer;
+ if (ReadField(Writer, Json, std::string_view(), Error))
+ {
+ return Writer.Save();
+ }
+ }
+
+ return CbFieldIterator();
+ }
+
+private:
+ static bool ReadField(CbWriter& Writer, const json11::Json& Json, const std::string_view FieldName, std::string& Error)
+ {
+ using namespace json11;
+
+ switch (Json.type())
+ {
+ case Json::Type::OBJECT:
+ {
+ if (FieldName.empty())
+ {
+ Writer.BeginObject();
+ }
+ else
+ {
+ Writer.BeginObject(FieldName);
+ }
+
+ for (const auto& Kv : Json.object_items())
+ {
+ const std::string& Name = Kv.first;
+ const json11::Json& Item = Kv.second;
+
+ if (ReadField(Writer, Item, Name, Error) == false)
+ {
+ return false;
+ }
+ }
+
+ Writer.EndObject();
+ }
+ break;
+ case Json::Type::ARRAY:
+ {
+ if (FieldName.empty())
+ {
+ Writer.BeginArray();
+ }
+ else
+ {
+ Writer.BeginArray(FieldName);
+ }
+
+ for (const json11::Json& Item : Json.array_items())
+ {
+ if (ReadField(Writer, Item, std::string_view(), Error) == false)
+ {
+ return false;
+ }
+ }
+
+ Writer.EndArray();
+ }
+ break;
+ case Json::Type::NUL:
+ {
+ if (FieldName.empty())
+ {
+ Writer.AddNull();
+ }
+ else
+ {
+ Writer.AddNull(FieldName);
+ }
+ }
+ break;
+ case Json::Type::BOOL:
+ {
+ if (FieldName.empty())
+ {
+ Writer.AddBool(Json.bool_value());
+ }
+ else
+ {
+ Writer.AddBool(FieldName, Json.bool_value());
+ }
+ }
+ break;
+ case Json::Type::NUMBER:
+ {
+ if (FieldName.empty())
+ {
+ Writer.AddFloat(Json.number_value());
+ }
+ else
+ {
+ Writer.AddFloat(FieldName, Json.number_value());
+ }
+ }
+ break;
+ case Json::Type::STRING:
+ {
+ Oid Id;
+ if (TryParseObjectId(Json.string_value(), Id))
+ {
+ if (FieldName.empty())
+ {
+ Writer.AddObjectId(Id);
+ }
+ else
+ {
+ Writer.AddObjectId(FieldName, Id);
+ }
+
+ return true;
+ }
+
+ IoHash Hash;
+ if (TryParseIoHash(Json.string_value(), Hash))
+ {
+ if (FieldName.empty())
+ {
+ Writer.AddHash(Hash);
+ }
+ else
+ {
+ Writer.AddHash(FieldName, Hash);
+ }
+
+ return true;
+ }
+
+ if (FieldName.empty())
+ {
+ Writer.AddString(Json.string_value());
+ }
+ else
+ {
+ Writer.AddString(FieldName, Json.string_value());
+ }
+ }
+ break;
+ default:
+ break;
+ }
+
+ return true;
+ }
+
+ static constexpr AsciiSet HexCharSet = AsciiSet("0123456789abcdefABCDEF");
+
+ static bool TryParseObjectId(std::string_view Str, Oid& Id)
+ {
+ using namespace std::literals;
+
+ if (Str.size() == Oid::StringLength && AsciiSet::HasOnly(Str, HexCharSet))
+ {
+ Id = Oid::FromHexString(Str);
+ return true;
+ }
+
+ if (Str.starts_with("0x"sv))
+ {
+ return TryParseObjectId(Str.substr(2), Id);
+ }
+
+ return false;
+ }
+
+ static bool TryParseIoHash(std::string_view Str, IoHash& Hash)
+ {
+ using namespace std::literals;
+
+ if (Str.size() == IoHash::StringLength && AsciiSet::HasOnly(Str, HexCharSet))
+ {
+ Hash = IoHash::FromHexString(Str);
+ return true;
+ }
+
+ if (Str.starts_with("0x"sv))
+ {
+ return TryParseIoHash(Str.substr(2), Hash);
+ }
+
+ return false;
+ }
+};
+
+CbFieldIterator
+LoadCompactBinaryFromJson(std::string_view Json, std::string& Error)
+{
+ if (Json.empty() == false)
+ {
+ return CbJsonReader::Read(Json, Error);
+ }
+
+ return CbFieldIterator();
+}
+
+CbFieldIterator
+LoadCompactBinaryFromJson(std::string_view Json)
+{
+ std::string Error;
+ return LoadCompactBinaryFromJson(Json, Error);
+}
+
+#if ZEN_WITH_TESTS
+void
+cbjson_forcelink()
+{
+}
+
+TEST_CASE("uson.json")
+{
+ using namespace std::literals;
+
+ SUBCASE("string")
+ {
+ CbObjectWriter Writer;
+ Writer << "KeyOne"
+ << "ValueOne";
+ Writer << "KeyTwo"
+ << "ValueTwo";
+ CbObject Obj = Writer.Save();
+
+ StringBuilder<128> Sb;
+ const char* JsonText = Obj.ToJson(Sb).Data();
+
+ std::string JsonError;
+ json11::Json Json = json11::Json::parse(JsonText, JsonError);
+
+ const std::string ValueOne = Json["KeyOne"].string_value();
+ const std::string ValueTwo = Json["KeyTwo"].string_value();
+
+ CHECK(JsonError.empty());
+ CHECK(ValueOne == "ValueOne");
+ CHECK(ValueTwo == "ValueTwo");
+ }
+
+ SUBCASE("number")
+ {
+ const float ExpectedFloatValue = 21.21f;
+ const double ExpectedDoubleValue = 42.42;
+
+ CbObjectWriter Writer;
+ Writer << "Float" << ExpectedFloatValue;
+ Writer << "Double" << ExpectedDoubleValue;
+
+ CbObject Obj = Writer.Save();
+
+ StringBuilder<128> Sb;
+ const char* JsonText = Obj.ToJson(Sb).Data();
+
+ std::string JsonError;
+ json11::Json Json = json11::Json::parse(JsonText, JsonError);
+
+ const float FloatValue = float(Json["Float"].number_value());
+ const double DoubleValue = Json["Double"].number_value();
+
+ CHECK(JsonError.empty());
+ CHECK(FloatValue == Approx(ExpectedFloatValue));
+ CHECK(DoubleValue == Approx(ExpectedDoubleValue));
+ }
+
+ SUBCASE("number.nan")
+ {
+ constexpr float FloatNan = std::numeric_limits<float>::quiet_NaN();
+ constexpr double DoubleNan = std::numeric_limits<double>::quiet_NaN();
+
+ CbObjectWriter Writer;
+ Writer << "FloatNan" << FloatNan;
+ Writer << "DoubleNan" << DoubleNan;
+
+ CbObject Obj = Writer.Save();
+
+ StringBuilder<128> Sb;
+ const char* JsonText = Obj.ToJson(Sb).Data();
+
+ std::string JsonError;
+ json11::Json Json = json11::Json::parse(JsonText, JsonError);
+
+ const double FloatValue = Json["FloatNan"].number_value();
+ const double DoubleValue = Json["DoubleNan"].number_value();
+
+ CHECK(JsonError.empty());
+ CHECK(FloatValue == 0);
+ CHECK(DoubleValue == 0);
+ }
+
+ SUBCASE("stream")
+ {
+ const auto MakeObject = [&](std::string_view Name, const std::vector<int>& Fields) -> CbObject {
+ CbWriter Writer;
+ Writer.SetName(Name);
+ Writer.BeginObject();
+ for (const auto& Field : Fields)
+ {
+ Writer.AddInteger(fmt::format("{}", Field), Field);
+ }
+ Writer.EndObject();
+ return Writer.Save().AsObject();
+ };
+
+ std::vector<uint8_t> Buffer;
+
+ auto AppendToBuffer = [&](const void* Data, size_t Count) {
+ const uint8_t* AppendBytes = reinterpret_cast<const uint8_t*>(Data);
+ Buffer.insert(Buffer.end(), AppendBytes, AppendBytes + Count);
+ };
+
+ auto Append = [&](const CbFieldView& Field) {
+ Field.WriteToStream([&](const void* Data, size_t Count) {
+ const uint8_t* AppendBytes = reinterpret_cast<const uint8_t*>(Data);
+ Buffer.insert(Buffer.end(), AppendBytes, AppendBytes + Count);
+ });
+ };
+
+ CbObject DataObjects[] = {MakeObject("Empty object"sv, {}),
+ MakeObject("OneField object"sv, {5}),
+ MakeObject("TwoField object"sv, {-5, 999}),
+ MakeObject("ThreeField object"sv, {1, 2, -129})};
+ for (const CbObject& Object : DataObjects)
+ {
+ Object.AsField().WriteToStream(AppendToBuffer);
+ }
+
+ ExtendableStringBuilder<128> Sb;
+ CompactBinaryToJson(MemoryView(Buffer.data(), Buffer.size()), Sb);
+ std::string JsonText = Sb.ToString().c_str();
+ std::string JsonError;
+ json11::Json Json = json11::Json::parse(JsonText, JsonError);
+ std::string ParsedJsonString = Json.dump();
+ CHECK(!ParsedJsonString.empty());
+ }
+}
+
+TEST_CASE("json.uson")
+{
+ using namespace std::literals;
+ using namespace json11;
+
+ SUBCASE("empty")
+ {
+ CbFieldIterator It = LoadCompactBinaryFromJson(""sv);
+ CHECK(It.HasValue() == false);
+ }
+
+ SUBCASE("object")
+ {
+ const Json JsonObject = Json::object{{"Null", nullptr},
+ {"String", "Value1"},
+ {"Bool", true},
+ {"Number", 46.2},
+ {"Array", Json::array{1, 2, 3}},
+ {"Object",
+ Json::object{
+ {"String", "Value2"},
+ }}};
+
+ CbObject Cb = LoadCompactBinaryFromJson(JsonObject.dump()).AsObject();
+
+ CHECK(Cb["Null"].IsNull());
+ CHECK(Cb["String"].AsString() == "Value1"sv);
+ CHECK(Cb["Bool"].AsBool());
+ CHECK(Cb["Number"].AsDouble() == 46.2);
+ CHECK(Cb["Object"].IsObject());
+ CbObjectView Object = Cb["Object"].AsObjectView();
+ CHECK(Object["String"].AsString() == "Value2"sv);
+ }
+
+ SUBCASE("array")
+ {
+ const Json JsonArray = Json::array{42, 43, 44};
+ CbArray Cb = LoadCompactBinaryFromJson(JsonArray.dump()).AsArray();
+
+ auto It = Cb.CreateIterator();
+ CHECK((*It).AsDouble() == 42);
+ It++;
+ CHECK((*It).AsDouble() == 43);
+ It++;
+ CHECK((*It).AsDouble() == 44);
+ }
+
+ SUBCASE("objectid")
+ {
+ const Oid& Id = Oid::NewOid();
+
+ StringBuilder<64> Sb;
+ Id.ToString(Sb);
+
+ Json JsonObject = Json::object{{"value", Sb.ToString()}};
+ CbObject Cb = LoadCompactBinaryFromJson(JsonObject.dump()).AsObject();
+
+ CHECK(Cb["value"sv].IsObjectId());
+ CHECK(Cb["value"sv].AsObjectId() == Id);
+
+ Sb.Reset();
+ Sb << "0x";
+ Id.ToString(Sb);
+
+ JsonObject = Json::object{{"value", Sb.ToString()}};
+ Cb = LoadCompactBinaryFromJson(JsonObject.dump()).AsObject();
+
+ CHECK(Cb["value"sv].IsObjectId());
+ CHECK(Cb["value"sv].AsObjectId() == Id);
+ }
+
+ SUBCASE("iohash")
+ {
+ const uint8_t Data[] = {
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9,
+ };
+
+ const IoHash Hash = IoHash::HashBuffer(Data, sizeof(Data));
+
+ Json JsonObject = Json::object{{"value", Hash.ToHexString()}};
+ CbObject Cb = LoadCompactBinaryFromJson(JsonObject.dump()).AsObject();
+
+ CHECK(Cb["value"sv].IsHash());
+ CHECK(Cb["value"sv].AsHash() == Hash);
+
+ JsonObject = Json::object{{"value", "0x" + Hash.ToHexString()}};
+ Cb = LoadCompactBinaryFromJson(JsonObject.dump()).AsObject();
+
+ CHECK(Cb["value"sv].IsHash());
+ CHECK(Cb["value"sv].AsHash() == Hash);
+ }
+}
+
+#endif // ZEN_WITH_TESTS
+
+} // namespace zen
diff --git a/src/zencore/compactbinaryyaml.cpp b/src/zencore/compactbinaryyaml.cpp
new file mode 100644
index 000000000..3a9705684
--- /dev/null
+++ b/src/zencore/compactbinaryyaml.cpp
@@ -0,0 +1,352 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#include "zencore/compactbinary.h"
+#include "zencore/compactbinarybuilder.h"
+#include "zencore/compactbinaryvalue.h"
+
+#include <zencore/assertfmt.h>
+#include <zencore/base64.h>
+#include <zencore/fmtutils.h>
+#include <zencore/string.h>
+#include <zencore/testing.h>
+
+#include <fmt/format.h>
+#include <string_view>
+#include <vector>
+
+ZEN_THIRD_PARTY_INCLUDES_START
+#include <ryml/ryml.hpp>
+#include <ryml/ryml_std.hpp>
+ZEN_THIRD_PARTY_INCLUDES_END
+
+namespace zen {
+
+//////////////////////////////////////////////////////////////////////////
+
+class CbYamlWriter
+{
+public:
+ explicit CbYamlWriter(StringBuilderBase& InBuilder) : m_StrBuilder(InBuilder) { m_NodeStack.push_back(m_Tree.rootref()); }
+
+ void WriteField(CbFieldView Field)
+ {
+ ryml::NodeRef Node;
+
+ if (m_IsFirst)
+ {
+ Node = Top();
+
+ m_IsFirst = false;
+ }
+ else
+ {
+ Node = Top().append_child();
+ }
+
+ if (std::u8string_view Name = Field.GetU8Name(); !Name.empty())
+ {
+ Node.set_key_serialized(ryml::csubstr((const char*)Name.data(), Name.size()));
+ }
+
+ switch (CbValue Accessor = Field.GetValue(); Accessor.GetType())
+ {
+ case CbFieldType::Null:
+ Node.set_val("null");
+ break;
+ case CbFieldType::Object:
+ case CbFieldType::UniformObject:
+ Node |= ryml::MAP;
+ m_NodeStack.push_back(Node);
+ for (CbFieldView It : Field)
+ {
+ WriteField(It);
+ }
+ m_NodeStack.pop_back();
+ break;
+ case CbFieldType::Array:
+ case CbFieldType::UniformArray:
+ Node |= ryml::SEQ;
+ m_NodeStack.push_back(Node);
+ for (CbFieldView It : Field)
+ {
+ WriteField(It);
+ }
+ m_NodeStack.pop_back();
+ break;
+ case CbFieldType::Binary:
+ {
+ ExtendableStringBuilder<256> Builder;
+ const MemoryView Value = Accessor.AsBinary();
+ ZEN_ASSERT(Value.GetSize() <= 512 * 1024 * 1024);
+ const uint32_t EncodedSize = Base64::GetEncodedDataSize(uint32_t(Value.GetSize()));
+ const size_t EncodedIndex = Builder.AddUninitialized(size_t(EncodedSize));
+ Base64::Encode(static_cast<const uint8_t*>(Value.GetData()), uint32_t(Value.GetSize()), Builder.Data() + EncodedIndex);
+
+ Node.set_key_serialized(Builder.c_str());
+ }
+ break;
+ case CbFieldType::String:
+ {
+ const std::u8string_view U8String = Accessor.AsU8String();
+ Node.set_val(ryml::csubstr((const char*)U8String.data(), U8String.size()));
+ }
+ break;
+ case CbFieldType::IntegerPositive:
+ Node << Accessor.AsIntegerPositive();
+ break;
+ case CbFieldType::IntegerNegative:
+ Node << Accessor.AsIntegerNegative();
+ break;
+ case CbFieldType::Float32:
+ if (const float Value = Accessor.AsFloat32(); std::isfinite(Value))
+ {
+ Node << Value;
+ }
+ else
+ {
+ Node << "null";
+ }
+ break;
+ case CbFieldType::Float64:
+ if (const double Value = Accessor.AsFloat64(); std::isfinite(Value))
+ {
+ Node << Value;
+ }
+ else
+ {
+ Node << "null";
+ }
+ break;
+ case CbFieldType::BoolFalse:
+ Node << "false";
+ break;
+ case CbFieldType::BoolTrue:
+ Node << "true";
+ break;
+ case CbFieldType::ObjectAttachment:
+ case CbFieldType::BinaryAttachment:
+ Node << Accessor.AsAttachment().ToHexString();
+ break;
+ case CbFieldType::Hash:
+ Node << Accessor.AsHash().ToHexString();
+ break;
+ case CbFieldType::Uuid:
+ Node << fmt::format("{}", Accessor.AsUuid());
+ break;
+ case CbFieldType::DateTime:
+ Node << DateTime(Accessor.AsDateTimeTicks()).ToIso8601();
+ break;
+ case CbFieldType::TimeSpan:
+ if (const TimeSpan Span(Accessor.AsTimeSpanTicks()); Span.GetDays() == 0)
+ {
+ Node << Span.ToString("%h:%m:%s.%n");
+ }
+ else
+ {
+ Node << Span.ToString("%d.%h:%m:%s.%n");
+ }
+ break;
+ case CbFieldType::ObjectId:
+ Node << fmt::format("{}", Accessor.AsObjectId());
+ break;
+ case CbFieldType::CustomById:
+ {
+ CbCustomById Custom = Accessor.AsCustomById();
+
+ Node |= ryml::MAP;
+
+ ryml::NodeRef IdNode = Node.append_child();
+ IdNode.set_key("Id");
+ IdNode.set_val_serialized(fmt::format("{}", Custom.Id));
+
+ ryml::NodeRef DataNode = Node.append_child();
+ DataNode.set_key("Data");
+
+ ExtendableStringBuilder<256> Builder;
+ const MemoryView& Value = Custom.Data;
+ const uint32_t EncodedSize = Base64::GetEncodedDataSize(uint32_t(Value.GetSize()));
+ const size_t EncodedIndex = Builder.AddUninitialized(size_t(EncodedSize));
+ Base64::Encode(static_cast<const uint8_t*>(Value.GetData()), uint32_t(Value.GetSize()), Builder.Data() + EncodedIndex);
+
+ DataNode.set_val_serialized(Builder.c_str());
+ }
+ break;
+ case CbFieldType::CustomByName:
+ {
+ CbCustomByName Custom = Accessor.AsCustomByName();
+
+ Node |= ryml::MAP;
+
+ ryml::NodeRef NameNode = Node.append_child();
+ NameNode.set_key("Name");
+ std::string_view Name = std::string_view((const char*)Custom.Name.data(), Custom.Name.size());
+ NameNode.set_val_serialized(std::string(Name));
+
+ ryml::NodeRef DataNode = Node.append_child();
+ DataNode.set_key("Data");
+
+ ExtendableStringBuilder<256> Builder;
+ const MemoryView& Value = Custom.Data;
+ const uint32_t EncodedSize = Base64::GetEncodedDataSize(uint32_t(Value.GetSize()));
+ const size_t EncodedIndex = Builder.AddUninitialized(size_t(EncodedSize));
+ Base64::Encode(static_cast<const uint8_t*>(Value.GetData()), uint32_t(Value.GetSize()), Builder.Data() + EncodedIndex);
+
+ DataNode.set_val_serialized(Builder.c_str());
+ }
+ break;
+ default:
+ ZEN_ASSERT_FORMAT(false, "invalid field type: {}", uint8_t(Accessor.GetType()));
+ break;
+ }
+
+ if (m_NodeStack.size() == 1)
+ {
+ std::string Yaml = ryml::emitrs_yaml<std::string>(m_Tree);
+ m_StrBuilder << Yaml;
+ }
+ }
+
+private:
+ StringBuilderBase& m_StrBuilder;
+ bool m_IsFirst = true;
+
+ ryml::Tree m_Tree;
+ std::vector<ryml::NodeRef> m_NodeStack;
+ ryml::NodeRef& Top() { return m_NodeStack.back(); }
+};
+
+void
+CompactBinaryToYaml(const CbObjectView& Object, StringBuilderBase& Builder)
+{
+ CbYamlWriter Writer(Builder);
+ Writer.WriteField(Object.AsFieldView());
+}
+
+void
+CompactBinaryToYaml(const CbArrayView& Array, StringBuilderBase& Builder)
+{
+ CbYamlWriter Writer(Builder);
+ Writer.WriteField(Array.AsFieldView());
+}
+
+#if ZEN_WITH_TESTS
+void
+cbyaml_forcelink()
+{
+}
+
+TEST_CASE("uson.yaml")
+{
+ using namespace std::literals;
+
+ SUBCASE("simple")
+ {
+ CbObjectWriter Writer;
+ Writer << "KeyOne"
+ << "ValueOne";
+ Writer << "KeyTwo"
+ << "ValueTwo";
+ CbObject Obj = Writer.Save();
+
+ StringBuilder<128> Sb;
+ CbYamlWriter YamlWriter(Sb);
+ YamlWriter.WriteField(Obj.AsFieldView());
+
+ CHECK_EQ(Sb.ToView(), "KeyOne: ValueOne\nKeyTwo: ValueTwo\n"sv);
+ }
+
+ SUBCASE("scalar_fields")
+ {
+ CbObjectWriter Writer;
+ Writer << "small_int"sv << 10;
+ Writer << "neg_small_int"sv << -10;
+ Writer << "small_real"sv << 10.5f;
+ Writer << "neg_small_real"sv << -10.5f;
+ Writer.AddNull("null_val"sv);
+ Writer.AddDateTimeTicks("date"sv, 622'033'000'000'000'000ull);
+ Writer.AddHash("hash"sv, IoHash::FromHexString("0011223344556677889900112233445566778899"sv));
+ Writer.AddObjectId("oid"sv, Oid::FromHexString("112233445566778899001122"sv));
+ Writer.AddTimeSpanTicks("dt"sv, 3'000'000'000'000ull);
+ Writer.AddUuid("guid"sv, Guid::FromString("E0596ADC-996A-4BA4-ACA3-A2A378AB2796"));
+ Writer.AddBool("yes"sv, true);
+ Writer.AddBool("no"sv, false);
+ CbObject Obj = Writer.Save();
+
+ ExtendableStringBuilder<128> Sb;
+ CbYamlWriter YamlWriter(Sb);
+ YamlWriter.WriteField(Obj.AsFieldView());
+
+ CHECK_EQ(Sb.ToView(),
+ "small_int: 10\n"
+ "neg_small_int: -10\n"
+ "small_real: 10.5\n"
+ "neg_small_real: -10.5\n"
+ "null_val: null\n"
+ "date: '1972-02-23T14:26:40.000Z'\n"
+ "hash: 0011223344556677889900112233445566778899\n"
+ "oid: 112233445566778899001122\n"
+ "dt: '+3.11:20:00.000000000'\n"
+ "guid: 'e0596adc-996a-4ba4-aca3-a2a378ab2796'\n"
+ "yes: true\n"
+ "no: false\n"sv);
+ }
+
+ SUBCASE("complex_fields")
+ {
+ CbObjectWriter Writer;
+ Writer.BeginObject("sub");
+ Writer.AddBool("no"sv, false);
+ Writer.BeginObject("sub");
+ Writer.AddBool("yes"sv, true);
+ Writer.BeginArray("seq");
+ Writer.AddInteger(1);
+ Writer.AddInteger(2);
+ Writer.AddInteger(3);
+ Writer.EndArray();
+ Writer.EndObject();
+ Writer.EndObject();
+ Writer.BeginArray("seq");
+ Writer.AddInteger(1);
+ Writer.AddInteger(2);
+ Writer.AddInteger(3);
+ Writer.EndArray();
+ Writer.BeginArray("mixed_seq");
+ Writer.AddInteger(1);
+ Writer.AddString("hello"sv);
+ Writer.AddFloat(44.4f);
+ Writer.BeginObject();
+ Writer.AddBool("yes"sv, true);
+ Writer.AddBool("no"sv, false);
+ Writer.EndObject();
+ Writer.EndArray();
+ CbObject Obj = Writer.Save();
+
+ ExtendableStringBuilder<128> Sb;
+ CbYamlWriter YamlWriter(Sb);
+ YamlWriter.WriteField(Obj.AsFieldView());
+
+ CHECK_EQ(Sb.ToView(),
+ R"(sub:
+ no: false
+ sub:
+ yes: true
+ seq:
+ - 1
+ - 2
+ - 3
+seq:
+ - 1
+ - 2
+ - 3
+mixed_seq:
+ - 1
+ - hello
+ - 44.4
+ - yes: true
+ no: false
+)"sv);
+ }
+}
+#endif
+
+} // namespace zen
diff --git a/src/zencore/include/zencore/compactbinary.h b/src/zencore/include/zencore/compactbinary.h
index 675e2a8d4..a8707ba2b 100644
--- a/src/zencore/include/zencore/compactbinary.h
+++ b/src/zencore/include/zencore/compactbinary.h
@@ -895,6 +895,11 @@ private:
ZENCORE_API void CompactBinaryToJson(const CbArrayView& Object, StringBuilderBase& Builder);
/**
+ * Serialize a compact binary array to YAML.
+ */
+ZENCORE_API void CompactBinaryToYaml(const CbArrayView& Object, StringBuilderBase& Builder);
+
+/**
* Array of CbField that have no names.
*
* Accessing a field of the array requires iteration. Access by index is not provided because the
@@ -974,6 +979,12 @@ public:
return Builder;
}
+ StringBuilderBase& ToYaml(StringBuilderBase& Builder) const
+ {
+ CompactBinaryToYaml(*this, Builder);
+ return Builder;
+ }
+
private:
friend inline CbFieldViewIterator begin(const CbArrayView& Array) { return Array.CreateViewIterator(); }
friend inline CbFieldViewIterator end(const CbArrayView&) { return CbFieldViewIterator(); }
@@ -986,6 +997,10 @@ private:
* Serialize a compact binary object to JSON.
*/
ZENCORE_API void CompactBinaryToJson(const CbObjectView& Object, StringBuilderBase& Builder);
+/**
+ * Serialize a compact binary object to YAML.
+ */
+ZENCORE_API void CompactBinaryToYaml(const CbObjectView& Object, StringBuilderBase& Builder);
class CbObjectView : protected CbFieldView
{
@@ -1075,6 +1090,12 @@ public:
return Builder;
}
+ StringBuilderBase& ToYaml(StringBuilderBase& Builder) const
+ {
+ CompactBinaryToYaml(*this, Builder);
+ return Builder;
+ }
+
private:
friend inline CbFieldViewIterator begin(const CbObjectView& Object) { return Object.CreateViewIterator(); }
friend inline CbFieldViewIterator end(const CbObjectView&) { return CbFieldViewIterator(); }
@@ -1497,12 +1518,19 @@ end(CbFieldView&)
}
/**
- * Serialize serialized compact binary blob to jaons. It must be 0 to n fields with including type for each field
+ * Serialize serialized compact binary blob to JSON. It must be 0 to n fields with including type for each field
*/
ZENCORE_API void CompactBinaryToJson(MemoryView Data, StringBuilderBase& InBuilder);
+/**
+ * Serialize serialized compact binary blob to YAML. It must be 0 to n fields with including type for each field
+ */
+ZENCORE_API void CompactBinaryToYaml(MemoryView Data, StringBuilderBase& InBuilder);
+
ZENCORE_API std::vector<CbFieldView> ReadCompactBinaryStream(MemoryView Data);
-void uson_forcelink(); // internal
+void uson_forcelink(); // internal
+void cbjson_forcelink(); // internal
+void cbyaml_forcelink(); // internal
} // namespace zen
diff --git a/src/zencore/xmake.lua b/src/zencore/xmake.lua
index 5420464fa..e6102679d 100644
--- a/src/zencore/xmake.lua
+++ b/src/zencore/xmake.lua
@@ -32,6 +32,8 @@ target('zencore')
add_packages(
"vcpkg::blake3",
"vcpkg::json11",
+ "vcpkg::ryml",
+ "vcpkg::c4core",
"vcpkg::mimalloc",
"vcpkg::openssl", -- required for crypto
"vcpkg::spdlog")
diff --git a/src/zencore/zencore.cpp b/src/zencore/zencore.cpp
index b80b1d280..c97f5e5ca 100644
--- a/src/zencore/zencore.cpp
+++ b/src/zencore/zencore.cpp
@@ -160,6 +160,8 @@ zencore_forcelinktests()
zen::uson_forcelink();
zen::usonbuilder_forcelink();
zen::usonpackage_forcelink();
+ zen::cbjson_forcelink();
+ zen::cbyaml_forcelink();
zen::workthreadpool_forcelink();
}
} // namespace zen
diff --git a/src/zenhttp/httpserver.cpp b/src/zenhttp/httpserver.cpp
index adb3128bf..a0d4ef3f3 100644
--- a/src/zenhttp/httpserver.cpp
+++ b/src/zenhttp/httpserver.cpp
@@ -477,6 +477,11 @@ HttpServerRequest::WriteResponse(HttpResponseCode ResponseCode, CbObject Data)
ExtendableStringBuilder<1024> Sb;
WriteResponse(ResponseCode, HttpContentType::kJSON, Data.ToJson(Sb).ToView());
}
+ else if (m_AcceptType == HttpContentType::kYAML)
+ {
+ ExtendableStringBuilder<1024> Sb;
+ WriteResponse(ResponseCode, HttpContentType::kYAML, Data.ToYaml(Sb).ToView());
+ }
else
{
SharedBuffer Buf = Data.GetBuffer();
@@ -493,6 +498,11 @@ HttpServerRequest::WriteResponse(HttpResponseCode ResponseCode, CbArray Array)
ExtendableStringBuilder<1024> Sb;
WriteResponse(ResponseCode, HttpContentType::kJSON, Array.ToJson(Sb).ToView());
}
+ else if (m_AcceptType == HttpContentType::kYAML)
+ {
+ ExtendableStringBuilder<1024> Sb;
+ WriteResponse(ResponseCode, HttpContentType::kYAML, Array.ToYaml(Sb).ToView());
+ }
else
{
SharedBuffer Buf = Array.GetBuffer();