#nullable enable
using Newtonsoft.Json.Linq;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Cysharp.Threading.Tasks;
using UVC.Extention;
namespace UVC.Data.Core
{
///
/// 키-값 쌍의 데이터를 관리하고 변경 사항을 추적하는 동적 데이터 객체입니다.
/// SortedDictionary를 상속하여 키를 기준으로 정렬된 데이터를 제공합니다.
///
///
/// 이 클래스는 JSON과 호환되는 데이터 구조를 표현하며, 데이터 변경 추적 기능을 통해
/// 효율적인 데이터 동기화를 지원합니다.
///
///
///
/// // DataMask 생성 및 설정
/// var mask = new DataMask();
/// mask.ObjectIdKey = "Id";
/// mask.ObjectName = "users";
///
/// // 필드 이름 변환 설정
/// mask.NamesForReplace = new Dictionary
/// {
/// { "userName", "name" },
/// { "userEmail", "email" }
/// };
///
/// // JObject 속성으로 마스킹 규칙 추가
/// mask["include"] = new JArray("name", "email", "age");
/// mask["exclude"] = new JArray("password", "token");
///
/// // DataObject에 마스크 적용하는 예시
/// var dataObject = new DataObject();
/// dataObject["userName"] = "홍길동";
/// dataObject["userEmail"] = "hong@example.com";
/// dataObject["password"] = "secret123";
///
/// // 마스크 적용 결과 (개념적 예시):
/// // - userName은 name으로 변환됨
/// // - userEmail은 email로 변환됨
/// // - password는 제외됨
///
///
public partial class DataObject : OrderedDictionary, IDataObject
{
private bool isInPool = false;
///
/// 이 객체가 객체 풀에 있는지 여부를 나타냅니다.
/// 중복 반환을 방지하기 위해 DataObjectPool에서 내부적으로 사용됩니다.
///
internal bool IsInPool
{
get => isInPool;
set
{
isInPool = value;
foreach (var item in this)
{
if (item.Value is DataObject dataObject)
{
dataObject.isInPool = value; // 내부 DataObject도 풀에 있다고 표시합니다.
}
else if (item.Value is DataArray dataArray)
{
dataArray.IsInPool = value; // 내부 DataArray도 풀에 있다고 표시합니다.
}
}
}
}
private bool createdFromPool = false;
///
/// 객체가 풀에서 생성되었는지 여부를 나타냅니다.
///
internal bool CreatedFromPool
{
get => createdFromPool;
set
{
createdFromPool = value;
foreach (var item in this)
{
if (item.Value is DataObject dataObject)
{
dataObject.CreatedFromPool = value; // 내부 DataObject도 풀에 있다고 표시합니다.
}
else if (item.Value is DataArray dataArray)
{
dataArray.CreatedFromPool = value; // 내부 DataArray도 풀에 있다고 표시합니다.
}
}
}
}
///
/// 객체의 고유 식별자를 나타내는 속성입니다. DataArray에서 데이터를 식별하는 데 사용됩니다.
///
public string Id
{
get
{
if (IdKey != null && ContainsKey(IdKey))
{
return this[IdKey]?.ToString() ?? string.Empty;
}
if (Count > 0)
{
return this.First().Value?.ToString() ?? string.Empty;
}
return string.Empty;
}
}
///
/// Id에 해당하는 key 문자열
///
public string? IdKey { get; set; } = null;
///
/// DataObject의 이름을 나타내는 속성입니다.
///
public string Name { get; internal set; } = string.Empty;
///
/// 직접적인 변경이 있었던 키를 저장하는 리스트입니다.
///
protected HashSet changedProperies = new HashSet();
///
/// 변경된 속성의 키 목록을 읽기 전용으로 반환합니다.
///
public IReadOnlyCollection ChangedProperies => changedProperies;
// 객체 생성 중에는 변경 추적을 비활성화하기 위한 플래그
private bool _isInitializing = false;
///
/// 기본 생성자입니다. 빈 데이터 객체를 생성합니다.
///
public DataObject() { }
public DataObject(string jsonString) : this(JObject.Parse(jsonString))
{
}
public DataObject(System.IO.Stream jsonStream)
{
if (jsonStream == null)
throw new ArgumentNullException(nameof(jsonStream));
// 스트림 처리 최적화를 위해 청크 단위로 읽을 수 있지만,
// 현재는 Newtonsoft.Json의 기본 역직렬화 사용
using (var reader = new Newtonsoft.Json.JsonTextReader(new System.IO.StreamReader(jsonStream)))
{
// 청크 읽기 설정 - 메모리 사용량 최적화
reader.SupportMultipleContent = true;
var serializer = new Newtonsoft.Json.JsonSerializer();
var sourceObject = serializer.Deserialize(reader);
// 수정된 코드: 생성자를 호출하는 대신 FromJObject 메서드를 사용
if (sourceObject != null) FromJObject(sourceObject);
}
}
///
/// JObject로부터 데이터 객체를 생성합니다.
///
/// 복사할 JObject 객체
public DataObject(JObject other)
{
_isInitializing = true;
try
{
// JObject로부터 속성을 복사
foreach (var prop in other.Properties())
{
// 인덱서를 사용하여 JToken을 객체로 변환하고 추가
this[prop.Name] = ConvertJTokenToObject(prop.Value);
}
}
finally
{
_isInitializing = false;
}
}
///
/// Dictionary로 데이터 객체를 초기화합니다.
///
/// 초기화에 사용할 Dictionary 객체
public DataObject(Dictionary dictionary) : base()
{
_isInitializing = true;
try
{
// 생성자에서 초기 속성들을 등록
foreach (var item in dictionary)
{
this[item.Key] = item.Value;
}
}
finally
{
_isInitializing = false;
}
}
///
/// 지정된 의 속성과 값으로 현재 객체를 채웁니다.
///
/// 이 메서드는 제공된 의 모든 속성을 반복하고
/// 해당 값을 현재 객체의 해당 속성에 할당합니다. 현재 객체에 속성이 없는 경우
/// 동적으로 추가됩니다. 이 메서드는 채우기 프로세스 중 적절한 처리를 보장하기 위해 내부 초기화
/// 플래그를 임시로 설정합니다.
/// 복사할 속성과 값이 포함된 입니다.
public DataObject FromJObject(JObject other)
{
IdKey = null; // IdKey를 초기화합니다.
Name = string.Empty; // Name을 초기화합니다.
_isInitializing = true;
try
{
// JObject로부터 속성을 복사
foreach (var prop in other.Properties())
{
// 인덱서를 사용하여 JToken을 객체로 변환하고 추가
this[prop.Name] = ConvertJTokenToObject(prop.Value);
}
}
finally
{
_isInitializing = false;
}
return this;
}
///
/// 지정된 사전의 키-값 쌍으로 현재 객체를 채웁니다.
///
/// 이 메서드는 제공된 사전을 기반으로 객체의 속성을 업데이트합니다.
/// 사전의 키가 속성 이름과 일치하면 속성 값이 해당 값으로 설정됩니다.
/// 사전에 예상되는 속성 유형과 일치하는 유효한 키와 값이 포함되어 있는지 확인합니다.
/// 객체의 속성을 초기화하는 키-값 쌍을 포함하는 사전입니다. 키는 속성
/// 이름을 나타내고 값은 해당 속성 값을 나타냅니다.
public DataObject FromDictionary(Dictionary dictionary)
{
IdKey = null; // IdKey를 초기화합니다.
Name = string.Empty; // Name을 초기화합니다.
_isInitializing = true;
try
{
// 생성자에서 초기 속성들을 등록
foreach (var item in dictionary)
{
this[item.Key] = item.Value;
}
}
finally
{
_isInitializing = false;
}
return this;
}
///
/// JSON 문자열 표현에서 인스턴스를 생성합니다.
///
/// 이 메서드를 사용하여 JSON 문자열을 로 역직렬화합니다.
/// 입력 문자열이 예상 객체 구조의 유효한 JSON 표현인지 확인합니다.
/// 로 파싱할 JSON 문자열입니다. null이거나 비어 있으면 안 됩니다.
/// JSON 문자열의 데이터로 채워진 새 인스턴스를 반환합니다. 이 null이거나 비어 있으면 현재 인스턴스를 반환합니다.
public DataObject FromJsonString(string jsonString)
{
if (!string.IsNullOrEmpty(jsonString))
{
return FromJObject(JObject.Parse(jsonString));
}
return this;
}
///
/// UniTask 기반 비차단 방식으로 JObject로부터 데이터를 채웁니다. WebGL 같은 싱글스레드 환경에서
/// UI 블로킹을 피하기 위해 일정 수의 키마다 await로 제어권을 양보합니다.
/// await myDataObject.FromJObjectIncrementalAsync(jObject, 200, cancellationToken);
///
/// JObject 소스
/// 한 번에 처리할 속성 수
/// 취소 토큰
public async UniTask FromJObjectIncrementalAsync(JObject other, int batchSize =50, CancellationToken cancellationToken = default)
{
if (other == null) return;
IdKey = null;
Name = string.Empty;
_isInitializing = true;
int count =0;
try
{
foreach (var prop in other.Properties())
{
cancellationToken.ThrowIfCancellationRequested();
#if UNITY_WEBGL && !UNITY_EDITOR
var value = await ConvertJTokenToObjectAsync(prop.Value, batchSize, cancellationToken);
this[prop.Name] = value;
#else
this[prop.Name] = ConvertJTokenToObject(prop.Value);
#endif
count++;
if (count % batchSize ==0)
{
await UniTask.Yield(cancellationToken);
}
}
}
finally
{
_isInitializing = false;
}
}
///
/// UniTask 기반 비차단 방식으로 JSON 문자열을 스트리밍하여 파싱합니다.
/// JsonTextReader를 사용해 토큰 단위로 읽고, 일정 수의 항목마다 await로 제어권을 양보합니다.
/// await myDataObject.FromJsonStringIncrementalAsync(jsonString, 200, cancellationToken);
///
/// JSON 문자열
/// 한 번에 처리할 속성 수
/// 취소 토큰
public async UniTask FromJsonStringIncrementalAsync(string jsonString, int batchSize =50, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(jsonString)) return;
using (var sr = new System.IO.StringReader(jsonString))
using (var reader = new Newtonsoft.Json.JsonTextReader(sr))
{
reader.SupportMultipleContent = false;
// 기대: StartObject
if (!reader.Read() || reader.TokenType != Newtonsoft.Json.JsonToken.StartObject) return;
IdKey = null;
Name = string.Empty;
_isInitializing = true;
int count =0;
while (reader.Read())
{
cancellationToken.ThrowIfCancellationRequested();
if (reader.TokenType == Newtonsoft.Json.JsonToken.PropertyName)
{
string propName = (string)reader.Value!;
// 다음 토큰은 값의 시작이어야 함. JToken.ReadFrom를 사용하여 값 전체를 읽는다.
if (!reader.Read()) break;
JToken token = JToken.ReadFrom(reader);
#if UNITY_WEBGL && !UNITY_EDITOR
var value = await ConvertJTokenToObjectAsync(token, batchSize, cancellationToken);
this[propName] = value;
#else
this[propName] = ConvertJTokenToObject(token);
#endif
count++;
if (count % batchSize ==0)
{
await UniTask.Yield(cancellationToken);
}
}
else if (reader.TokenType == Newtonsoft.Json.JsonToken.EndObject)
{
break;
}
}
_isInitializing = false;
}
}
///
/// JToken을 적절한 C# 객체 타입으로 변환하는 헬퍼 메서드입니다.
///
/// 변환할 JToken 객체
/// 변환된 C# 객체
private async UniTask