// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Linq.Expressions; using System.Reflection; namespace EpicGames.BuildGraph { /// /// Value of an object /// public class BgObjectDef { /// /// Deserialized object value /// private object? _value; /// /// Properties for the object /// public ImmutableDictionary Properties { get; } /// /// Constructor /// public BgObjectDef() { Properties = ImmutableDictionary.Empty.WithComparers(StringComparer.Ordinal); } /// /// Constructor /// public BgObjectDef(ImmutableDictionary properties) { Properties = properties; } /// /// Sets a property value /// /// Name of the property /// New value for the property /// Object definition with the new properties public BgObjectDef Set(string name, object? value) { return new BgObjectDef(Properties.SetItem(name, value)); } /// /// Gets a property value /// /// Name of the property /// Default value to return if the property is not set /// public object? Get(string name, object? defaultValue) { object? value; if (!Properties.TryGetValue(name, out value)) { value = defaultValue; } return value; } /// /// Creates a strongly typed object instance /// /// Type of the object /// public BgObjectDef WithType() => new BgObjectDef(Properties); /// /// Cache of serializer instances /// static readonly ConcurrentDictionary s_typeToSerializer = new ConcurrentDictionary(); /// /// Deserialize an object of the given type, using the default serializer /// /// Type of the object to create /// Deserialized instance of the type public object Deserialize(Type type) { if (_value == null) { BgObjectSerializer? serializer; if (!s_typeToSerializer.TryGetValue(type, out serializer)) { Type serializerType = type.GetCustomAttribute()?.SerializerType ?? typeof(BgDefaultObjectSerializer<>).MakeGenericType(type); serializer = (BgObjectSerializer)Activator.CreateInstance(serializerType)!; serializer = s_typeToSerializer.GetOrAdd(type, serializer); } _value = serializer.Deserialize(this); } return _value; } /// /// Deserialize an object of the given type /// /// Type to deserialize /// Deserialized instance of the type public T Deserialize() => (T)Deserialize(typeof(T)); } /// /// Strongly typed instance of /// /// public class BgObjectDef : BgObjectDef { class PropertySetter { protected PropertyInfo Property { get; } public PropertySetter(PropertyInfo property) => Property = property; public virtual void SetValue(object instance, object? value) => Property.SetValue(instance, ConvertValue(value, Property.PropertyType)); } class CollectionPropertySetter : PropertySetter { public CollectionPropertySetter(PropertyInfo property) : base(property) { } public override void SetValue(object instance, object? value) { ICollection? list = (ICollection?)Property.GetValue(instance)!; if (list == null) { list = (ICollection)Activator.CreateInstance(Property.PropertyType)!; Property.SetValue(instance, list); } IEnumerable elements = (IEnumerable)value!; foreach (object element in elements) { if (element is IEnumerable range) { foreach (object? rangeElement in range) { list.Add((TElement)ConvertValue(rangeElement, typeof(TElement))!); } } else { list.Add((TElement)ConvertValue(element, typeof(TElement))!); } } } } static readonly IReadOnlyDictionary s_nameToSetter = GetPropertyMap(); /// /// Constructor /// public BgObjectDef() { } /// /// Constructor /// public BgObjectDef(ImmutableDictionary properties) : base(properties) { } /// /// Gets a property value /// /// /// /// /// public TValue Get(Expression> property, TValue defaultValue) { MemberExpression member = ((MemberExpression)property.Body); PropertyInfo propertyInfo = (PropertyInfo)member.Member; string name = propertyInfo.GetCustomAttribute()?.Name ?? propertyInfo.Name; return (TValue)ConvertValue(Get(name, defaultValue), typeof(TValue))!; } /// /// Copies properties from the object value to an instance /// /// public void CopyTo(object instance) { foreach ((string name, object? value) in Properties) { if (s_nameToSetter.TryGetValue(name, out PropertySetter? propertySetter)) { propertySetter.SetValue(instance, value); } } } /// /// Convert a value to a particular type /// /// Value to convert /// Target type /// The converted value static object? ConvertValue(object? value, Type type) { if (value == null) { return null; } else if (value.GetType() == type) { return value; } else if (type.IsClass && type != typeof(string) && type != typeof(BgNodeOutput)) { return ((BgObjectDef)value).Deserialize(type); } else { return value; } } /// /// Creates a map from name to property info /// /// static IReadOnlyDictionary GetPropertyMap() { Dictionary nameToProperty = new Dictionary(StringComparer.Ordinal); PropertyInfo[] properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance); foreach (PropertyInfo property in properties) { PropertySetter? setter = CreateSetter(property); if (setter != null) { BgPropertyAttribute? attribute = property.GetCustomAttribute(); string name = attribute?.Name ?? property.Name; nameToProperty[name] = setter; } } return nameToProperty; } static PropertySetter? CreateSetter(PropertyInfo property) { Type? collectionInterface = property.PropertyType.GetInterfaces().FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(ICollection<>)); if (collectionInterface != null) { Type setterType = typeof(CollectionPropertySetter<>).MakeGenericType(typeof(T), collectionInterface.GetGenericArguments()[0]); return (PropertySetter)Activator.CreateInstance(setterType, property)!; } if (property.SetMethod != null) { return new PropertySetter(property); } return null; } } /// /// Attribute marking that a property should be serialized to BuildGraph /// [AttributeUsage(AttributeTargets.Property)] public sealed class BgPropertyAttribute : Attribute { /// /// Name of the property. If unspecified, the property name will be used. /// public string? Name { get; } /// /// Constructor /// public BgPropertyAttribute(string? name = null) { Name = name; } } /// /// Attribute marking the type of serializer for an object /// [AttributeUsage(AttributeTargets.Class)] public sealed class BgObjectAttribute : Attribute { /// /// The serailizer to use for this object /// public Type SerializerType { get; } /// /// Constructor /// /// The serializer type public BgObjectAttribute(Type serializerType) { SerializerType = serializerType; } } /// /// Base class for deserializing objects from untyped instances /// public abstract class BgObjectSerializer { /// /// Deserialize an object from the given property bag /// /// Properties for the object /// New object instance public abstract object Deserialize(BgObjectDef obj); } /// /// Strongly typed base class for deserializing objects /// /// The object type public abstract class BgObjectSerializer : BgObjectSerializer { /// public sealed override object Deserialize(BgObjectDef obj) => Deserialize(obj.WithType())!; /// /// Typed deserialization method /// /// Properties for the object /// Object instance public abstract T Deserialize(BgObjectDef obj); } /// /// Default serializer for objects with a default constructor /// /// Object type public class BgDefaultObjectSerializer : BgObjectSerializer where T : new() { /// public override T Deserialize(BgObjectDef obj) { T result = new T(); obj.CopyTo(result); return result; } } }