mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 03:30:53 +01:00
236 lines
8.8 KiB
C#
236 lines
8.8 KiB
C#
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Linq.Expressions;
|
|
using Robust.Shared.Serialization.Manager.Definition;
|
|
using Robust.Shared.Serialization.Markdown;
|
|
using Robust.Shared.Serialization.Markdown.Mapping;
|
|
using Robust.Shared.Serialization.Markdown.Sequence;
|
|
using Robust.Shared.Serialization.Markdown.Validation;
|
|
using Robust.Shared.Serialization.Markdown.Value;
|
|
using Robust.Shared.Serialization.TypeSerializers.Interfaces;
|
|
using Robust.Shared.Utility;
|
|
|
|
namespace Robust.Shared.Serialization.Manager;
|
|
|
|
public sealed partial class SerializationManager
|
|
{
|
|
#region DelegateElements
|
|
|
|
private delegate ValidationNode ValidationDelegate(DataNode node, ISerializationContext? context);
|
|
|
|
private readonly ConcurrentDictionary<(Type type, Type node), ValidationDelegate> _validationDelegates = new();
|
|
|
|
private ValidationDelegate GetOrCreateValidationDelegate(Type type, Type node)
|
|
{
|
|
return _validationDelegates.GetOrAdd((type, node), static (key, manager) =>
|
|
{
|
|
var managerConst = Expression.Constant(manager);
|
|
var nodeParam = Expression.Parameter(typeof(DataNode), "node");
|
|
var contextParam = Expression.Parameter(typeof(ISerializationContext), "context");
|
|
|
|
Expression call;
|
|
bool couldRead = true;
|
|
|
|
if (manager._regularSerializerProvider.TryGetTypeNodeSerializer(typeof(ITypeValidator<,>), key.type, key.node, out var serializer))
|
|
{
|
|
var serializerConst = Expression.Constant(serializer);
|
|
|
|
call = Expression.Call(
|
|
managerConst,
|
|
nameof(ValidateNode),
|
|
new []{key.type, key.node},
|
|
serializerConst,
|
|
Expression.Convert(nodeParam, key.node),
|
|
contextParam);
|
|
}
|
|
else if (key.type.IsArray)
|
|
{
|
|
if (!key.node.IsAssignableTo(typeof(SequenceDataNode)))
|
|
{
|
|
call = manager.ErrorNodeExpression(nodeParam, "Invalid nodetype for array.", true);
|
|
}
|
|
else
|
|
{
|
|
var elementType = key.type.GetElementType();
|
|
if (elementType == null)
|
|
throw new ArgumentException($"Failed to get ElementType of ArrayType {key.type}");
|
|
|
|
call = Expression.Call(
|
|
managerConst,
|
|
nameof(ValidateArray),
|
|
new[] { elementType },
|
|
Expression.Convert(nodeParam, typeof(SequenceDataNode)),
|
|
contextParam);
|
|
}
|
|
}
|
|
else if (key.type.IsEnum)
|
|
{
|
|
// Does not include cases where the target type is System.Enum.
|
|
// Those get handled by the generic enum serializer which uses reflection to resolve strings into enums.
|
|
DebugTools.Assert(key.type != typeof(Enum));
|
|
|
|
call = Expression.Call(
|
|
managerConst,
|
|
nameof(ValidateEnum),
|
|
new[] { key.type },
|
|
nodeParam);
|
|
}
|
|
else if (key.type.IsAssignableTo(typeof(ISelfSerialize)))
|
|
{
|
|
if (key.node.IsAssignableTo(typeof(ValueDataNode)))
|
|
{
|
|
call = manager.ValidateNodeExpression(nodeParam);
|
|
}
|
|
else
|
|
{
|
|
call = manager.ErrorNodeExpression(nodeParam, "Invalid nodetype for ISelfSerialize");
|
|
}
|
|
}
|
|
else if (manager.TryGetDefinition(key.type, out var dataDefinition))
|
|
{
|
|
var dataDefConst = Expression.Constant(dataDefinition, typeof(DataDefinition<>).MakeGenericType(key.type));
|
|
|
|
call = Expression.Call(
|
|
managerConst,
|
|
nameof(ValidateDataDefinition),
|
|
new []{key.type},
|
|
nodeParam,
|
|
dataDefConst,
|
|
contextParam);
|
|
}
|
|
else
|
|
{
|
|
call = manager.ErrorNodeExpression(nodeParam, "Failed to read node.", false);
|
|
couldRead = false;
|
|
}
|
|
|
|
if (couldRead)
|
|
{
|
|
//insert a nullcheck at the beginning, but ONLY if we are actually found a way of validating this node
|
|
call = Expression.Condition(
|
|
Expression.Call(
|
|
typeof(SerializationManager),
|
|
nameof(IsNull),
|
|
Type.EmptyTypes,
|
|
nodeParam),
|
|
Expression.Convert(key.type.IsNullable()
|
|
? manager.ValidateNodeExpression(nodeParam)
|
|
: manager.ErrorNodeExpression(nodeParam, "Non-nullable field contained a null value", true), typeof(ValidationNode)),
|
|
Expression.Convert(call, typeof(ValidationNode)));
|
|
}
|
|
|
|
|
|
return Expression.Lambda<ValidationDelegate>(
|
|
call,
|
|
nodeParam,
|
|
contextParam).Compile();
|
|
}, this);
|
|
}
|
|
|
|
private Expression ErrorNodeExpression(ParameterExpression nodeParam, string message, bool alwaysRelevant = true)
|
|
{
|
|
return ExpressionUtils.NewExpression<ErrorNode>(nodeParam, message, alwaysRelevant);
|
|
}
|
|
|
|
private Expression ValidateNodeExpression(ParameterExpression nodeParam)
|
|
{
|
|
return ExpressionUtils.NewExpression<ValidatedValueNode>(nodeParam);
|
|
}
|
|
|
|
private ValidationNode ValidateArray<TElem>(SequenceDataNode sequenceDataNode, ISerializationContext? context)
|
|
{
|
|
var validatedList = new List<ValidationNode>();
|
|
foreach (var dataNode in sequenceDataNode.Sequence)
|
|
{
|
|
validatedList.Add(ValidateNode<TElem>(dataNode, context));
|
|
}
|
|
|
|
return new ValidatedSequenceNode(validatedList);
|
|
|
|
}
|
|
|
|
private ValidationNode ValidateEnum<T>(DataNode node)
|
|
{
|
|
var enumName = node switch
|
|
{
|
|
ValueDataNode valueNode => valueNode.Value,
|
|
SequenceDataNode sequenceNode => string.Join(", ", sequenceNode.Sequence),
|
|
_ => null
|
|
};
|
|
|
|
if (enumName == null)
|
|
{
|
|
return new ErrorNode(node, $"Invalid node type {node.GetType()} for enum {typeof(T)}.");
|
|
}
|
|
|
|
if (!Enum.TryParse(typeof(T), enumName, true, out var enumValue))
|
|
{
|
|
return new ErrorNode(node, $"{enumValue} is not a valid enum value of type {typeof(T)}", false);
|
|
}
|
|
|
|
return new ValidatedValueNode(node);
|
|
}
|
|
|
|
private ValidationNode ValidateDataDefinition<T>(DataNode node, DataDefinition<T> dataDefinition, ISerializationContext? context) where T : notnull
|
|
{
|
|
return node switch
|
|
{
|
|
ValueDataNode valueDataNode => valueDataNode.Value == ""
|
|
? new ValidatedValueNode(valueDataNode)
|
|
: new ErrorNode(node, "Invalid NodeType for DataDefinition", false),
|
|
MappingDataNode mappingDataNode => dataDefinition.Validate(this, mappingDataNode, context),
|
|
_ => new ErrorNode(node, "Invalid NodeType for DataDefinition")
|
|
};
|
|
}
|
|
|
|
#endregion
|
|
|
|
public ValidationNode ValidateNode<T>(DataNode node, ISerializationContext? context = null)
|
|
{
|
|
return ValidateNode(typeof(T), node, context);
|
|
}
|
|
|
|
public ValidationNode ValidateNode<T, TNode>(ITypeValidator<T, TNode> typeValidator, TNode node,
|
|
ISerializationContext? context = null) where TNode : DataNode
|
|
{
|
|
return typeValidator.Validate(this, node, DependencyCollection, context);
|
|
}
|
|
|
|
public ValidationNode ValidateNode<T, TNode, TValidator>(TNode node,
|
|
ISerializationContext? context = null) where TNode : DataNode where TValidator : ITypeValidator<T, TNode>
|
|
{
|
|
return ValidateNode(GetOrCreateCustomTypeSerializer<TValidator>(), node, context);
|
|
}
|
|
|
|
public ValidationNode ValidateNode(Type type, DataNode node, ISerializationContext? context = null)
|
|
{
|
|
var underlyingType = type.GetUnderlyingType();
|
|
|
|
if (underlyingType != null) // implies that type was nullable
|
|
{
|
|
if (IsNull(node))
|
|
return new ValidatedValueNode(node);
|
|
}
|
|
else
|
|
{
|
|
underlyingType = type;
|
|
}
|
|
|
|
if (node.Tag?.StartsWith("!type:") == true)
|
|
{
|
|
var typeString = node.Tag.Substring(6);
|
|
try
|
|
{
|
|
underlyingType = ResolveConcreteType(underlyingType, typeString);
|
|
}
|
|
catch (InvalidOperationException)
|
|
{
|
|
return new ErrorNode(node, $"Failed to resolve !type tag: {typeString}", false);
|
|
}
|
|
}
|
|
|
|
return GetOrCreateValidationDelegate(underlyingType, node.GetType())(node, context);
|
|
}
|
|
}
|