using System; using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Reflection; using System.Text; namespace FlowScope.Data { public static class ReactivePropertyJsonConverter { public static string SerializeObject(object value) { return SimpleJson.Write(ToJsonValue(value)); } public static T DeserializeObject(string json) { var instance = (T)Activator.CreateInstance(typeof(T), true); PopulateObject(json, instance); return instance; } public static void PopulateObject(string json, object target) { if (target == null) { throw new ArgumentNullException(nameof(target)); } if (SimpleJson.Parse(json) is not Dictionary values) { throw new InvalidOperationException($"JSON root must be an object for {target.GetType().Name}."); } PopulateFromDictionary(values, target); } public static IReadOnlyList DeserializeArray(string json) where T : class { if (SimpleJson.Parse(json) is not List rows) { throw new InvalidOperationException("JSON root must be an array."); } var results = new List(rows.Count); foreach (var row in rows) { if (row is not Dictionary rowValues) { throw new InvalidOperationException($"JSON row must be an object for {typeof(T).Name}."); } var instance = (T)Activator.CreateInstance(typeof(T), true); PopulateFromDictionary(rowValues, instance); results.Add(instance); } return results; } internal static object ToJsonValue(object value) { if (value == null) { return null; } var type = value.GetType(); if (IsSimple(type)) { return value; } if (TryGetReactivePropertyValue(value, out var reactiveValue)) { return ToJsonValue(reactiveValue); } if (IsReactiveCollection(type) || value is IEnumerable && value is not string) { var array = new List(); foreach (var item in (IEnumerable)value) { array.Add(ToJsonValue(item)); } return array; } var result = new Dictionary(); foreach (var property in GetSerializableProperties(type)) { result[property.Name] = ToJsonValue(property.GetValue(value)); } foreach (var field in GetSerializableFields(type)) { result[field.Name] = ToJsonValue(field.GetValue(value)); } return result; } internal static object ConvertValue(object value, Type targetType, string fieldName) { if (value == null) { if (targetType.IsValueType && Nullable.GetUnderlyingType(targetType) == null) { throw new InvalidOperationException($"Field '{fieldName}' cannot be null."); } return null; } var nullableType = Nullable.GetUnderlyingType(targetType); if (nullableType != null) { return ConvertValue(value, nullableType, fieldName); } if (targetType == typeof(string)) { if (value is string text) { return text; } throw TypeMismatch(fieldName, targetType, value); } if (targetType == typeof(bool)) { if (value is bool boolValue) { return boolValue; } throw TypeMismatch(fieldName, targetType, value); } if (targetType.IsEnum) { if (value is string enumName) { return Enum.Parse(targetType, enumName); } return Enum.ToObject(targetType, Convert.ToInt32(value, CultureInfo.InvariantCulture)); } if (IsNumeric(targetType)) { if (value is string || value is bool) { throw TypeMismatch(fieldName, targetType, value); } return Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture); } if (value is Dictionary nested) { var instance = Activator.CreateInstance(targetType, true); PopulateFromDictionary(nested, instance); return instance; } throw TypeMismatch(fieldName, targetType, value); } private static void PopulateFromDictionary(IReadOnlyDictionary values, object target) { var members = GetWritableMembers(target.GetType()); foreach (var pair in values) { if (!members.TryGetValue(pair.Key, out var member)) { continue; } PopulateMember(target, member, pair.Value, pair.Key); } } private static void PopulateMember(object target, MemberInfo member, object value, string fieldName) { var memberType = GetMemberType(member); var currentValue = GetMemberValue(member, target); if (IsReactiveProperty(memberType)) { var valueProperty = memberType.GetProperty("Value", BindingFlags.Instance | BindingFlags.Public); var converted = ConvertValue(value, valueProperty.PropertyType, fieldName); valueProperty.SetValue(currentValue, converted); return; } if (IsReactiveCollection(memberType) || IsMutableCollection(memberType, currentValue)) { PopulateCollection(currentValue, memberType, value, fieldName); return; } if (CanSet(member)) { SetMemberValue(member, target, ConvertValue(value, memberType, fieldName)); } } private static void PopulateCollection(object collection, Type collectionType, object value, string fieldName) { if (value is not List rows) { throw TypeMismatch(fieldName, collectionType, value); } var itemType = GetCollectionItemType(collectionType); collectionType.GetMethod("Clear", Type.EmptyTypes)?.Invoke(collection, Array.Empty()); var addMethod = collectionType.GetMethod("Add", new[] { itemType }); if (addMethod == null) { throw new InvalidOperationException($"Field '{fieldName}' collection does not expose Add."); } foreach (var row in rows) { addMethod.Invoke(collection, new[] { ConvertValue(row, itemType, fieldName) }); } } private static Dictionary GetWritableMembers(Type type) { var members = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var property in type.GetProperties(BindingFlags.Instance | BindingFlags.Public)) { if (property.GetIndexParameters().Length == 0) { members[property.Name] = property; } } foreach (var field in type.GetFields(BindingFlags.Instance | BindingFlags.Public)) { members[field.Name] = field; } return members; } private static IEnumerable GetSerializableProperties(Type type) { foreach (var property in type.GetProperties(BindingFlags.Instance | BindingFlags.Public)) { if (property.GetIndexParameters().Length == 0 && property.CanRead) { yield return property; } } } private static IEnumerable GetSerializableFields(Type type) { return type.GetFields(BindingFlags.Instance | BindingFlags.Public); } private static Type GetMemberType(MemberInfo member) { return member switch { PropertyInfo property => property.PropertyType, FieldInfo field => field.FieldType, _ => throw new NotSupportedException(member.MemberType.ToString()) }; } private static object GetMemberValue(MemberInfo member, object target) { return member switch { PropertyInfo property => property.GetValue(target), FieldInfo field => field.GetValue(target), _ => null }; } private static void SetMemberValue(MemberInfo member, object target, object value) { switch (member) { case PropertyInfo property: property.SetValue(target, value); break; case FieldInfo field: field.SetValue(target, value); break; } } private static bool CanSet(MemberInfo member) { return member switch { PropertyInfo property => property.CanWrite, FieldInfo field => !field.IsInitOnly, _ => false }; } private static bool TryGetReactivePropertyValue(object value, out object reactiveValue) { reactiveValue = null; if (!IsReactiveProperty(value.GetType())) { return false; } reactiveValue = value.GetType().GetProperty("Value", BindingFlags.Instance | BindingFlags.Public)?.GetValue(value); return true; } private static bool IsReactiveProperty(Type type) { return type.IsGenericType && type.GetGenericTypeDefinition().FullName == "R3.ReactiveProperty`1"; } private static bool IsReactiveCollection(Type type) { return type.IsGenericType && type.GetGenericTypeDefinition().FullName == "R3.ReactiveCollection`1"; } private static bool IsMutableCollection(Type type, object value) { return value != null && type != typeof(string) && GetCollectionItemType(type) != null && type.GetMethod("Clear", Type.EmptyTypes) != null; } private static Type GetCollectionItemType(Type type) { if (type.IsArray) { return type.GetElementType(); } if (type.IsGenericType && type.GetGenericArguments().Length == 1) { return type.GetGenericArguments()[0]; } foreach (var interfaceType in type.GetInterfaces()) { if (interfaceType.IsGenericType && interfaceType.GetGenericTypeDefinition() == typeof(ICollection<>)) { return interfaceType.GetGenericArguments()[0]; } } return null; } private static bool IsSimple(Type type) { type = Nullable.GetUnderlyingType(type) ?? type; return type.IsPrimitive || type.IsEnum || type == typeof(string) || type == typeof(decimal); } private static bool IsNumeric(Type type) { type = Nullable.GetUnderlyingType(type) ?? type; return type == typeof(byte) || type == typeof(sbyte) || type == typeof(short) || type == typeof(ushort) || type == typeof(int) || type == typeof(uint) || type == typeof(long) || type == typeof(ulong) || type == typeof(float) || type == typeof(double) || type == typeof(decimal); } private static InvalidOperationException TypeMismatch(string fieldName, Type targetType, object value) { return new InvalidOperationException( $"Field '{fieldName}' expected {targetType.Name}, but JSON value was {value.GetType().Name}."); } internal static class SimpleJson { public static object Parse(string json) { var parser = new Parser(json); var value = parser.ParseValue(); parser.SkipWhitespace(); if (!parser.IsEnd) { throw new FormatException("Unexpected trailing JSON content."); } return value; } public static string Write(object value) { var builder = new StringBuilder(); WriteValue(builder, value); return builder.ToString(); } private static void WriteValue(StringBuilder builder, object value) { switch (value) { case null: builder.Append("null"); break; case string text: WriteString(builder, text); break; case bool boolValue: builder.Append(boolValue ? "true" : "false"); break; case IDictionary map: WriteObject(builder, map); break; case IEnumerable array: WriteArray(builder, array); break; case float or double or decimal: builder.Append(Convert.ToString(value, CultureInfo.InvariantCulture)); break; default: if (value.GetType().IsEnum) { WriteString(builder, value.ToString()); } else { builder.Append(Convert.ToString(value, CultureInfo.InvariantCulture)); } break; } } private static void WriteObject(StringBuilder builder, IDictionary map) { builder.Append('{'); var first = true; foreach (var pair in map) { if (!first) { builder.Append(','); } first = false; WriteString(builder, pair.Key); builder.Append(':'); WriteValue(builder, pair.Value); } builder.Append('}'); } private static void WriteArray(StringBuilder builder, IEnumerable values) { builder.Append('['); var first = true; foreach (var value in values) { if (!first) { builder.Append(','); } first = false; WriteValue(builder, value); } builder.Append(']'); } private static void WriteString(StringBuilder builder, string value) { builder.Append('"'); foreach (var character in value) { switch (character) { case '"': builder.Append("\\\""); break; case '\\': builder.Append("\\\\"); break; case '\n': builder.Append("\\n"); break; case '\r': builder.Append("\\r"); break; case '\t': builder.Append("\\t"); break; default: builder.Append(character); break; } } builder.Append('"'); } private sealed class Parser { private readonly string _json; private int _index; public Parser(string json) { _json = json ?? throw new ArgumentNullException(nameof(json)); } public bool IsEnd => _index >= _json.Length; public object ParseValue() { SkipWhitespace(); if (IsEnd) { throw new FormatException("Unexpected end of JSON."); } return _json[_index] switch { '{' => ParseObject(), '[' => ParseArray(), '"' => ParseString(), 't' => ParseLiteral("true", true), 'f' => ParseLiteral("false", false), 'n' => ParseLiteral("null", null), _ => ParseNumber() }; } public void SkipWhitespace() { while (!IsEnd && char.IsWhiteSpace(_json[_index])) { _index++; } } private Dictionary ParseObject() { Expect('{'); var map = new Dictionary(StringComparer.OrdinalIgnoreCase); SkipWhitespace(); if (TryConsume('}')) { return map; } while (true) { SkipWhitespace(); var key = ParseString(); SkipWhitespace(); Expect(':'); map[key] = ParseValue(); SkipWhitespace(); if (TryConsume('}')) { return map; } Expect(','); } } private List ParseArray() { Expect('['); var array = new List(); SkipWhitespace(); if (TryConsume(']')) { return array; } while (true) { array.Add(ParseValue()); SkipWhitespace(); if (TryConsume(']')) { return array; } Expect(','); } } private string ParseString() { Expect('"'); var builder = new StringBuilder(); while (!IsEnd) { var character = _json[_index++]; if (character == '"') { return builder.ToString(); } if (character != '\\') { builder.Append(character); continue; } if (IsEnd) { throw new FormatException("Unexpected end of JSON escape sequence."); } var escaped = _json[_index++]; builder.Append(escaped switch { '"' => '"', '\\' => '\\', '/' => '/', 'b' => '\b', 'f' => '\f', 'n' => '\n', 'r' => '\r', 't' => '\t', _ => throw new FormatException($"Unsupported JSON escape '\\{escaped}'.") }); } throw new FormatException("Unterminated JSON string."); } private object ParseNumber() { var start = _index; if (!IsEnd && _json[_index] == '-') { _index++; } while (!IsEnd && char.IsDigit(_json[_index])) { _index++; } var isFloatingPoint = false; if (!IsEnd && _json[_index] == '.') { isFloatingPoint = true; _index++; while (!IsEnd && char.IsDigit(_json[_index])) { _index++; } } if (!IsEnd && (_json[_index] == 'e' || _json[_index] == 'E')) { isFloatingPoint = true; _index++; if (!IsEnd && (_json[_index] == '+' || _json[_index] == '-')) { _index++; } while (!IsEnd && char.IsDigit(_json[_index])) { _index++; } } var text = _json.Substring(start, _index - start); if (string.IsNullOrEmpty(text) || text == "-") { throw new FormatException("Invalid JSON number."); } return isFloatingPoint ? double.Parse(text, CultureInfo.InvariantCulture) : long.Parse(text, CultureInfo.InvariantCulture); } private object ParseLiteral(string literal, object value) { if (_index + literal.Length > _json.Length || string.CompareOrdinal(_json, _index, literal, 0, literal.Length) != 0) { throw new FormatException($"Invalid JSON literal near index {_index}."); } _index += literal.Length; return value; } private bool TryConsume(char expected) { if (!IsEnd && _json[_index] == expected) { _index++; return true; } return false; } private void Expect(char expected) { if (IsEnd || _json[_index] != expected) { throw new FormatException($"Expected '{expected}' near index {_index}."); } _index++; } } } } }