Add FieldLoader/Saver support for ImmutableArray, FrozenSet, FrozenDictionary.
As config is often meant to be loaded and then never modified, it is helpful to support collections that are read-only after creation. This allows various classes that load config from YAML and elsewhere to deserialize straight into read-only collections and enforce invariants around data mutation.
This commit is contained in:
committed by
Paul Chote
parent
e53f2b6044
commit
797c71e500
@@ -10,7 +10,9 @@
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
@@ -99,6 +101,9 @@ namespace OpenRA
|
||||
{ typeof(HashSet<>), ParseHashSetOrList },
|
||||
{ typeof(List<>), ParseHashSetOrList },
|
||||
{ typeof(Dictionary<,>), ParseDictionary },
|
||||
{ typeof(ImmutableArray<>), ParseImmutableArray },
|
||||
{ typeof(FrozenSet<>), ParseFrozenSet },
|
||||
{ typeof(FrozenDictionary<,>), ParseFrozenDictionary },
|
||||
{ typeof(BitSet<>), ParseBitSet },
|
||||
{ typeof(Nullable<>), ParseNullable },
|
||||
};
|
||||
@@ -107,6 +112,24 @@ namespace OpenRA
|
||||
static readonly object BoxedFalse = false;
|
||||
static readonly object[] BoxedInts = Exts.MakeArray(33, i => (object)i);
|
||||
|
||||
static readonly MethodInfo ToImmutableArray =
|
||||
typeof(ImmutableArray)
|
||||
.GetMethods()
|
||||
.Single(m =>
|
||||
m.Name == nameof(ImmutableArray.ToImmutableArray) &&
|
||||
m.GetParameters()?.First().ParameterType.GetGenericTypeDefinition() == typeof(IEnumerable<>));
|
||||
|
||||
static readonly MethodInfo ToFrozenSet =
|
||||
typeof(FrozenSet)
|
||||
.GetMethod(nameof(FrozenSet.ToFrozenSet));
|
||||
|
||||
static readonly MethodInfo ToFrozenDictionary =
|
||||
typeof(FrozenDictionary)
|
||||
.GetMethods()
|
||||
.Single(m =>
|
||||
m.Name == nameof(FrozenDictionary.ToFrozenDictionary) &&
|
||||
m.GetParameters().Length == 2);
|
||||
|
||||
static object ParseInt(string fieldName, Type fieldType, string value)
|
||||
{
|
||||
if (Exts.TryParseInt32Invariant(value, out var res))
|
||||
@@ -508,6 +531,24 @@ namespace OpenRA
|
||||
return InvalidValueAction(value, fieldType, fieldName);
|
||||
}
|
||||
|
||||
static object ParseArray(string fieldName, Type fieldType, string value)
|
||||
{
|
||||
var elementType = fieldType.GetElementType();
|
||||
|
||||
if (value == null)
|
||||
return typeof(Array)
|
||||
.GetMethod(nameof(Array.Empty))
|
||||
.MakeGenericMethod(elementType)
|
||||
.Invoke(null, null);
|
||||
|
||||
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
|
||||
var ret = Array.CreateInstance(elementType, parts.Length);
|
||||
for (var i = 0; i < parts.Length; i++)
|
||||
ret.SetValue(GetValue(fieldName, elementType, parts[i]), i);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static object ParseHashSetOrList(string fieldName, Type fieldType, string value, MiniYaml yaml)
|
||||
{
|
||||
if (value == null)
|
||||
@@ -546,6 +587,66 @@ namespace OpenRA
|
||||
return dict;
|
||||
}
|
||||
|
||||
static object ParseImmutableArray(string fieldName, Type fieldType, string value, MiniYaml yaml)
|
||||
{
|
||||
var typeArgs = fieldType.GenericTypeArguments;
|
||||
|
||||
if (value == null)
|
||||
return typeof(ImmutableArray<>).MakeGenericType(typeArgs)
|
||||
.GetField(nameof(ImmutableArray<object>.Empty))
|
||||
.GetValue(null);
|
||||
|
||||
object array;
|
||||
if (typeArgs[0] == typeof(WVec))
|
||||
array = ParseWVecArray(fieldName, typeArgs[0].MakeArrayType(), value);
|
||||
else if (typeArgs[0] == typeof(CPos))
|
||||
array = ParseCPosArray(fieldName, typeArgs[0].MakeArrayType(), value);
|
||||
else if (typeArgs[0] == typeof(CVec))
|
||||
array = ParseCVecArray(fieldName, typeArgs[0].MakeArrayType(), value);
|
||||
else if (typeArgs[0] == typeof(int2))
|
||||
array = ParseInt2Array(fieldName, typeArgs[0].MakeArrayType(), value);
|
||||
else
|
||||
array = ParseArray(fieldName, typeArgs[0].MakeArrayType(), value);
|
||||
|
||||
var toImmutableArray = ToImmutableArray.MakeGenericMethod(typeArgs);
|
||||
|
||||
return toImmutableArray.Invoke(null, [array]);
|
||||
}
|
||||
|
||||
static object ParseFrozenSet(string fieldName, Type fieldType, string value, MiniYaml yaml)
|
||||
{
|
||||
var typeArgs = fieldType.GenericTypeArguments;
|
||||
|
||||
if (value == null)
|
||||
return typeof(FrozenSet<>).MakeGenericType(typeArgs)
|
||||
.GetProperty(nameof(FrozenSet<object>.Empty))
|
||||
.GetValue(null);
|
||||
|
||||
var set =
|
||||
ParseHashSetOrList(fieldName, typeof(HashSet<>).MakeGenericType(typeArgs), value, yaml);
|
||||
|
||||
var toFrozenSet = ToFrozenSet.MakeGenericMethod(typeArgs);
|
||||
|
||||
return toFrozenSet.Invoke(null, [set, null]);
|
||||
}
|
||||
|
||||
static object ParseFrozenDictionary(string fieldName, Type fieldType, string value, MiniYaml yaml)
|
||||
{
|
||||
var typeArgs = fieldType.GenericTypeArguments;
|
||||
|
||||
if (yaml == null)
|
||||
return typeof(FrozenDictionary<,>).MakeGenericType(typeArgs)
|
||||
.GetProperty(nameof(FrozenDictionary<object, object>.Empty))
|
||||
.GetValue(null);
|
||||
|
||||
var dict =
|
||||
ParseDictionary(fieldName, typeof(Dictionary<,>).MakeGenericType(typeArgs), value, yaml);
|
||||
|
||||
var toFrozenDict = ToFrozenDictionary.MakeGenericMethod(typeArgs);
|
||||
|
||||
return toFrozenDict.Invoke(null, [dict, null]);
|
||||
}
|
||||
|
||||
static object ParseBitSet(string fieldName, Type fieldType, string value, MiniYaml yaml)
|
||||
{
|
||||
if (value != null)
|
||||
@@ -685,17 +786,7 @@ namespace OpenRA
|
||||
return parseFunc(fieldName, fieldType, value);
|
||||
|
||||
if (fieldType.IsArray && fieldType.GetArrayRank() == 1)
|
||||
{
|
||||
if (value == null)
|
||||
return Array.CreateInstance(fieldType.GetElementType(), 0);
|
||||
|
||||
var parts = value.Split(Comma, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
|
||||
var ret = Array.CreateInstance(fieldType.GetElementType(), parts.Length);
|
||||
for (var i = 0; i < parts.Length; i++)
|
||||
ret.SetValue(GetValue(fieldName, fieldType.GetElementType(), parts[i]), i);
|
||||
return ret;
|
||||
}
|
||||
return ParseArray(fieldName, fieldType, value);
|
||||
|
||||
if (fieldType.IsEnum)
|
||||
return ParseEnum(fieldName, fieldType, value);
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
@@ -78,11 +80,35 @@ namespace OpenRA
|
||||
if (t.IsArray && t.GetArrayRank() == 1)
|
||||
return ((Array)v).Cast<object>().Select(FormatValue).JoinWith(", ");
|
||||
|
||||
if (t.IsGenericType && (t.GetGenericTypeDefinition() == typeof(HashSet<>) || t.GetGenericTypeDefinition() == typeof(List<>)))
|
||||
if (t.IsGenericType &&
|
||||
t.GetGenericTypeDefinition() == typeof(ImmutableArray<>))
|
||||
{
|
||||
try
|
||||
{
|
||||
return ((System.Collections.IEnumerable)v).Cast<object>().Select(FormatValue).JoinWith(", ");
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
if (t.IsGenericType &&
|
||||
(t.GetGenericTypeDefinition() == typeof(List<>) ||
|
||||
t.GetGenericTypeDefinition() == typeof(HashSet<>) ||
|
||||
t.GetGenericTypeDefinition()
|
||||
.BaseTypes()
|
||||
.Select(bt => bt.IsGenericType ? bt.GetGenericTypeDefinition() : null)
|
||||
.Any(bt => bt == typeof(FrozenSet<>))))
|
||||
return ((System.Collections.IEnumerable)v).Cast<object>().Select(FormatValue).JoinWith(", ");
|
||||
|
||||
// This is only for documentation generation
|
||||
if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Dictionary<,>))
|
||||
if (t.IsGenericType &&
|
||||
(t.GetGenericTypeDefinition() == typeof(Dictionary<,>) ||
|
||||
t.GetGenericTypeDefinition()
|
||||
.BaseTypes()
|
||||
.Select(bt => bt.IsGenericType ? bt.GetGenericTypeDefinition() : null)
|
||||
.Any(bt => bt == typeof(FrozenDictionary<,>))))
|
||||
{
|
||||
var result = new StringBuilder();
|
||||
var dict = (System.Collections.IDictionary)v;
|
||||
|
||||
@@ -274,15 +274,22 @@ namespace OpenRA.Mods.Common.Lint
|
||||
foreach (var o in (IEnumerable<object>)fieldValue)
|
||||
ExtractFluentKeys(modData, o, prefix, keys);
|
||||
|
||||
if (type.IsGenericType &&
|
||||
(type.GetGenericTypeDefinition() == typeof(Dictionary<,>) ||
|
||||
type.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>)))
|
||||
Type dictionaryInterface = null;
|
||||
if (type.IsGenericType)
|
||||
{
|
||||
if (type.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>))
|
||||
dictionaryInterface = type;
|
||||
else
|
||||
dictionaryInterface = type.GetInterface(typeof(IReadOnlyDictionary<,>).FullName);
|
||||
}
|
||||
|
||||
if (dictionaryInterface != null)
|
||||
{
|
||||
// Use an intermediate list to cover the unlikely case where both keys and values are lintable.
|
||||
if (dictionaryReference.HasFlag(LintDictionaryReference.Keys))
|
||||
{
|
||||
IEnumerable fieldKeys = ((IDictionary)fieldValue).Keys;
|
||||
if (typeof(IEnumerable<object>).IsAssignableFrom(type.GenericTypeArguments[0]))
|
||||
if (typeof(IEnumerable<object>).IsAssignableFrom(dictionaryInterface.GenericTypeArguments[0]))
|
||||
fieldKeys = ((ICollection<IEnumerable<object>>)fieldKeys).SelectMany(v => v);
|
||||
|
||||
foreach (var k in fieldKeys)
|
||||
@@ -292,7 +299,7 @@ namespace OpenRA.Mods.Common.Lint
|
||||
if (dictionaryReference.HasFlag(LintDictionaryReference.Values))
|
||||
{
|
||||
IEnumerable fieldValues = ((IDictionary)fieldValue).Values;
|
||||
if (typeof(IEnumerable<object>).IsAssignableFrom(type.GenericTypeArguments[1]))
|
||||
if (typeof(IEnumerable<object>).IsAssignableFrom(dictionaryInterface.GenericTypeArguments[1]))
|
||||
fieldValues = ((ICollection<IEnumerable<object>>)fieldValues).SelectMany(v => v);
|
||||
|
||||
foreach (var v in fieldValues)
|
||||
|
||||
@@ -36,19 +36,26 @@ namespace OpenRA.Mods.Common.Lint
|
||||
return expr != null ? expr.Variables : [];
|
||||
}
|
||||
|
||||
if (type.IsGenericType &&
|
||||
(type.GetGenericTypeDefinition() == typeof(Dictionary<,>) ||
|
||||
type.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>)))
|
||||
Type dictionaryInterface = null;
|
||||
if (type.IsGenericType)
|
||||
{
|
||||
if (type.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>))
|
||||
dictionaryInterface = type;
|
||||
else
|
||||
dictionaryInterface = type.GetInterface(typeof(IReadOnlyDictionary<,>).FullName);
|
||||
}
|
||||
|
||||
if (dictionaryInterface != null)
|
||||
{
|
||||
// Use an intermediate list to cover the unlikely case where both keys and values are lintable.
|
||||
var dictionaryValues = new List<string>();
|
||||
if (dictionaryReference.HasFlag(LintDictionaryReference.Keys) && type.GenericTypeArguments[0] == typeof(string))
|
||||
if (dictionaryReference.HasFlag(LintDictionaryReference.Keys) && dictionaryInterface.GenericTypeArguments[0] == typeof(string))
|
||||
dictionaryValues.AddRange((IEnumerable<string>)((IDictionary)fieldInfo.GetValue(ruleInfo)).Keys);
|
||||
|
||||
if (dictionaryReference.HasFlag(LintDictionaryReference.Values) && type.GenericTypeArguments[1] == typeof(string))
|
||||
if (dictionaryReference.HasFlag(LintDictionaryReference.Values) && dictionaryInterface.GenericTypeArguments[1] == typeof(string))
|
||||
dictionaryValues.AddRange((IEnumerable<string>)((IDictionary)fieldInfo.GetValue(ruleInfo)).Values);
|
||||
|
||||
if (dictionaryReference.HasFlag(LintDictionaryReference.Values) && type.GenericTypeArguments[1] == typeof(IEnumerable<string>))
|
||||
if (dictionaryReference.HasFlag(LintDictionaryReference.Values) && dictionaryInterface.GenericTypeArguments[1] == typeof(IEnumerable<string>))
|
||||
foreach (var row in (IEnumerable<IEnumerable<string>>)((IDictionary)fieldInfo.GetValue(ruleInfo)).Values)
|
||||
dictionaryValues.AddRange(row);
|
||||
|
||||
@@ -58,9 +65,6 @@ namespace OpenRA.Mods.Common.Lint
|
||||
var supportedTypes = new[]
|
||||
{
|
||||
"string", "IEnumerable<string>",
|
||||
"Dictionary<string, T> (LintDictionaryReference.Keys)",
|
||||
"Dictionary<T, string> (LintDictionaryReference.Values)",
|
||||
"Dictionary<T, IEnumerable<string>> (LintDictionaryReference.Values)",
|
||||
"IReadOnlyDictionary<string, T> (LintDictionaryReference.Keys)",
|
||||
"IReadOnlyDictionary<T, string> (LintDictionaryReference.Values)",
|
||||
"IReadOnlyDictionary<T, IEnumerable<string>> (LintDictionaryReference.Values)",
|
||||
@@ -88,17 +92,26 @@ namespace OpenRA.Mods.Common.Lint
|
||||
return expr != null ? expr.Variables : [];
|
||||
}
|
||||
|
||||
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>))
|
||||
Type dictionaryInterface = null;
|
||||
if (type.IsGenericType)
|
||||
{
|
||||
if (type.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>))
|
||||
dictionaryInterface = type;
|
||||
else
|
||||
dictionaryInterface = type.GetInterface(typeof(IReadOnlyDictionary<,>).FullName);
|
||||
}
|
||||
|
||||
if (dictionaryInterface != null)
|
||||
{
|
||||
// Use an intermediate list to cover the unlikely case where both keys and values are lintable.
|
||||
var dictionaryValues = new List<string>();
|
||||
if (dictionaryReference.HasFlag(LintDictionaryReference.Keys) && type.GenericTypeArguments[0] == typeof(string))
|
||||
if (dictionaryReference.HasFlag(LintDictionaryReference.Keys) && dictionaryInterface.GenericTypeArguments[0] == typeof(string))
|
||||
dictionaryValues.AddRange((IEnumerable<string>)((IDictionary)propertyInfo.GetValue(ruleInfo)).Keys);
|
||||
|
||||
if (dictionaryReference.HasFlag(LintDictionaryReference.Values) && type.GenericTypeArguments[1] == typeof(string))
|
||||
if (dictionaryReference.HasFlag(LintDictionaryReference.Values) && dictionaryInterface.GenericTypeArguments[1] == typeof(string))
|
||||
dictionaryValues.AddRange((IEnumerable<string>)((IDictionary)propertyInfo.GetValue(ruleInfo)).Values);
|
||||
|
||||
if (dictionaryReference.HasFlag(LintDictionaryReference.Values) && type.GenericTypeArguments[1] == typeof(IEnumerable<string>))
|
||||
if (dictionaryReference.HasFlag(LintDictionaryReference.Values) && dictionaryInterface.GenericTypeArguments[1] == typeof(IEnumerable<string>))
|
||||
foreach (var row in (IEnumerable<IEnumerable<string>>)((IDictionary)propertyInfo.GetValue(ruleInfo)).Values)
|
||||
dictionaryValues.AddRange(row);
|
||||
|
||||
@@ -108,9 +121,9 @@ namespace OpenRA.Mods.Common.Lint
|
||||
var supportedTypes = new[]
|
||||
{
|
||||
"string", "IEnumerable<string>",
|
||||
"Dictionary<string, T> (LintDictionaryReference.Keys)",
|
||||
"Dictionary<T, string> (LintDictionaryReference.Values)",
|
||||
"Dictionary<T, IEnumerable<string>> (LintDictionaryReference.Values)",
|
||||
"IReadOnlyDictionary<string, T> (LintDictionaryReference.Keys)",
|
||||
"IReadOnlyDictionary<T, string> (LintDictionaryReference.Values)",
|
||||
"IReadOnlyDictionary<T, IEnumerable<string>> (LintDictionaryReference.Values)",
|
||||
"BooleanExpression", "IntegerExpression"
|
||||
};
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
@@ -270,10 +271,20 @@ namespace OpenRA.Mods.Common
|
||||
if (t.IsEnum)
|
||||
return $"{t.Name} (enum)";
|
||||
|
||||
if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(HashSet<>))
|
||||
if (t.IsGenericType &&
|
||||
(t.GetGenericTypeDefinition() == typeof(HashSet<>) ||
|
||||
t.GetGenericTypeDefinition()
|
||||
.BaseTypes()
|
||||
.Select(bt => bt.IsGenericType ? bt.GetGenericTypeDefinition() : null)
|
||||
.Any(bt => bt == typeof(FrozenSet<>))))
|
||||
return $"Set of {t.GetGenericArguments().Select(FriendlyTypeName).First()}";
|
||||
|
||||
if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Dictionary<,>))
|
||||
if (t.IsGenericType &&
|
||||
(t.GetGenericTypeDefinition() == typeof(Dictionary<,>) ||
|
||||
t.GetGenericTypeDefinition()
|
||||
.BaseTypes()
|
||||
.Select(bt => bt.IsGenericType ? bt.GetGenericTypeDefinition() : null)
|
||||
.Any(bt => bt == typeof(FrozenDictionary<,>))))
|
||||
{
|
||||
var args = t.GetGenericArguments().Select(FriendlyTypeName).ToArray();
|
||||
return $"Dictionary with Key: {args[0]}, Value: {args[1]}";
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
@@ -275,6 +277,14 @@ namespace OpenRA.Test
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetValue_NullString()
|
||||
{
|
||||
var actual = FieldLoader.GetValue<string>("field", null);
|
||||
|
||||
Assert.That(actual, Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetValue_BooleanExpression()
|
||||
{
|
||||
@@ -372,6 +382,38 @@ namespace OpenRA.Test
|
||||
Assert.That(actual, Is.EqualTo(new int2[] { new(1, 2), new(3, 4), new(5, 6) }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetValue_WVecImmutableArray()
|
||||
{
|
||||
var actual = FieldLoader.GetValue<ImmutableArray<WVec>>("field", " 1 , 2 , 3 , 4 , 5 , 6 , , ");
|
||||
|
||||
Assert.That(actual, Is.EqualTo(new WVec[] { new(1, 2, 3), new(4, 5, 6) }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetValue_CPosImmutableArray()
|
||||
{
|
||||
var actual = FieldLoader.GetValue<ImmutableArray<CPos>>("field", " 1 , 2 , 3 , 4 , 5 , 6 , , ");
|
||||
|
||||
Assert.That(actual, Is.EqualTo(new CPos[] { new(1, 2), new(3, 4), new(5, 6) }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetValue_CVecImmutableArray()
|
||||
{
|
||||
var actual = FieldLoader.GetValue<ImmutableArray<CVec>>("field", " 1 , 2 , 3 , 4 , 5 , 6 , , ");
|
||||
|
||||
Assert.That(actual, Is.EqualTo(new CVec[] { new(1, 2), new(3, 4), new(5, 6) }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetValue_int2ImmutableArray()
|
||||
{
|
||||
var actual = FieldLoader.GetValue<ImmutableArray<int2>>("field", " 1 , 2 , 3 , 4 , 5 , 6 , , ");
|
||||
|
||||
Assert.That(actual, Is.EqualTo(new int2[] { new(1, 2), new(3, 4), new(5, 6) }));
|
||||
}
|
||||
|
||||
[TestCase(null, null)]
|
||||
[TestCase("", null)]
|
||||
[TestCase("123", 123)]
|
||||
@@ -459,9 +501,54 @@ namespace OpenRA.Test
|
||||
Assert.That(actual, Is.Empty);
|
||||
}
|
||||
|
||||
[TestCase(null, new int[] { })]
|
||||
[TestCase("", new int[] { })]
|
||||
[TestCase("1", new int[] { 1 })]
|
||||
[TestCase("1,2,3", new int[] { 1, 2, 3 })]
|
||||
[TestCase("1,,3", new int[] { 1, 3 })]
|
||||
[TestCase(" 1 , 2 , 3 ", new int[] { 1, 2, 3 })]
|
||||
[TestCase(" 1 , , 3 ", new int[] { 1, 3 })]
|
||||
[TestCase("1,1,2,2,3", new int[] { 1, 1, 2, 2, 3 })]
|
||||
[TestCase("1,1,1,1", new int[] { 1, 1, 1, 1 })]
|
||||
public void GetValue_ImmutableArray(string input, int[] expected)
|
||||
{
|
||||
var actual = FieldLoader.GetValue<ImmutableArray<int>>("field", input);
|
||||
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[TestCase(null, new int[] { })]
|
||||
[TestCase("", new int[] { })]
|
||||
[TestCase("1", new int[] { 1 })]
|
||||
[TestCase("1,2,3", new int[] { 1, 2, 3 })]
|
||||
[TestCase("1,,3", new int[] { 1, 3 })]
|
||||
[TestCase(" 1 , 2 , 3 ", new int[] { 1, 2, 3 })]
|
||||
[TestCase(" 1 , , 3 ", new int[] { 1, 3 })]
|
||||
[TestCase("1,1,2,2,3", new int[] { 1, 2, 3 })]
|
||||
[TestCase("1,1,1,1", new int[] { 1 })]
|
||||
public void GetValue_FrozenSet(string input, int[] expected)
|
||||
{
|
||||
var actual = FieldLoader.GetValue<FrozenSet<int>>("field", input);
|
||||
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[TestCase(null)]
|
||||
[TestCase("")]
|
||||
[TestCase("1")]
|
||||
[TestCase("1,2")]
|
||||
public void GetValue_FrozenDictionary(string input)
|
||||
{
|
||||
var actual = FieldLoader.GetValue<FrozenDictionary<int, int>>("field", input);
|
||||
|
||||
Assert.That(actual, Is.Empty);
|
||||
}
|
||||
|
||||
[TestCase(TypeArgs = [typeof(int[][])])]
|
||||
[TestCase(TypeArgs = [typeof(List<List<int>>)])]
|
||||
[TestCase(TypeArgs = [typeof(HashSet<HashSet<int>>)])]
|
||||
[TestCase(TypeArgs = [typeof(ImmutableArray<ImmutableArray<int>>)])]
|
||||
[TestCase(TypeArgs = [typeof(FrozenSet<FrozenSet<int>>)])]
|
||||
public void GetValue_NestedCollections<T>()
|
||||
{
|
||||
var actual = FieldLoader.GetValue<T>("field", "1,2,3");
|
||||
@@ -590,6 +677,33 @@ namespace OpenRA.Test
|
||||
Assert.That(target.Dictionary, Is.EquivalentTo(new Dictionary<int, int> { { 12, 34 }, { 56, 78 } }));
|
||||
}
|
||||
|
||||
sealed class LoadFrozenDictionaryTarget
|
||||
{
|
||||
public FrozenDictionary<int, int> Dictionary;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Load_FrozenDictionary()
|
||||
{
|
||||
var target = new LoadFrozenDictionaryTarget();
|
||||
var yaml = new MiniYaml(
|
||||
null,
|
||||
[
|
||||
new MiniYamlNode(
|
||||
nameof(LoadFrozenDictionaryTarget.Dictionary),
|
||||
new MiniYaml(
|
||||
null,
|
||||
[
|
||||
new MiniYamlNode("12", "34"),
|
||||
new MiniYamlNode("56", "78")
|
||||
]))
|
||||
]);
|
||||
|
||||
FieldLoader.Load(target, yaml);
|
||||
|
||||
Assert.That(target.Dictionary, Is.EquivalentTo(new Dictionary<int, int> { { 12, 34 }, { 56, 78 } }));
|
||||
}
|
||||
|
||||
sealed class LoadRequiredTarget
|
||||
{
|
||||
[FieldLoader.Require]
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
@@ -113,6 +115,38 @@ namespace OpenRA.Test
|
||||
Assert.That(actual, Is.EqualTo("1,2, 3,4, 5,6"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FormatValue_WVecImmutableArray()
|
||||
{
|
||||
var actual = FieldSaver.FormatValue(new WVec[] { new(1, 2, 3), new(4, 5, 6) }.ToImmutableArray());
|
||||
|
||||
Assert.That(actual, Is.EqualTo("1,2,3, 4,5,6"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FormatValue_CPosImmutableArray()
|
||||
{
|
||||
var actual = FieldSaver.FormatValue(new CPos[] { new(1, 2), new(3, 4), new(5, 6) }.ToImmutableArray());
|
||||
|
||||
Assert.That(actual, Is.EqualTo("1,2, 3,4, 5,6"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FormatValue_CVecImmutableArray()
|
||||
{
|
||||
var actual = FieldSaver.FormatValue(new CVec[] { new(1, 2), new(3, 4), new(5, 6) }.ToImmutableArray());
|
||||
|
||||
Assert.That(actual, Is.EqualTo("1,2, 3,4, 5,6"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FormatValue_int2ImmutableArray()
|
||||
{
|
||||
var actual = FieldSaver.FormatValue(new int2[] { new(1, 2), new(3, 4), new(5, 6) }.ToImmutableArray());
|
||||
|
||||
Assert.That(actual, Is.EqualTo("1,2, 3,4, 5,6"));
|
||||
}
|
||||
|
||||
[TestCase(null, "")]
|
||||
[TestCase(123, "123")]
|
||||
public void FormatValue_Nullable(int? input, string expected)
|
||||
@@ -183,6 +217,50 @@ namespace OpenRA.Test
|
||||
Assert.That(actual, Is.EqualTo($"12: 34{Environment.NewLine}56: 78{Environment.NewLine}"));
|
||||
}
|
||||
|
||||
[TestCase(null, "")]
|
||||
[TestCase(new int[] { }, "")]
|
||||
[TestCase(new int[] { 1 }, "1")]
|
||||
[TestCase(new int[] { 1, 2, 3 }, "1, 2, 3")]
|
||||
[TestCase(new int[] { 1, 1, 2, 2, 3 }, "1, 1, 2, 2, 3")]
|
||||
[TestCase(new int[] { 1, 1, 1, 1 }, "1, 1, 1, 1")]
|
||||
public void FormatValue_ImmutableArray(int[] input, string expected)
|
||||
{
|
||||
var actual = FieldSaver.FormatValue(input?.ToImmutableArray());
|
||||
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FormatValue_ImmutableArray_Default()
|
||||
{
|
||||
var actual = FieldSaver.FormatValue(default(ImmutableArray<int>));
|
||||
|
||||
Assert.That(actual, Is.Empty);
|
||||
}
|
||||
|
||||
[TestCase(null, "")]
|
||||
[TestCase(new int[] { }, "")]
|
||||
[TestCase(new int[] { 1 }, "1")]
|
||||
[TestCase(new int[] { 1, 2, 3 }, "1, 2, 3")]
|
||||
[TestCase(new int[] { 1, 1, 2, 2, 3 }, "1, 2, 3")]
|
||||
[TestCase(new int[] { 1, 1, 1, 1 }, "1")]
|
||||
public void FormatValue_FrozenSet(int[] input, string expected)
|
||||
{
|
||||
var actual = FieldSaver.FormatValue(input?.ToFrozenSet());
|
||||
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FormatValue_FrozenDictionary()
|
||||
{
|
||||
var input = new Dictionary<int, int> { { 12, 34 }, { 56, 78 } }.ToFrozenDictionary();
|
||||
|
||||
var actual = FieldSaver.FormatValue(input);
|
||||
|
||||
Assert.That(actual, Is.EqualTo($"12: 34{Environment.NewLine}56: 78{Environment.NewLine}"));
|
||||
}
|
||||
|
||||
sealed class FieldTarget
|
||||
{
|
||||
public int Int = 123;
|
||||
|
||||
Reference in New Issue
Block a user