// MIT License - Copyright (c) 2023 wallstop
// Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE
namespace WallstopStudios.UnityHelpers.Core.Attributes
{
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.CompilerServices;
using Extension;
using Helper;
using UnityEngine;
using WallstopStudios.UnityHelpers.Utils;
using Object = UnityEngine.Object;
///
/// Specifies the type of message displayed in the inspector when a field fails validation.
///
public enum ValidateAssignmentMessageType
{
///
/// Displays as a warning (yellow) in the inspector.
///
Warning = 0,
///
/// Displays as an error (red) in the inspector.
///
Error = 1,
}
///
/// Validates that a field is properly assigned at edit time. Displays a warning or error in the inspector
/// when the field is null, empty (for strings), or has no elements (for collections).
/// Use to log warnings for all invalid fields,
/// or to check validity programmatically.
///
///
/// Unlike , this attribute validates:
///
/// - Object references (null check)
/// - Strings (empty or whitespace check)
/// - Lists and Collections (empty check)
/// - Enumerables (empty check)
///
///
///
///
/// public class EnemySpawner : MonoBehaviour
/// {
/// [ValidateAssignment]
/// public GameObject enemyPrefab;
///
/// [ValidateAssignment(ValidateAssignmentMessageType.Error, "Spawn points list cannot be empty")]
/// public List<Transform> spawnPoints;
///
/// private void Start()
/// {
/// this.ValidateAssignments();
/// }
/// }
///
///
[AttributeUsage(AttributeTargets.Field)]
public sealed class ValidateAssignmentAttribute : PropertyAttribute
{
///
/// The type of message to display in the inspector when the field is invalid.
///
public ValidateAssignmentMessageType MessageType { get; }
///
/// An optional custom message to display in the inspector when the field is invalid.
/// If null or empty, a default message will be generated based on the field name.
///
public string CustomMessage { get; }
///
/// Creates a new ValidateAssignment attribute with default settings (warning message type, auto-generated message).
///
public ValidateAssignmentAttribute()
: this(ValidateAssignmentMessageType.Warning, null) { }
///
/// Creates a new ValidateAssignment attribute with the specified message type and auto-generated message.
///
/// The type of message to display when invalid.
public ValidateAssignmentAttribute(ValidateAssignmentMessageType messageType)
: this(messageType, null) { }
///
/// Creates a new ValidateAssignment attribute with default message type (warning) and a custom message.
///
/// The custom message to display when invalid.
public ValidateAssignmentAttribute(string customMessage)
: this(ValidateAssignmentMessageType.Warning, customMessage) { }
///
/// Creates a new ValidateAssignment attribute with the specified message type and custom message.
///
/// The type of message to display when invalid.
/// The custom message to display when invalid.
public ValidateAssignmentAttribute(
ValidateAssignmentMessageType messageType,
string customMessage
)
{
MessageType = messageType;
CustomMessage = customMessage;
}
}
public static class ValidateAssignmentExtensions
{
private static readonly Dictionary FieldsByType = new();
private static FieldInfo[] GetOrAdd(Type objectType)
{
return FieldsByType.GetOrAdd(
objectType,
type =>
{
FieldInfo[] allFields = type.GetFields(
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic
);
if (allFields.Length == 0)
{
return Array.Empty();
}
using PooledResource> bufferResource =
Buffers.List.Get(out List result);
for (int i = 0; i < allFields.Length; i++)
{
FieldInfo field = allFields[i];
if (
field.IsAttributeDefined(
out _,
inherit: false
)
)
{
result.Add(field);
}
}
if (result.Count == 0)
{
return Array.Empty();
}
return result.ToArray();
}
);
}
public static void ValidateAssignments(this Object o)
{
#if UNITY_EDITOR
if (o == null)
{
return;
}
Type objectType = o.GetType();
FieldInfo[] fields = GetOrAdd(objectType);
foreach (FieldInfo field in fields)
{
bool logNotAssigned = IsFieldInvalid(field, o);
if (logNotAssigned)
{
o.LogNotAssigned(field.Name);
}
}
#endif
}
public static bool AreAnyAssignmentsInvalid(this Object o)
{
Type objectType = o.GetType();
FieldInfo[] fields = GetOrAdd(objectType);
foreach (FieldInfo field in fields)
{
if (IsFieldInvalid(field, o))
{
return true;
}
}
return false;
}
private static bool IsInvalid(IEnumerable enumerable)
{
try
{
IEnumerator enumerator = enumerable.GetEnumerator();
try
{
return !enumerator.MoveNext();
}
finally
{
if (enumerator is IDisposable disposable)
{
disposable.Dispose();
}
}
}
catch
{
return true;
}
}
private static bool IsFieldInvalid(FieldInfo field, Object o)
{
object fieldValue = field.GetValue(o);
return IsValueInvalid(fieldValue);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static bool IsValueInvalid(object value)
{
return value switch
{
Object unityObject => unityObject == null,
string stringValue => string.IsNullOrWhiteSpace(stringValue),
IList list => list.Count <= 0,
ICollection collection => collection.Count <= 0,
IEnumerable enumerable => IsInvalid(enumerable),
_ => value == null,
};
}
}
}