// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Text.Json; using EpicGames.Core; namespace EpicGames.Serialization { /// /// Marks a class as supporting serialization to compact-binary, even if it does not have exposed fields. This suppresses errors /// when base class objects are empty. /// [AttributeUsage(AttributeTargets.Class)] public sealed class CbObjectAttribute : Attribute { } /// /// Attribute used to mark a property that should be serialized to compact binary /// [AttributeUsage(AttributeTargets.Property)] public sealed class CbFieldAttribute : Attribute { /// /// Name of the serialized field /// public string? Name { get; } /// /// Default constructor /// public CbFieldAttribute() { } /// /// Constructor /// /// public CbFieldAttribute(string name) { Name = name; } } /// /// Attribute used to mark that a property should not be serialized to compact binary /// [AttributeUsage(AttributeTargets.Property)] public sealed class CbIgnoreAttribute : Attribute { } /// /// Attribute used to indicate that this object is the base for a class hierarchy. Each derived class must have a [CbDiscriminator] attribute. /// [AttributeUsage(AttributeTargets.Class)] public sealed class CbPolymorphicAttribute : Attribute { } /// /// Sets the name used for discriminating between derived classes during serialization /// /// Name used to identify this class [AttributeUsage(AttributeTargets.Class)] public sealed class CbDiscriminatorAttribute(string name) : Attribute { /// /// Name used to identify this class /// public string Name { get; } = name; } /// /// Exception thrown when serializing cb objects /// public class CbException : Exception { /// public CbException(string message) : base(message) { } /// public CbException(string message, Exception inner) : base(message, inner) { } } /// /// Exception indicating that a class does not have any fields to serialize /// /// /// Constructor /// public sealed class CbEmptyClassException(Type classType) : CbException($"{classType.Name} does not have any fields marked with a [CbField] attribute. If this is intended, explicitly mark the class with a [CbObject] attribute.") { /// /// Type with missing field annotations /// public Type ClassType { get; } = classType; } /// /// Attribute-driven compact binary serializer /// public static class CbSerializer { /// /// Serialize an object /// /// Type of the object to serialize /// /// public static CbObject Serialize(Type type, object value) { CbWriter writer = new CbWriter(); CbConverter.GetConverter(type).WriteObject(writer, value); return writer.ToObject(); } /// /// Serialize an object /// /// /// /// public static CbObject Serialize(T value) { CbWriter writer = new CbWriter(); CbConverter.GetConverter().Write(writer, value); return writer.ToObject(); } /// /// Serialize an object /// /// /// /// public static byte[] SerializeToByteArray(T value) { CbWriter writer = new CbWriter(); CbConverter.GetConverter().Write(writer, value); return writer.ToByteArray(); } /// /// Serialize a property to a given writer /// /// /// /// public static void Serialize(CbWriter writer, T value) { CbConverter.GetConverter().Write(writer, value); } /// /// Serialize a named property to the given writer /// /// /// /// /// public static void Serialize(CbWriter writer, CbFieldName name, T value) { CbConverter.GetConverter().WriteNamed(writer, name, value); } /// /// Deserialize an object from a /// /// /// Type of the object to read /// public static object? Deserialize(CbField field, Type type) { return CbConverter.GetConverter(type).ReadObject(field); } /// /// Deserialize an object from a /// /// /// /// public static T Deserialize(CbField field) { return CbConverter.GetConverter().Read(field); } /// /// Deserialize an object from a /// /// /// /// public static T Deserialize(CbObject obj) => Deserialize(obj.AsField()); /// /// Deserialize an object from a block of memory /// /// /// /// public static T Deserialize(ReadOnlyMemory data) => Deserialize(new CbField(data)); } /// /// Helper to convert a json string into a cb object with conventions for how to detect types that json can not naturally encode /// public static class CbJsonReader { /// /// Converts a json object into a CbObject /// /// public static CbObject FromJson(string json) { JsonDocument document = JsonDocument.Parse(json); CbWriter writer = new CbWriter(); ReadJsonField(document.RootElement, null, writer); return writer.ToObject(); } private static void ReadJsonField(JsonElement jsonElement, string? fieldName, CbWriter writer) { switch (jsonElement.ValueKind) { case JsonValueKind.Object: { if (String.IsNullOrEmpty(fieldName)) { writer.BeginObject(); } else { writer.BeginObject(fieldName); } foreach (JsonProperty prop in jsonElement.EnumerateObject()) { ReadJsonField(prop.Value, prop.Name, writer); } writer.EndObject(); break; } case JsonValueKind.Array: { if (String.IsNullOrEmpty(fieldName)) { writer.BeginArray(); } else { writer.BeginArray(fieldName); } foreach (JsonElement prop in jsonElement.EnumerateArray()) { ReadJsonField(prop, null, writer); } writer.EndArray(); break; } case JsonValueKind.Null: { if (String.IsNullOrEmpty(fieldName)) { writer.WriteNullValue(); } else { writer.WriteNull(fieldName); } break; } case JsonValueKind.True: { if (String.IsNullOrEmpty(fieldName)) { writer.WriteBoolValue(true); } else { writer.WriteBool(fieldName, true); } break; } case JsonValueKind.False: { if (String.IsNullOrEmpty(fieldName)) { writer.WriteBoolValue(false); } else { writer.WriteBool(fieldName, false); } break; } case JsonValueKind.Number: { if (String.IsNullOrEmpty(fieldName)) { writer.WriteDoubleValue(jsonElement.GetDouble()); } else { writer.WriteDouble(fieldName, jsonElement.GetDouble()); } break; } case JsonValueKind.String: { string? value = jsonElement.GetString() ?? ""; if (CbObjectId.TryParse(value, out CbObjectId oid)) { if (String.IsNullOrEmpty(fieldName)) { writer.WriteObjectIdValue(oid); } else { writer.WriteObjectId(fieldName, oid); } return; } { if (IoHash.TryParse(value, out IoHash hash)) { if (String.IsNullOrEmpty(fieldName)) { writer.WriteHashValue(hash); } else { writer.WriteHash(fieldName, hash); } return; } } int pos = value.IndexOf(BinaryAttachmentPrefix, StringComparison.OrdinalIgnoreCase); if (pos != -1) { value = value.Substring(pos); if (IoHash.TryParse(value, out IoHash hash)) { if (String.IsNullOrEmpty(fieldName)) { writer.WriteBinaryAttachmentValue(hash); } else { writer.WriteBinaryAttachment(fieldName, hash); } return; } } pos = value.IndexOf(CompactBinaryAttachmentPrefix, StringComparison.OrdinalIgnoreCase); if (pos != -1) { value = value.Substring(pos); if (IoHash.TryParse(value, out IoHash hash)) { if (String.IsNullOrEmpty(fieldName)) { writer.WriteObjectAttachmentValue(hash); } else { writer.WriteObjectAttachment(fieldName, hash); } return; } } if (String.IsNullOrEmpty(fieldName)) { writer.WriteStringValue(value); } else { writer.WriteString(fieldName, value); } break; } case JsonValueKind.Undefined: break; default: throw new NotImplementedException(); } } private const string CompactBinaryAttachmentPrefix = "obj/"; private const string BinaryAttachmentPrefix = "bin/"; } }