Files
XRLib/Assets/Scripts/UVC/Data/DataValidator.cs
2025-06-25 18:50:19 +09:00

386 lines
16 KiB
C#

#nullable enable
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
namespace UVC.Data
{
/// <summary>
/// JSON 데이터의 유효성을 검사하는 클래스입니다.
/// 이 클래스는 특정 속성(프로퍼티)에 대한 유효성 검사 규칙을 추가하고,
/// 데이터가 해당 규칙을 모두 충족하는지 확인할 수 있습니다.
/// </summary>
/// <example>
/// <code>
/// // 유효성 검사기 생성
/// var _validator = new DataValidator();
///
/// // 검증 규칙 추가: name 속성은 비어있지 않아야 함
/// _validator.AddValidator("name", value => value is string name && !string.IsNullOrEmpty(name));
///
/// // 검증 규칙 추가: age 속성은 18 이상이어야 함
/// _validator.AddValidator("age", value => value is long age && age >= 18);
///
/// // 데이터 검증 예시
/// var userData = new JObject {
/// { "name", "홍길동" },
/// { "age", 20 }
/// };
///
/// if (_validator.IsValid(userData))
/// {
/// Console.WriteLine("유효한 사용자 데이터입니다.");
/// }
/// else
/// {
/// Console.WriteLine("유효하지 않은 사용자 데이터입니다.");
/// }
/// </code>
/// </example>
public class DataValidator
{
/// <summary>
/// 유효성 검사 규칙의 목록입니다.
/// 각 사전은 속성 이름(string)과 해당 속성의 검증 함수(Func<object, bool>)를 포함합니다.
/// </summary>
private List<Dictionary<string, Func<object?, bool>>> validaters = new List<Dictionary<string, Func<object?, bool>>>();
/// <summary>
/// 대용량 JSON 데이터를 스트리밍 방식으로 처리할지 여부를 나타내는 속성입니다.
/// 스트리밍 방식은 메모리 사용량을 줄이기 위해 대용량 데이터를 처리할 때 유용합니다.
/// </summary>
/// <remarks>
/// true로 설정하면 대용량 JSON 데이터를 처리할 때 스트리밍 방식을 사용합니다.
/// false로 설정하면 항상 전체 데이터를 메모리에 로드합니다.
/// </remarks>
public bool SupportsStreamParsing { get; internal set; } = true;
/// <summary>
/// 스트리밍 방식을 적용할 JSON 데이터의 최소 길이(바이트)입니다.
/// 데이터 길이가 이 값보다 크면 스트리밍 방식으로 처리합니다.
/// </summary>
/// <remarks>
/// 기본값은 10,000바이트입니다. 작은 데이터의 경우 스트리밍 처리가 오히려
/// 오버헤드를 발생시킬 수 있으므로, 특정 크기 이상일 때만 스트리밍 방식을 적용합니다.
/// </remarks>
public int SupportsStreamLength { get; internal set; } = 10000;
/// <summary>
/// 새로운 유효성 검사 규칙을 추가합니다.
/// </summary>
/// <param name="propertyName">검사할 속성 이름</param>
/// <param name="validator">유효성 검사 함수 (매개변수: 속성값, 반환값: 유효성 여부)</param>
/// <example>
/// <code>
/// var _validator = new DataValidator();
///
/// // 문자열 속성이 비어있지 않은지 확인하는 규칙
/// _validator.AddValidator("username", value =>
/// {
/// if (value is string str)
/// return !string.IsNullOrEmpty(str);
/// return false;
/// });
///
/// // 숫자 속성이 특정 범위 내에 있는지 확인하는 규칙
/// _validator.AddValidator("score", value =>
/// {
/// if (value is long num)
/// return num >= 0 && num <= 100;
/// return false;
/// });
/// </code>
/// </example>
public void AddValidator(string propertyName, Func<object?, bool> validator)
{
// 유효성 검사기를 추가합니다.
validaters.Add(new Dictionary<string, Func<object?, bool>> { { propertyName, validator } });
}
/// <summary>
/// 특정 속성에 대한 모든 유효성 검사 규칙을 제거합니다.
/// </summary>
/// <param name="propertyName">규칙을 제거할 속성 이름</param>
/// <example>
/// <code>
/// var _validator = new DataValidator();
/// _validator.AddValidator("age", value => (long)value >= 18);
///
/// // 검증 과정 중에 'age' 속성 검사가 더 이상 필요하지 않을 경우
/// _validator.RemoveValidator("age");
/// </code>
/// </example>
public void RemoveValidator(string propertyName)
{
// 유효성 검사기를 제거합니다.
validaters.RemoveAll(v => v.ContainsKey(propertyName));
}
/// <summary>
/// DataObject 객체가 모든 유효성 검사 규칙을 통과하는지 확인합니다.
/// </summary>
/// <param name="data">검사할 DataObject</param>
/// <returns>모든 규칙을 통과하면 true, 하나라도 실패하면 false</returns>
/// <example>
/// <code>
/// var _validator = new DataValidator();
/// _validator.AddValidator("email", value => {
/// var emailStr = value as string;
/// return emailStr != null && emailStr.Contains("@");
/// });
///
/// var userData = new DataObject();
/// userData["name"] = "홍길동";
/// userData["email"] = "hong@example.com";
///
/// bool isValid = _validator.IsValid(userData); // true 반환
/// </code>
/// </example>
public bool IsValid(DataObject? data)
{
// 데이터가 null인 경우 유효하지 않음
if (data == null)
{
return false;
}
// 각 유효성 검사기를 순회하며 검증을 수행합니다.
foreach (var validator in validaters)
{
foreach (var kvp in validator)
{
// 해당 속성이 데이터에 존재하는지 확인
if (!data.ContainsKey(kvp.Key))
{
continue; // 속성이 없으면 검증을 건너뜀
}
// 속성 값에 대한 검증을 수행
if (!kvp.Value(data[kvp.Key]))
{
// 검증에 실패하면 false를 반환
return false;
}
}
}
// 모든 검증을 통과하면 유효함
return true;
}
/// <summary>
/// JObject가 모든 유효성 검사 규칙을 통과하는지 확인합니다.
/// </summary>
/// <param name="data">검사할 JObject</param>
/// <returns>모든 규칙을 통과하면 true, 하나라도 실패하면 false</returns>
/// <example>
/// <code>
/// var _validator = new DataValidator();
///
/// // 사용자 나이가 18세 이상인지 확인하는 규칙
/// _validator.AddValidator("age", value =>
/// value is long age && age >= 18
/// );
///
/// // JSON 데이터 생성
/// var jsonObject = JObject.Parse(@"{
/// ""name"": ""김철수"",
/// ""age"": 16
/// }");
///
/// bool isValid = _validator.IsValid(jsonObject); // false 반환 (age가 18 미만)
/// </code>
/// </example>
public bool IsValid(JObject? data)
{
// 데이터가 null인 경우 유효하지 않음
if (data == null)
{
return false;
}
// 각 유효성 검사기를 순회하며 검증을 수행합니다.
foreach (var validator in validaters)
{
foreach (var kvp in validator)
{
// 해당 속성이 데이터에 존재하는지 확인
if (!data.ContainsKey(kvp.Key))
{
continue; // 속성이 없으면 검증을 건너뜀
}
// 속성 값에 대한 검증을 수행
if (!kvp.Value(ConvertJTokenToObject(data[kvp.Key])))
{
// 검증에 실패하면 false를 반환
return false;
}
}
}
// 모든 검증을 통과하면 유효함
return true;
}
/// <summary>
/// JSON 데이터 스트림이 모든 유효성 검사 규칙을 통과하는지 확인합니다.
/// 대용량 JSON 데이터를 메모리 효율적으로 처리할 수 있습니다.
/// </summary>
/// <param name="jsonStream">검사할 JSON 데이터 스트림</param>
/// <returns>모든 규칙을 통과하면 true, 하나라도 실패하면 false</returns>
/// <example>
/// <code>
/// var _validator = new DataValidator();
/// _validator.AddValidator("isActive", value => value is bool active && active == true);
///
/// // 파일에서 JSON 데이터 스트림을 읽어 유효성 검사
/// using (var fileStream = File.OpenRead("user-data.json"))
/// {
/// bool isValid = _validator.IsValid(fileStream);
/// Console.WriteLine($"데이터 유효성: {isValid}");
/// }
/// </code>
/// </example>
public bool IsValid(System.IO.Stream jsonStream)
{
if (jsonStream == null) return false;
// 스트림 처리 최적화를 위해 청크 단위로 읽을 수 있지만,
// 현재는 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<JObject>(reader);
return IsValid(sourceObject);
}
}
/// <summary>
/// JSON 배열에서 유효성 검사 규칙을 통과하는 항목만 필터링하여 반환합니다.
/// </summary>
/// <param name="data">필터링할 JSON 배열</param>
/// <returns>유효한 항목만 포함된 새 JSON 배열 (유효한 항목이 없으면 null)</returns>
/// <example>
/// <code>
/// var _validator = new DataValidator();
///
/// // 유효성 검사 규칙: status가 'active'인 항목만 유효
/// _validator.AddValidator("status", value =>
/// value is string status && status == "active"
/// );
///
/// // 필터링할 JSON 배열 생성
/// var usersArray = JArray.Parse(@"[
/// {""id"": 1, ""name"": ""홍길동"", ""status"": ""active""},
/// {""id"": 2, ""name"": ""김철수"", ""status"": ""inactive""},
/// {""id"": 3, ""name"": ""이영희"", ""status"": ""active""}
/// ]");
///
/// // 유효한 항목(status가 'active'인 항목)만 필터링
/// JArray? validUsers = _validator.GetValidData(usersArray);
///
/// // validUsers는 홍길동과 이영희의 데이터만 포함 (2개 항목)
/// Console.WriteLine($"유효한 사용자 수: {validUsers?.Count ?? 0}");
/// </code>
/// </example>
public JArray? GetValidData(JArray? data)
{
if (data == null || data.Count == 0)
{
return data; // 빈 배열은 유효하지 않음
}
JArray validData = new JArray();
foreach (var item in data)
{
if (item is JObject jObject && IsValid(jObject))
{
validData.Add(jObject);
}
}
return validData.Count > 0 ? validData : null; // 유효한 데이터가 없으면 null 반환
}
/// <summary>
/// JSON 데이터 스트림에서 유효성 검사 규칙을 통과하는 항목만 필터링하여 반환합니다.
/// 대용량 JSON 배열을 메모리 효율적으로 처리하기 위해 스트리밍 방식을 사용합니다.
/// </summary>
/// <param name="jsonStream">필터링할 JSON 배열 데이터 스트림</param>
/// <returns>유효한 항목만 포함된 JSON 배열 (유효한 항목이 없으면 null)</returns>
/// <example>
/// <code>
/// var _validator = new DataValidator();
///
/// // 필수 필드가 모두 있는지 확인하는 규칙 추가
/// _validator.AddValidator("name", value => value != null &amp;&amp; !string.IsNullOrEmpty(value.ToString()));
/// _validator.AddValidator("email", value => value != null &amp;&amp; value.ToString().Contains("@"));
///
/// // 파일에서 JSON 배열 데이터 스트림을 읽어 유효한 항목만 필터링
/// using (var fileStream = File.OpenRead("users-data.json"))
/// {
/// JArray? validUsers = _validator.GetValidData(fileStream);
///
/// if (validUsers != null)
/// {
/// Console.WriteLine($"유효한 사용자 수: {validUsers.Count}");
///
/// // 첫 번째 유효한 사용자 정보 출력
/// if (validUsers.Count > 0)
/// {
/// var firstUser = validUsers[0];
/// Console.WriteLine($"첫 번째 유효 사용자: {firstUser["name"]}");
/// }
/// }
/// else
/// {
/// Console.WriteLine("유효한 사용자가 없습니다.");
/// }
/// }
/// </code>
/// </example>
public JArray? GetValidData(System.IO.Stream jsonStream)
{
if (jsonStream == null) return null;
using (var reader = new Newtonsoft.Json.JsonTextReader(new System.IO.StreamReader(jsonStream)))
{
// 청크 읽기 설정 - 메모리 사용량 최적화
reader.SupportMultipleContent = true;
var serializer = new Newtonsoft.Json.JsonSerializer();
var sourceArray = serializer.Deserialize<JArray>(reader);
return GetValidData(sourceArray);
}
}
private object? ConvertJTokenToObject(JToken? token)
{
if(token == null || token.Type == JTokenType.Null)
{
return null; // null 처리
}
switch (token.Type)
{
case JTokenType.String:
return token.ToString();
case JTokenType.Integer:
return token.ToObject<int>();
case JTokenType.Float:
return token.ToObject<float>();
case JTokenType.Boolean:
return token.ToObject<bool>();
//case JTokenType.Object:
// return new DataObject((JObject)token);
//case JTokenType.Array:
// JArray array = (JArray)token;
// return new DataArray(array);
default:
return token.ToString();
}
}
}
}