Files
EnglewoodLAB/Assets/Sample/Data/DataMapperTests.cs

627 lines
27 KiB
C#

#nullable enable
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using UnityEngine;
using UVC.Data.Core;
namespace UVC.Sample.Data
{
/// <summary>
/// DataMapper 기능 샘플 코드입니다.
/// DataSampleRunner Inspector에서 실행하거나 Play 버튼으로 확인합니다.
/// </summary>
public class DataMapperTests
{
/// <summary>
/// 모든 샘플을 순차 실행합니다.
/// </summary>
public void RunAll()
{
Debug.Log("===== DataMapper 샘플 시작 =====");
Run(nameof(Map_StringProperty_MapsCorrectly), Map_StringProperty_MapsCorrectly);
Run(nameof(Map_IntProperty_MapsCorrectly), Map_IntProperty_MapsCorrectly);
Run(nameof(Map_DoubleProperty_MapsCorrectly), Map_DoubleProperty_MapsCorrectly);
Run(nameof(Map_BoolProperty_MapsCorrectly), Map_BoolProperty_MapsCorrectly);
Run(nameof(Map_DateTimeProperty_MapsCorrectly), Map_DateTimeProperty_MapsCorrectly);
Run(nameof(Map_DataMapProperty_MapsCorrectly), Map_DataMapProperty_MapsCorrectly);
Run(nameof(Map_DataMapWithUnmappedValue_ReturnsOriginal), Map_DataMapWithUnmappedValue_ReturnsOriginal);
Run(nameof(Map_EnumProperty_MapsCorrectly), Map_EnumProperty_MapsCorrectly);
Run(nameof(Map_AdditionalProperty_AddsToResult), Map_AdditionalProperty_AddsToResult);
Run(nameof(Map_InvalidDateTimeString_ReturnsNull), Map_InvalidDateTimeString_ReturnsNull);
Run(nameof(Map_ComplexObject_MapsAllProperties), Map_ComplexObject_MapsAllProperties);
Run(nameof(Map_NestedObject_MapsCorrectly), Map_NestedObject_MapsCorrectly);
Run(nameof(Map_ArrayMapping_MapsCorrectly), Map_ArrayMapping_MapsCorrectly);
Run(nameof(Map_EmptyMaskArray_CopiesSourceArray), Map_EmptyMaskArray_CopiesSourceArray);
Run(nameof(Map_ComplexNestedStructure_MapsCorrectly), Map_ComplexNestedStructure_MapsCorrectly);
Run(nameof(Map_MixedArrayTypes_HandlesCorrectly), Map_MixedArrayTypes_HandlesCorrectly);
Run(nameof(Map_WithNamesForReplace_ShouldRenameProperties), Map_WithNamesForReplace_ShouldRenameProperties);
Run(nameof(Map_DeepClone_CopiesAllProperties), Map_DeepClone_CopiesAllProperties);
Run(nameof(Map_ToJObject_ConvertsCorrectly), Map_ToJObject_ConvertsCorrectly);
Run(nameof(Map_ParallelArrayProcessing_WorksCorrectly), Map_ParallelArrayProcessing_WorksCorrectly);
Run(nameof(Map_RecursionDepthLimit_HandlesCorrectly), Map_RecursionDepthLimit_HandlesCorrectly);
Debug.Log("===== DataMapper 샘플 완료 =====");
}
private void Run(string name, Action action)
{
try
{
action();
Debug.Log($"[OK] {name}");
}
catch (Exception ex)
{
Debug.LogError($"[FAIL] {name}\n{ex.Message}");
}
}
private static void Check(bool condition, string message)
{
if (!condition) throw new Exception($"검증 실패: {message}");
}
/// <summary>
/// 문자열 타입 매핑 샘플
/// </summary>
public void Map_StringProperty_MapsCorrectly()
{
var mask = new DataMask();
mask["Name"] = "홍길동";
mask.ObjectIdKey = "Name";
var source = JObject.Parse(@"{ ""Name"": ""김철수"" }");
var mapper = new DataMapper(mask);
var result = mapper.Map(source);
Check(result.ContainsKey("Name"), "결과에 'Name' 키가 있어야 합니다.");
Check(result.GetString("Name") == "김철수", "Name은 '김철수'여야 합니다.");
Check(result.Id == "김철수", "Id는 '김철수'여야 합니다.");
}
/// <summary>
/// 정수 타입 매핑 샘플
/// </summary>
public void Map_IntProperty_MapsCorrectly()
{
var mask = new DataMask();
mask["Age"] = 25;
mask.ObjectIdKey = "Age";
var source = JObject.Parse(@"{ ""Age"": 30 }");
var mapper = new DataMapper(mask);
var result = mapper.Map(source);
Check(result.ContainsKey("Age"), "결과에 'Age' 키가 있어야 합니다.");
Check(result.GetInt("Age") == 30, "Age는 30이어야 합니다.");
Check(result.Id == "30", "Id는 '30'이어야 합니다.");
}
/// <summary>
/// 부동소수점 타입 매핑 샘플
/// </summary>
public void Map_DoubleProperty_MapsCorrectly()
{
var mask = new DataMask();
mask["Height"] = 175.5;
mask.ObjectIdKey = "Height";
var source = JObject.Parse(@"{ ""Height"": 180.5 }");
var mapper = new DataMapper(mask);
var result = mapper.Map(source);
Check(result.ContainsKey("Height"), "결과에 'Height' 키가 있어야 합니다.");
Check(result.GetDouble("Height") == 180.5, "Height는 180.5여야 합니다.");
}
/// <summary>
/// Boolean 타입 매핑 샘플
/// </summary>
public void Map_BoolProperty_MapsCorrectly()
{
var mask = new DataMask();
mask["IsActive"] = false;
var source = JObject.Parse(@"{ ""IsActive"": true }");
var mapper = new DataMapper(mask);
var result = mapper.Map(source);
Check(result.ContainsKey("IsActive"), "결과에 'IsActive' 키가 있어야 합니다.");
Check(result.GetBool("IsActive") == true, "IsActive는 true여야 합니다.");
}
/// <summary>
/// DateTime 타입 매핑 샘플
/// </summary>
public void Map_DateTimeProperty_MapsCorrectly()
{
var mask = new DataMask();
mask["BirthDate"] = JToken.FromObject(DateTime.Now);
var expectedDate = new DateTime(1990, 1, 1);
var source = JObject.Parse(@"{ ""BirthDate"": ""1990-01-01T00:00:00.000"" }");
var mapper = new DataMapper(mask);
var result = mapper.Map(source);
Check(result.ContainsKey("BirthDate"), "결과에 'BirthDate' 키가 있어야 합니다.");
Check(result.GetDateTime("BirthDate") == expectedDate, "BirthDate는 1990-01-01이어야 합니다.");
}
/// <summary>
/// DataMap 값 매핑 샘플
/// </summary>
public void Map_DataMapProperty_MapsCorrectly()
{
var mask = new DataMask();
var valueMapper = new DataMap();
valueMapper["ON"] = "활성화";
valueMapper["OFF"] = "비활성화";
mask["Status"] = JToken.FromObject(valueMapper);
var source = JObject.Parse(@"{ ""Status"": ""ON"" }");
var mapper = new DataMapper(mask);
var result = mapper.Map(source);
Check(result.ContainsKey("Status"), "결과에 'Status' 키가 있어야 합니다.");
Check(result.GetString("Status") == "활성화", "Status는 '활성화'여야 합니다.");
}
/// <summary>
/// DataMap에 없는 값은 원본 그대로 반환됩니다.
/// </summary>
public void Map_DataMapWithUnmappedValue_ReturnsOriginal()
{
var mask = new DataMask();
var valueMapper = new DataMap();
valueMapper["ON"] = "활성화";
valueMapper["OFF"] = "비활성화";
mask["Status"] = JToken.FromObject(valueMapper);
var source = JObject.Parse(@"{ ""Status"": ""UNKNOWN"" }");
var mapper = new DataMapper(mask);
var result = mapper.Map(source);
Check(result.ContainsKey("Status"), "결과에 'Status' 키가 있어야 합니다.");
Check(result.GetString("Status") == "UNKNOWN", "매핑되지 않은 값은 원본을 반환해야 합니다.");
}
/// <summary>
/// Enum 타입 매핑 샘플
/// </summary>
public void Map_EnumProperty_MapsCorrectly()
{
var mask = new DataMask();
mask["Status"] = JToken.FromObject(SampleUserStatus.Inactive);
var source = JObject.Parse(@"{ ""Status"": ""Active"" }");
var mapper = new DataMapper(mask);
var result = mapper.Map(source);
Check(result.ContainsKey("Status"), "결과에 'Status' 키가 있어야 합니다.");
Check(result.GetEnum<SampleUserStatus>("Status") == SampleUserStatus.Active, "Status는 Active여야 합니다.");
}
/// <summary>
/// 마스크에 없는 추가 프로퍼티는 결과에 포함되지 않습니다.
/// </summary>
public void Map_AdditionalProperty_AddsToResult()
{
var mask = new DataMask();
mask["Name"] = "홍길동";
var source = JObject.Parse(@"{ ""Name"": ""김철수"", ""Email"": ""kim@example.com"" }");
var mapper = new DataMapper(mask);
var result = mapper.Map(source);
Check(!result.ContainsKey("Email"), "마스크에 없는 'Email' 키는 결과에 없어야 합니다.");
Check(result.GetString("Email") == null, "GetString('Email')은 null이어야 합니다.");
}
/// <summary>
/// 유효하지 않은 DateTime 문자열은 null을 반환합니다.
/// </summary>
public void Map_InvalidDateTimeString_ReturnsNull()
{
var mask = new DataMask();
mask["BirthDate"] = JToken.FromObject(DateTime.Now);
var source = JObject.Parse(@"{ ""BirthDate"": ""InvalidDateTime"" }");
var mapper = new DataMapper(mask);
var result = mapper.Map(source);
Check(result.ContainsKey("BirthDate"), "결과에 'BirthDate' 키가 있어야 합니다.");
Check(result["BirthDate"] == null, "잘못된 DateTime은 null이어야 합니다.");
}
/// <summary>
/// 복합 객체 매핑 샘플
/// </summary>
public void Map_ComplexObject_MapsAllProperties()
{
var mask = new DataMask();
mask["Id"] = 1;
mask["Name"] = "홍길동";
mask["IsActive"] = false;
mask["JoinDate"] = JToken.FromObject(DateTime.Now);
mask["Height"] = 175.5;
mask["Status"] = JToken.FromObject(SampleUserStatus.Inactive);
string json = @"{
""Id"": 100,
""Name"": ""김철수"",
""IsActive"": true,
""JoinDate"": ""2023-01-01T00:00:00"",
""Height"": 180.0,
""Status"": ""Active"",
""Email"": ""kim@example.com""
}";
var source = JObject.Parse(json);
var mapper = new DataMapper(mask);
var result = mapper.Map(source);
Check(result.Count == 6, "결과에 6개의 프로퍼티가 있어야 합니다.");
Check(result.GetInt("Id") == 100, "Id는 100이어야 합니다.");
Check(result.GetString("Name") == "김철수", "Name은 '김철수'여야 합니다.");
Check(result.GetBool("IsActive") == true, "IsActive는 true여야 합니다.");
Check(result.GetDateTime("JoinDate") == new DateTime(2023, 1, 1), "JoinDate는 2023-01-01이어야 합니다.");
Check(result.GetDouble("Height") == 180.0, "Height는 180.0이어야 합니다.");
Check(result.GetEnum<SampleUserStatus>("Status") == SampleUserStatus.Active, "Status는 Active여야 합니다.");
Check(result.GetString("Email") == null, "Email은 null이어야 합니다.");
}
/// <summary>
/// 중첩 객체 매핑 샘플
/// </summary>
public void Map_NestedObject_MapsCorrectly()
{
var mask = new DataMask
{
["User"] = new DataMask
{
["Name"] = "홍길동",
["Address"] = new DataMask
{
["City"] = "서울",
["ZipCode"] = "12345"
}
}
};
var source = JObject.Parse(@"{
""User"": {
""Name"": ""김철수"",
""Address"": {
""City"": ""부산"",
""ZipCode"": ""67890"",
""Country"": ""대한민국""
}
}
}");
var mapper = new DataMapper(mask);
var result = mapper.Map(source);
Check(result.ContainsKey("User"), "결과에 'User' 키가 있어야 합니다.");
var user = result.GetDataObject("User");
Check(user != null, "User는 null이 아니어야 합니다.");
Check(user!.GetString("Name") == "김철수", "User.Name은 '김철수'여야 합니다.");
var address = user.GetDataObject("Address");
Check(address != null, "Address는 null이 아니어야 합니다.");
Check(address!.GetString("City") == "부산", "Address.City는 '부산'이어야 합니다.");
Check(address.GetString("ZipCode") == "67890", "Address.ZipCode는 '67890'이어야 합니다.");
Check(address.GetString("Country") == null, "마스크에 없는 Country는 null이어야 합니다.");
}
/// <summary>
/// 배열 매핑 샘플
/// </summary>
public void Map_ArrayMapping_MapsCorrectly()
{
var mask = new DataMask
{
["Contacts"] = new List<DataMask>
{
new DataMask
{
["Type"] = "mobile",
["Number"] = "010-0000-0000"
}
}
};
var source = JObject.Parse(@"{
""Contacts"": [
{ ""Type"": ""mobile"", ""Number"": ""010-1234-5678"" },
{ ""Type"": ""home"", ""Number"": ""02-123-4567"", ""Extension"": ""123"" }
]
}");
var mapper = new DataMapper(mask);
var result = mapper.Map(source);
Check(result.ContainsKey("Contacts"), "결과에 'Contacts' 키가 있어야 합니다.");
var contacts = result.GetDataArray("Contacts");
Check(contacts != null, "Contacts는 null이 아니어야 합니다.");
Check(contacts!.Count == 2, "Contacts에 2개 항목이 있어야 합니다.");
Check(contacts[0].GetString("Type") == "mobile", "첫 번째 Type은 'mobile'이어야 합니다.");
Check(contacts[0].GetString("Number") == "010-1234-5678", "첫 번째 Number가 올바르지 않습니다.");
Check(contacts[1].GetString("Type") == "home", "두 번째 Type은 'home'이어야 합니다.");
Check(contacts[1].GetString("Extension") == null, "마스크에 없는 Extension은 null이어야 합니다.");
}
/// <summary>
/// 빈 가이드 배열은 소스 배열을 그대로 복사합니다.
/// </summary>
public void Map_EmptyMaskArray_CopiesSourceArray()
{
var mask = new DataMask
{
["Tags"] = new List<DataMask>()
};
var source = JObject.Parse(@"{
""Tags"": [""개발"", ""테스트"", ""배포""]
}");
var mapper = new DataMapper(mask);
var result = mapper.Map(source);
Check(result.ContainsKey("Tags"), "결과에 'Tags' 키가 있어야 합니다.");
var tags = result.GetDataArray("Tags");
Check(tags != null, "Tags는 null이 아니어야 합니다.");
Check(tags!.Count == 3, "Tags에 3개 항목이 있어야 합니다.");
}
/// <summary>
/// 복잡한 중첩 구조 매핑 샘플
/// </summary>
public void Map_ComplexNestedStructure_MapsCorrectly()
{
var mask = new DataMask
{
["Company"] = new DataMask
{
["Name"] = "회사명",
["Founded"] = JToken.FromObject(DateTime.Now),
["Departments"] = new List<DataMask>
{
new DataMask
{
["Name"] = "부서명",
["Employees"] = new List<DataMask>
{
new DataMask
{
["Name"] = "직원명",
["Age"] = 30,
["Status"] = JToken.FromObject(SampleUserStatus.Active)
}
}
}
}
}
};
var source = JObject.Parse(@"{
""Company"": {
""Name"": ""XYZ 주식회사"",
""Founded"": ""2000-01-01T00:00:00"",
""Departments"": [
{
""Name"": ""개발부"",
""Employees"": [
{ ""Name"": ""김개발"", ""Age"": 35, ""Status"": ""Active"" },
{ ""Name"": ""이테스트"", ""Age"": 28, ""Status"": ""Inactive"" }
]
},
{
""Name"": ""마케팅부"",
""Employees"": [
{ ""Name"": ""박마케팅"", ""Age"": 32, ""Status"": ""Active"" }
],
""Budget"": 500000
}
],
""Address"": ""서울시 강남구""
}
}");
var mapper = new DataMapper(mask);
var result = mapper.Map(source);
var company = result.GetDataObject("Company");
Check(company != null, "Company는 null이 아니어야 합니다.");
Check(company!.GetString("Name") == "XYZ 주식회사", "Company.Name이 올바르지 않습니다.");
Check(company.GetDateTime("Founded") == new DateTime(2000, 1, 1), "Company.Founded가 올바르지 않습니다.");
Check(company.GetString("Address") == null, "마스크에 없는 Address는 null이어야 합니다.");
var departments = company.GetDataArray("Departments");
Check(departments != null && departments.Count == 2, "Departments에 2개 항목이 있어야 합니다.");
var devDept = departments![0];
Check(devDept.GetString("Name") == "개발부", "첫 번째 부서는 '개발부'여야 합니다.");
var devEmployees = devDept.GetDataArray("Employees");
Check(devEmployees != null && devEmployees.Count == 2, "개발부에 2명이 있어야 합니다.");
Check(devEmployees![0].GetString("Name") == "김개발", "첫 번째 직원은 '김개발'이어야 합니다.");
Check(devEmployees[0].GetInt("Age") == 35, "첫 번째 직원 나이는 35여야 합니다.");
Check(devEmployees[0].GetEnum<SampleUserStatus>("Status") == SampleUserStatus.Active, "첫 번째 직원 Status는 Active여야 합니다.");
}
/// <summary>
/// 다양한 형식이 혼합된 배열 매핑 샘플
/// </summary>
public void Map_MixedArrayTypes_HandlesCorrectly()
{
var mask = new DataMask(@"{
""MixedArray"": [
{ ""Key"": ""Value"", ""Key1"": 1, ""Key2"": true}
]
}");
var source = JObject.Parse(@"{
""MixedArray"": [
{ ""Key"": ""NewValue"", ""Key1"": 456, ""Key2"": false, ""Extra"": ""ExtraValue"" },
{ ""items"": [1, 2, 3] }
]
}");
var mapper = new DataMapper(mask);
var result = mapper.Map(source);
Check(result.ContainsKey("MixedArray"), "결과에 'MixedArray' 키가 있어야 합니다.");
var mixedArray = result.GetDataArray("MixedArray");
Check(mixedArray != null && mixedArray.Count == 2, "MixedArray에 2개 항목이 있어야 합니다.");
Check(mixedArray![0].GetString("Key") == "NewValue", "Key는 'NewValue'여야 합니다.");
Check(mixedArray[0].GetInt("Key1") == 456, "Key1은 456이어야 합니다.");
Check(mixedArray[0].GetBool("Key2") == false, "Key2는 false여야 합니다.");
}
/// <summary>
/// NamesForReplace로 프로퍼티 이름을 변경합니다.
/// </summary>
public void Map_WithNamesForReplace_ShouldRenameProperties()
{
var mask = new DataMask();
mask["OldName"] = "홍길동";
mask["Age"] = 25;
mask.NamesForReplace = new Dictionary<string, string> {
{ "OldName", "NewName" }
};
var source = JObject.Parse(@"{ ""OldName"": ""김철수"", ""Age"": 30 }");
var mapper = new DataMapper(mask);
var result = mapper.Map(source);
Check(result.ContainsKey("NewName"), "결과에 'NewName' 키가 있어야 합니다.");
Check(result.GetString("NewName") == "김철수", "NewName은 '김철수'여야 합니다.");
Check(!result.ContainsKey("OldName"), "결과에 'OldName' 키가 없어야 합니다.");
Check(result.GetInt("Age") == 30, "Age는 30이어야 합니다.");
}
/// <summary>
/// DeepClone: 원본과 독립된 복사본을 만듭니다.
/// </summary>
public void Map_DeepClone_CopiesAllProperties()
{
var originalMask = new DataMask();
originalMask["Name"] = "홍길동";
originalMask["Age"] = 25;
originalMask.ObjectIdKey = "Name";
originalMask.ObjectName = "Person";
originalMask.NamesForReplace = new Dictionary<string, string> {
{ "full_name", "Name" },
{ "user_age", "Age" }
};
var clonedMask = originalMask.DeepClone();
originalMask["Name"] = "변경된 이름";
originalMask.ObjectName = "ModifiedPerson";
originalMask.NamesForReplace["full_name"] = "FullName";
Check((string)clonedMask["Name"] == "홍길동", "클론의 Name은 원본 변경에 영향받지 않아야 합니다.");
Check(clonedMask.ObjectIdKey == "Name", "클론의 ObjectIdKey는 'Name'이어야 합니다.");
Check(clonedMask.ObjectName == "Person", "클론의 ObjectName은 'Person'이어야 합니다.");
Check(clonedMask.NamesForReplace["full_name"] == "Name", "클론의 NamesForReplace가 원본 변경에 영향받지 않아야 합니다.");
}
/// <summary>
/// ToJObject: DataMask를 JObject로 변환합니다.
/// </summary>
public void Map_ToJObject_ConvertsCorrectly()
{
var mask = new DataMask();
mask["Name"] = "홍길동";
mask["Age"] = 25;
mask["IsActive"] = true;
mask["Height"] = 175.5;
var jObject = mask.ToJObject();
Check(jObject != null, "jObject는 null이 아니어야 합니다.");
Check(jObject!.Count == 4, "jObject에 4개의 프로퍼티가 있어야 합니다.");
Check(jObject["Name"]!.ToString() == "홍길동", "Name은 '홍길동'이어야 합니다.");
Check(jObject["Age"]!.ToObject<int>() == 25, "Age는 25여야 합니다.");
Check(jObject["IsActive"]!.ToObject<bool>() == true, "IsActive는 true여야 합니다.");
Check(jObject["Height"]!.ToObject<double>() == 175.5, "Height는 175.5여야 합니다.");
}
/// <summary>
/// 대용량 배열 병렬 처리 샘플 (2000개 항목)
/// </summary>
public void Map_ParallelArrayProcessing_WorksCorrectly()
{
var mask = new DataMask();
mask["Items"] = new List<DataMask> { new DataMask { ["Id"] = 0, ["Name"] = "" } };
var itemsArray = new JArray();
for (int i = 1; i <= 2000; i++)
{
itemsArray.Add(new JObject
{
["Id"] = i,
["Name"] = $"Item {i}"
});
}
var source = new JObject { ["Items"] = itemsArray };
var mapper = new DataMapper(mask);
var result = mapper.Map(source);
var items = result.GetDataArray("Items");
Check(items != null, "Items는 null이 아니어야 합니다.");
Check(items!.Count == 2000, "Items에 2000개 항목이 있어야 합니다.");
Check(items[0].GetInt("Id") == 1, "첫 번째 Id는 1이어야 합니다.");
Check(items[999].GetInt("Id") == 1000, "1000번째 Id는 1000이어야 합니다.");
Check(items[1999].GetInt("Id") == 2000, "마지막 Id는 2000이어야 합니다.");
Debug.Log($" → 2000개 항목 병렬 처리 완료. 마지막 항목: Id={items[1999].GetInt("Id")}, Name={items[1999].GetString("Name")}");
}
/// <summary>
/// 재귀 깊이 제한이 올바르게 동작합니다.
/// </summary>
public void Map_RecursionDepthLimit_HandlesCorrectly()
{
var mask = CreateDeepNestedMask(20);
var source = CreateDeepNestedJObject(20);
var mapper = new DataMapper(mask);
var result = mapper.Map(source);
var level1 = result.GetDataObject("Level20");
Check(level1 != null, "Level20은 null이 아니어야 합니다.");
}
private DataMask CreateDeepNestedMask(int depth)
{
if (depth <= 0) return new DataMask { ["Value"] = "마스크 값" };
var mask = new DataMask();
mask[$"Level{depth}"] = CreateDeepNestedMask(depth - 1);
return mask;
}
private JObject CreateDeepNestedJObject(int depth)
{
if (depth <= 0) return new JObject { ["Value"] = "소스 값" };
var obj = new JObject();
obj[$"Level{depth}"] = CreateDeepNestedJObject(depth - 1);
return obj;
}
}
/// <summary>
/// 샘플에 사용하는 열거형
/// </summary>
public enum SampleUserStatus
{
Active,
Inactive,
Suspended
}
}