新增 FlowScope Git 包雏形
This commit is contained in:
@@ -0,0 +1,731 @@
|
||||
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<T>(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<string, object> values)
|
||||
{
|
||||
throw new InvalidOperationException($"JSON root must be an object for {target.GetType().Name}.");
|
||||
}
|
||||
|
||||
PopulateFromDictionary(values, target);
|
||||
}
|
||||
|
||||
public static IReadOnlyList<T> DeserializeArray<T>(string json) where T : class
|
||||
{
|
||||
if (SimpleJson.Parse(json) is not List<object> rows)
|
||||
{
|
||||
throw new InvalidOperationException("JSON root must be an array.");
|
||||
}
|
||||
|
||||
var results = new List<T>(rows.Count);
|
||||
foreach (var row in rows)
|
||||
{
|
||||
if (row is not Dictionary<string, object> 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<object>();
|
||||
foreach (var item in (IEnumerable)value)
|
||||
{
|
||||
array.Add(ToJsonValue(item));
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
var result = new Dictionary<string, object>();
|
||||
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<string, object> nested)
|
||||
{
|
||||
var instance = Activator.CreateInstance(targetType, true);
|
||||
PopulateFromDictionary(nested, instance);
|
||||
return instance;
|
||||
}
|
||||
|
||||
throw TypeMismatch(fieldName, targetType, value);
|
||||
}
|
||||
|
||||
private static void PopulateFromDictionary(IReadOnlyDictionary<string, object> 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<object> rows)
|
||||
{
|
||||
throw TypeMismatch(fieldName, collectionType, value);
|
||||
}
|
||||
|
||||
var itemType = GetCollectionItemType(collectionType);
|
||||
collectionType.GetMethod("Clear", Type.EmptyTypes)?.Invoke(collection, Array.Empty<object>());
|
||||
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<string, MemberInfo> GetWritableMembers(Type type)
|
||||
{
|
||||
var members = new Dictionary<string, MemberInfo>(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<PropertyInfo> 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<FieldInfo> 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<string, object> map:
|
||||
WriteObject(builder, map);
|
||||
break;
|
||||
case IEnumerable<object> 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<string, object> 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<object> 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<string, object> ParseObject()
|
||||
{
|
||||
Expect('{');
|
||||
var map = new Dictionary<string, object>(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<object> ParseArray()
|
||||
{
|
||||
Expect('[');
|
||||
var array = new List<object>();
|
||||
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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0c9f18f342e54f8d9a4dbdc61b74a2d1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user