Make MappingDataNode use string keys (#5783)

* string keys

* obsoletions

* Fix ValueTupleSerializer

* Release notes

* fix release note conflict

* cleanup

* Fix yaml validator & tests

* cleanup release notes

* a

* enumerator allocations

* Also sequence enumerator alloc
This commit is contained in:
Leon Friedrich
2025-04-18 19:01:34 +10:00
committed by GitHub
parent 7365a59bd9
commit 2f8f4f2f7a
22 changed files with 362 additions and 268 deletions

View File

@@ -35,7 +35,14 @@ END TEMPLATE-->
### Breaking changes
*None yet*
* Yaml mappings/dictionaries now only support string keys instead of generic nodes
* Several MappingDataNode method arguments or return values now use strings instead of a DataNode object
* The MappingDataNode class has various helper methods that still accept a ValueDataNode, but these methods are marked as obsolete and may be removed in the future.
* yaml validators should use `MappingDataNode.GetKeyNode()` when validating mapping keys, so that errors can print node start & end information
* ValueTuple yaml serialization has changed
* Previously they would get serialized into a single mapping with one entry (i.e., `{foo : bar }`
* Now they serialize into a sequence (i.e., `[foo, bar]`
* The ValueTuple serializer will still try to read mappings, but due to the MappingDataNode this may fail if the previously serialized "key" can't be read as a simple string
### New features

View File

@@ -475,7 +475,7 @@ public sealed class EntityDeserializer :
foreach (var (key, value) in tileMap.Children)
{
var yamlTileId = ((ValueDataNode) key).AsInt();
var yamlTileId = int.Parse(key, CultureInfo.InvariantCulture);
var tileName = ((ValueDataNode) value).Value;
if (migrations.TryGetValue(tileName, out var @new))
tileName = @new;

View File

@@ -69,7 +69,7 @@ internal sealed class MapChunkSerializer : ITypeSerializer<MapChunk, MappingData
var tileDefinitionManager = dependencies.Resolve<ITileDefinitionManager>();
node.TryGetValue(new ValueDataNode("version"), out var versionNode);
node.TryGetValue("version", out var versionNode);
var version = ((ValueDataNode?) versionNode)?.AsInt() ?? 1;
for (ushort y = 0; y < chunk.ChunkSize; y++)

View File

@@ -29,11 +29,9 @@ public sealed class FixtureSerializer : ITypeSerializer<Dictionary<string, Fixtu
foreach (var subNode in node)
{
var key = (ValueDataNode)subNode.Key;
if (!keys.Add(key.Value))
if (!keys.Add(subNode.Key))
{
seq.Add(new ErrorNode(subNode.Key, $"Found duplicate fixture ID {key.Value}"));
seq.Add(new ErrorNode(new ValueDataNode(subNode.Key), $"Found duplicate fixture ID {subNode.Key}"));
continue;
}
@@ -50,10 +48,8 @@ public sealed class FixtureSerializer : ITypeSerializer<Dictionary<string, Fixtu
foreach (var subNode in node)
{
var key = (ValueDataNode)subNode.Key;
var fixture = serializationManager.Read<Fixture>(subNode.Value, hookCtx, context, notNullableOverride: true);
value.Add(key.Value, fixture);
value.Add(subNode.Key, fixture);
}
return value;

View File

@@ -342,8 +342,7 @@ public partial class PrototypeManager
{
if (abstractNode is not ValueDataNode abstractValueNode)
{
mapping.Remove(abstractNode);
mapping.Add("abstract", "true");
mapping["abstract"] = new ValueDataNode("true");
return;
}

View File

@@ -112,7 +112,7 @@ public sealed class ReplayData
ClientSideRecording = clientSideRecording;
YamlData = yamlData;
if (YamlData.TryGet(new ValueDataNode(ReplayConstants.MetaKeyRecordedBy), out ValueDataNode? node)
if (YamlData.TryGet(ReplayConstants.MetaKeyRecordedBy, out ValueDataNode? node)
&& Guid.TryParse(node.Value, out var guid))
{
Recorder = new NetUserId(guid);

View File

@@ -266,27 +266,21 @@ namespace Robust.Shared.Serialization.Manager.Definition
foreach (var (key, val) in mapping.Children)
{
if (key is not ValueDataNode valueDataNode)
if (!TryGetIndex(key, out var idx))
{
validatedMapping.Add(new ErrorNode(key, "Key not ValueDataNode."), new InconclusiveNode(val));
continue;
}
if (!TryGetIndex(valueDataNode.Value, out var idx))
{
if (TryGetIncludeMappingPair(includeValidations, valueDataNode.Value, out var validatedNotFoundPair))
if (TryGetIncludeMappingPair(includeValidations, key, out var validatedNotFoundPair))
{
validatedMapping.Add(validatedNotFoundPair.Key, validatedNotFoundPair.Value);
continue;
}
var error = new FieldNotFoundErrorNode(valueDataNode, typeof(T));
var error = new FieldNotFoundErrorNode(mapping.GetKeyNode(key), typeof(T));
validatedMapping.Add(error, new InconclusiveNode(val));
continue;
}
var keyValidated = serialization.ValidateNode<string>(key, context);
var keyValidated = serialization.ValidateNode<string>(mapping.GetKeyNode(key), context);
ValidationNode valNode;
if (IsNull(val))
@@ -295,7 +289,7 @@ namespace Robust.Shared.Serialization.Manager.Definition
{
var error = new ErrorNode(
val,
$"Field \"{valueDataNode.Value}\" had null value despite not being annotated as nullable.");
$"Field \"{key}\" had null value despite not being annotated as nullable.");
validatedMapping.Add(keyValidated, error);
continue;
@@ -309,7 +303,7 @@ namespace Robust.Shared.Serialization.Manager.Definition
}
//include node errors override successful nodes on the main datadef
if (valNode is not ErrorNode && TryGetIncludeMappingPair(includeValidations, valueDataNode.Value, out var validatedPair))
if (valNode is not ErrorNode && TryGetIncludeMappingPair(includeValidations, key, out var validatedPair))
{
if (validatedPair.Value is ErrorNode)
{

View File

@@ -190,7 +190,8 @@ public partial class SerializationManager
{
// tag is set on data definition creation
if(!processedTags.Add(dfa.Tag!)) continue; //tag was already processed, probably because we are using the same tag in an include
var key = new ValueDataNode(dfa.Tag);
var key = dfa.Tag!;
if (parent.TryGetValue(key, out var parentValue))
{
if (newMapping.TryGetValue(key, out var childValue))

View File

@@ -1,3 +1,4 @@
using System;
using System.IO;
using Robust.Shared.Serialization.Manager;
using YamlDotNet.Core;
@@ -28,6 +29,7 @@ namespace Robust.Shared.Serialization.Markdown
/// </summary>
public abstract DataNode? Except(DataNode node);
[Obsolete("Use SerializationManager.PushComposition()")]
public abstract DataNode PushInheritance(DataNode parent);
public T CopyCast<T>() where T : DataNode
@@ -60,6 +62,7 @@ namespace Robust.Shared.Serialization.Markdown
public abstract T? Except(T node);
[Obsolete("Use SerializationManager.PushComposition()")]
public abstract T PushInheritance(T node);
public override DataNode? Except(DataNode node)
@@ -67,6 +70,7 @@ namespace Robust.Shared.Serialization.Markdown
return node is not T tNode ? throw new InvalidNodeTypeException() : Except(tNode);
}
[Obsolete("Use SerializationManager.PushComposition()")]
public override DataNode PushInheritance(DataNode parent)
{
return parent is not T tNode ? throw new InvalidNodeTypeException() : PushInheritance(tNode);

View File

@@ -25,10 +25,7 @@ public static class DataNodeHelpers
foreach (var (k, v) in node)
{
foreach (var child in GetAllNodes(k))
{
yield return child;
}
yield return node.GetKeyNode(k);
foreach (var child in GetAllNodes(v))
{

View File

@@ -85,6 +85,16 @@ public static class DataNodeParser
return node;
}
private static string ParseKey(Parser parser)
{
var ev = parser.Consume<Scalar>();
if (!ev.Anchor.IsEmpty)
throw new NotSupportedException();
return ev.Value;
}
private static SequenceDataNode ParseSequence(Parser parser, DocumentState state)
{
var ev = parser.Consume<SequenceStart>();
@@ -124,12 +134,11 @@ public static class DataNodeParser
MappingEnd mapEnd;
while (!parser.TryConsume(out mapEnd))
{
var key = Parse(parser, state);
var key = ParseKey(parser);
var value = Parse(parser, state);
node.Add(key, value);
unresolvedAlias |= key is DataNodeAlias;
unresolvedAlias |= value is DataNodeAlias;
}
@@ -173,17 +182,16 @@ public static class DataNodeParser
private static void ResolveMappingAliases(MappingDataNode mapping, DocumentState state)
{
var swaps = new ValueList<(DataNode key, DataNode value)>();
var swaps = new ValueList<(string key, DataNode value)>();
foreach (var (key, value) in mapping)
{
if (key is not DataNodeAlias && value is not DataNodeAlias)
if (value is not DataNodeAlias valueAlias)
return;
var newKey = key is DataNodeAlias keyAlias ? ResolveAlias(keyAlias, state) : key;
var newValue = value is DataNodeAlias valueAlias ? ResolveAlias(valueAlias, state) : value;
var newValue = ResolveAlias(valueAlias, state);
swaps.Add((newKey, newValue));
swaps.Add((key, newValue));
mapping.Remove(key);
}
@@ -242,6 +250,7 @@ public static class DataNodeParser
throw new NotSupportedException();
}
[Obsolete("Use SerializationManager.PushComposition()")]
public override DataNode PushInheritance(DataNode parent)
{
throw new NotSupportedException();

View File

@@ -5,25 +5,30 @@ using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using Robust.Shared.Serialization.Markdown.Value;
using Robust.Shared.Utility;
using YamlDotNet.Core;
using YamlDotNet.RepresentationModel;
namespace Robust.Shared.Serialization.Markdown.Mapping
{
public sealed class MappingDataNode : DataNode<MappingDataNode>, IDictionary<DataNode, DataNode>
public sealed class MappingDataNode : DataNode<MappingDataNode>, IDictionary<string, DataNode>
{
// To fetch nodes by key name with YAML, we NEED a YamlScalarNode.
// We use a thread local one to avoid allocating one every fetch, since we just replace the inner value.
// Obviously thread local to avoid threading issues.
private static readonly ThreadLocal<ValueDataNode> FetchNode =
new(() => new ValueDataNode(""));
private readonly Dictionary<string, DataNode> _children;
private readonly List<KeyValuePair<string,DataNode>> _list;
private readonly Dictionary<DataNode, DataNode> _children;
private readonly List<KeyValuePair<DataNode,DataNode>> _list;
/// <summary>
/// ValueDataNodes associated with each key. This is used for yaml validation / error reporting.
/// I.e., if a key is meant to be an EntityPrototype ID, we want to print an error that points to the
/// corresponding yaml lines.
/// </summary>
private IReadOnlyDictionary<string, ValueDataNode>? _keyNodes;
// TODO avoid populating this unless we are running the yaml linter?
public IReadOnlyDictionary<DataNode, DataNode> Children => _children;
public override bool IsEmpty => _children.Count == 0;
public int Count => _children.Count;
public bool IsReadOnly => false;
public IReadOnlyDictionary<string, DataNode> Children => _children;
public MappingDataNode() : base(NodeMark.Invalid, NodeMark.Invalid)
{
@@ -41,113 +46,106 @@ namespace Robust.Shared.Serialization.Markdown.Mapping
{
_children = new(mapping.Children.Count);
_list = new(mapping.Children.Count);
foreach (var (key, val) in mapping.Children)
var keyNodes = new Dictionary<string, ValueDataNode>(mapping.Children.Count);
foreach (var (keyNode, val) in mapping.Children)
{
Add(key.ToDataNode(), val.ToDataNode());
if (keyNode is not YamlScalarNode scalarNode)
throw new NotSupportedException("Mapping data nodes must have a scalar keys");
var valueNode = new ValueDataNode(scalarNode);
Add(valueNode.Value, val.ToDataNode());
keyNodes.Add(valueNode.Value, valueNode);
}
_keyNodes = keyNodes;
Tag = mapping.Tag.IsEmpty ? null : mapping.Tag.Value;
}
public MappingDataNode(Dictionary<DataNode, DataNode> nodes) : base(NodeMark.Invalid, NodeMark.Invalid)
public MappingDataNode(Dictionary<string, DataNode> nodes) : base(NodeMark.Invalid, NodeMark.Invalid)
{
_children = new(nodes.Count);
_list = new(nodes.Count);
foreach (var (key, val) in nodes)
{
Add(key, val);
}
_children = new(nodes);
_list = new(_children);
}
public KeyValuePair<DataNode, DataNode> this[int key] => _list[key];
public DataNode this[string index]
{
get => Get(index);
set => Add(index, value);
}
public KeyValuePair<string, DataNode> this[int key] => _list[key];
public MappingDataNode Add(string key, DataNode node)
{
Add(new ValueDataNode(key), node);
return this;
}
private static ValueDataNode GetFetchNode(string key)
{
var node = FetchNode.Value!;
node.Value = key;
return node;
}
public MappingDataNode Add(DataNode key, DataNode node)
{
_children.Add(key, node);
_list.Add(new(key, node));
return this;
}
public DataNode this[DataNode key]
public DataNode this[string key]
{
get => _children[key];
set
{
if (_children.TryAdd(key, value))
{
_list.Add(new( key, value));
_list.Add(new(key, value));
return;
}
var i = _list.IndexOf(new(key, _children[key]));
_list[i] = new(key, value);
var index = IndexOf(key);
if (index == -1)
throw new Exception("Key exists in Children, but not list?");
_list[index] = new(key, value);
_children[key] = value;
}
}
void IDictionary<DataNode, DataNode>.Add(DataNode key, DataNode value) => Add(key, value);
public int IndexOf(string key)
{
// TODO MappingDataNode
// Consider having a Dictionary<string,int> for faster lookups?
// IndexOf() gets called in Remove(), which itself gets called frequently (e.g., per serialized component,
// per entity, when loading a map.
//
// Then again, if most mappings only contain 1-4 entries, this list search is comparable in speed, reduces
// allocations, and makes adding/inserting entries faster.
public bool ContainsKey(DataNode key) => _children.ContainsKey(key);
for (var index = 0; index < _list.Count; index++)
{
if (_list[index].Key == key)
return index;
}
bool IDictionary<DataNode, DataNode>.Remove(DataNode key)
=> ((IDictionary<DataNode, DataNode>)this).Remove(key);
return -1;
}
public bool TryGetValue(DataNode key, [NotNullWhen(true)] out DataNode? value) => TryGet(key, out value);
void IDictionary<string, DataNode>.Add(string key, DataNode value) => Add(key, value);
public ICollection<DataNode> Keys => _list.Select(x => x.Key).ToArray();
public bool ContainsKey(string key) => _children.ContainsKey(key);
bool IDictionary<string, DataNode>.Remove(string key)
=> ((IDictionary<string, DataNode>)this).Remove(key);
public bool TryGetValue(string key, [NotNullWhen(true)] out DataNode? value)
=> TryGet(key, out value);
// TODO consider changing these to unsorted collections
// I.e., just redirect to _children.Keys to avoid hidden linq & allocations.
public ICollection<string> Keys => _list.Select(x => x.Key).ToArray();
public ICollection<DataNode> Values => _list.Select(x => x.Value).ToArray();
public DataNode Get(DataNode key)
public DataNode Get(string key)
{
return _children[key];
}
public T Get<T>(DataNode key) where T : DataNode
public T Get<T>(string key) where T : DataNode
{
return (T) Get(key);
}
public DataNode Get(string key)
public bool TryGet(string key, [NotNullWhen(true)] out DataNode? node)
{
return Get(GetFetchNode(key));
return _children.TryGetValue(key, out node);
}
public T Get<T>(string key) where T : DataNode
{
return Get<T>(GetFetchNode(key));
}
public bool TryGet(DataNode key, [NotNullWhen(true)] out DataNode? node)
{
if (_children.TryGetValue(key, out node))
{
return true;
}
node = null;
return false;
}
public bool TryGet<T>(DataNode key, [NotNullWhen(true)] out T? node) where T : DataNode
public bool TryGet<T>(string key, [NotNullWhen(true)] out T? node) where T : DataNode
{
node = null;
if (!TryGet(key, out var rawNode) || rawNode is not T castNode)
@@ -156,41 +154,27 @@ namespace Robust.Shared.Serialization.Markdown.Mapping
return true;
}
public bool TryGet(string key, [NotNullWhen(true)] out DataNode? node)
{
return TryGet(GetFetchNode(key), out node);
}
public bool TryGet<T>(string key, [NotNullWhen(true)] out T? node) where T : DataNode
{
return TryGet(GetFetchNode(key), out node);
}
public bool Has(DataNode key)
public bool Has(string key)
{
return _children.ContainsKey(key);
}
public bool Has(string key)
public bool Remove(string key)
{
return Has(GetFetchNode(key));
if (!_children.Remove(key))
return false;
var index = IndexOf(key);
if (index == -1)
throw new Exception("Key exists in Children, but not list?");
_list.RemoveAt(index);
return true;
}
public MappingDataNode Remove(DataNode key)
public T Cast<T>(string key) where T : DataNode
{
if (_children.Remove(key, out var val))
_list.Remove(new(key, val));
return this;
}
public MappingDataNode Remove(string key)
{
return Remove(GetFetchNode(key));
}
public T Cast<T>(string index) where T : DataNode
{
return (T) this[index];
return (T) this[key];
}
public YamlMappingNode ToYaml()
@@ -199,7 +183,23 @@ namespace Robust.Shared.Serialization.Markdown.Mapping
foreach (var (key, val) in _list)
{
mapping.Add(key.ToYamlNode(), val.ToYamlNode());
YamlScalarNode yamlKeyNode;
if (_keyNodes != null && _keyNodes.TryGetValue(key, out var keyNode))
{
yamlKeyNode = (YamlScalarNode)keyNode;
}
else
{
// This is matches the ValueDataNode -> YamlScalarNode cast operator
yamlKeyNode = new(key)
{
Style = ValueDataNode.IsNullLiteral(key) || string.IsNullOrWhiteSpace(key)
? ScalarStyle.DoubleQuoted
: ScalarStyle.Any
};
}
mapping.Add(yamlKeyNode, val.ToYamlNode());
}
mapping.Tag = Tag;
@@ -207,6 +207,11 @@ namespace Robust.Shared.Serialization.Markdown.Mapping
return mapping;
}
public ValueDataNode GetKeyNode(string key)
{
return _keyNodes?.GetValueOrDefault(key) ?? new ValueDataNode(key);
}
public MappingDataNode Merge(MappingDataNode otherMapping)
{
var newMapping = Copy();
@@ -227,12 +232,12 @@ namespace Robust.Shared.Serialization.Markdown.Mapping
if (!skipDuplicates || !Has(key))
{
// Intentionally raises an ArgumentException
Add(key.Copy(), val.Copy());
Add(key, val.Copy());
}
}
}
public void InsertAt(int index, DataNode key, DataNode value)
public void InsertAt(int index, string key, DataNode value)
{
if (index > _list.Count || index < 0)
throw new ArgumentOutOfRangeException();
@@ -243,20 +248,6 @@ namespace Robust.Shared.Serialization.Markdown.Mapping
_list.Insert(index, new(key, value));
}
public void InsertAt(int index, string key, DataNode value)
{
if (index > _list.Count || index < 0)
throw new ArgumentOutOfRangeException();
var k = new ValueDataNode(key);
if (!_children.TryAdd(k, value))
throw new InvalidOperationException($"Already contains key {key}");
_list.Insert(index, new(k, value));
}
public override bool IsEmpty => _children.Count == 0;
public override MappingDataNode Copy()
{
var newMapping = new MappingDataNode(_children.Count)
@@ -268,9 +259,10 @@ namespace Robust.Shared.Serialization.Markdown.Mapping
foreach (var (key, val) in _list)
{
newMapping.Add(key.Copy(), val.Copy());
newMapping.Add(key, val.Copy());
}
newMapping._keyNodes = _keyNodes;
return newMapping;
}
@@ -308,17 +300,13 @@ namespace Robust.Shared.Serialization.Markdown.Mapping
foreach (var (key, val) in _list)
{
var other = node._list.FirstOrNull(p => p.Key.Equals(key));
if (other == null)
if (!node._children.TryGetValue(key, out var otherVal))
{
mappingNode.Add(key.Copy(), val.Copy());
mappingNode.Add(key, val.Copy());
}
else
else if (val.Except(otherVal) is { } newValue)
{
// We recursively call except on the values and keep only the differences.
var newValue = val.Except(other.Value.Value);
if (newValue == null) continue;
mappingNode.Add(key.Copy(), newValue);
mappingNode.Add(key, newValue);
}
}
@@ -336,18 +324,8 @@ namespace Robust.Shared.Serialization.Markdown.Mapping
foreach (var (key, val) in _list)
{
var other = node._list.FirstOrNull(p => p.Key.Equals(key));
if (other == null)
{
mappingNode.Add(key.Copy(), val.Copy());
}
else
{
// We only keep the entry if the values are not equal
if (!val.Equals(other.Value.Value))
mappingNode.Add(key.Copy(), val.Copy());
}
if (!node._children.TryGetValue(key, out var otherVal) || !val.Equals(otherVal))
mappingNode.Add(key, val.Copy());
}
return mappingNode._children.Count == 0 ? null : mappingNode;
@@ -384,34 +362,24 @@ namespace Robust.Shared.Serialization.Markdown.Mapping
if (_children.Count != other._children.Count)
return false;
if (Tag != other.Tag)
return false;
foreach (var (key, otherValue) in other)
{
if (!_children.TryGetValue(key, out var ownValue) ||
!otherValue.Equals(ownValue))
if (!_children.TryGetValue(key, out var ownValue)
|| !otherValue.Equals(ownValue))
{
return false;
}
}
return Tag == other.Tag;
return true;
}
public override MappingDataNode PushInheritance(MappingDataNode node)
{
var newNode = Copy();
foreach (var (key, val) in node)
{
if(_children.ContainsKey(key))
continue;
newNode.Remove(key);
newNode.Add(key.Copy(), val.Copy());
}
return newNode;
}
public IEnumerator<KeyValuePair<DataNode, DataNode>> GetEnumerator() => _list.GetEnumerator();
public List<KeyValuePair<string, DataNode>>.Enumerator GetEnumerator() => _list.GetEnumerator();
IEnumerator<KeyValuePair<string, DataNode>> IEnumerable<KeyValuePair<string, DataNode>>.GetEnumerator() =>
_list.GetEnumerator();
public override int GetHashCode()
{
@@ -430,7 +398,7 @@ namespace Robust.Shared.Serialization.Markdown.Mapping
return GetEnumerator();
}
public void Add(KeyValuePair<DataNode, DataNode> item) => Add(item.Key, item.Value);
public void Add(KeyValuePair<string, DataNode> item) => Add(item.Key, item.Value);
public void Clear()
{
@@ -438,18 +406,31 @@ namespace Robust.Shared.Serialization.Markdown.Mapping
_list.Clear();
}
public bool Contains(KeyValuePair<DataNode, DataNode> item) => _children.ContainsKey(item.Key);
public bool Contains(KeyValuePair<string, DataNode> item) => _children.ContainsKey(item.Key);
public void CopyTo(KeyValuePair<DataNode, DataNode>[] array, int arrayIndex)
[Obsolete("Use SerializationManager.PushComposition()")]
public override MappingDataNode PushInheritance(MappingDataNode node)
{
var newNode = Copy();
foreach (var (key, val) in node)
{
if (_children.ContainsKey(key))
continue;
newNode.Remove(key);
newNode.Add(key, val.Copy());
}
return newNode;
}
public void CopyTo(KeyValuePair<string, DataNode>[] array, int arrayIndex)
=> _list.CopyTo(array, arrayIndex);
public bool Remove(KeyValuePair<DataNode, DataNode> item)
=> ((IDictionary<DataNode, DataNode>)this).Remove(item.Key);
public bool Remove(KeyValuePair<string, DataNode> item)
=> ((IDictionary<string, DataNode>) this).Remove(item.Key);
public int Count => _children.Count;
public bool IsReadOnly => false;
public bool TryAdd(DataNode key, DataNode value)
public bool TryAdd(string key, DataNode value)
{
if (!_children.TryAdd(key, value))
return false;
@@ -458,7 +439,7 @@ namespace Robust.Shared.Serialization.Markdown.Mapping
return true;
}
public bool TryAddCopy(DataNode key, DataNode value)
public bool TryAddCopy(string key, DataNode value)
{
ref var entry = ref CollectionsMarshal.GetValueRefOrAddDefault(_children, key, out var exists);
if (exists)
@@ -468,5 +449,52 @@ namespace Robust.Shared.Serialization.Markdown.Mapping
_list.Add(new(key, entry));
return true;
}
// These methods are probably fine to keep around as helper methods, but are currently marked as obsolete
// so that people don't uneccesarily allocate a ValueDataNode. I.e., to prevent people from using code like
// mapping.TryGet(new ValueDataNode("key"), ...)
#region ValueDataNode Helpers
[Obsolete("Use string keys instead of ValueDataNode")]
public bool TryGet(ValueDataNode key, [NotNullWhen(true)] out DataNode? value)
=> TryGet(key.Value, out value);
[Obsolete("Use string keys instead of ValueDataNode")]
public DataNode this[ValueDataNode key]
{
get => this[key.Value];
set => this[key.Value] = value;
}
[Obsolete("Use string keys instead of ValueDataNode")]
public bool TryGetValue(ValueDataNode key, [NotNullWhen(true)] out DataNode? value)
=> TryGet(key.Value, out value);
[Obsolete("Use string keys instead of ValueDataNode")]
public bool TryGet<T>(ValueDataNode key, [NotNullWhen(true)] out T? node) where T : DataNode
=> TryGet(key.Value, out node);
[Obsolete("Use string keys instead of ValueDataNode")]
public bool Has(ValueDataNode key) => Has(key.Value);
[Obsolete("Use string keys instead of ValueDataNode")]
public T Cast<T>(ValueDataNode key) where T : DataNode => Cast<T>(key.Value);
[Obsolete("Use string keys instead of ValueDataNode")]
public void Add(KeyValuePair<ValueDataNode, DataNode> item) => Add(item.Key, item.Value);
[Obsolete("Use string keys instead of ValueDataNode")]
public MappingDataNode Add(ValueDataNode key, DataNode node) => Add(key.Value, node);
[Obsolete("Use string keys instead of ValueDataNode")]
public void InsertAt(int index, ValueDataNode key, DataNode value) => InsertAt(index, key.Value, value);
[Obsolete("Use string keys instead of ValueDataNode")]
public bool Contains(KeyValuePair<ValueDataNode, DataNode> item) => _children.ContainsKey(item.Key.Value);
[Obsolete("Use string keys instead of ValueDataNode")]
public bool Remove(ValueDataNode key) => Remove(key.Value);
#endregion
}
}

View File

@@ -6,21 +6,15 @@ namespace Robust.Shared.Serialization.Markdown.Mapping
{
public static class MappingDataNodeExtensions
{
public static MappingDataNode Add(this MappingDataNode mapping, string key, DataNode node)
{
mapping.Add(new ValueDataNode(key), node);
return mapping;
}
public static MappingDataNode Add(this MappingDataNode mapping, string key, string value)
{
mapping.Add(new ValueDataNode(key), new ValueDataNode(value));
mapping.Add(key, new ValueDataNode(value));
return mapping;
}
public static MappingDataNode Add(this MappingDataNode mapping, string key, List<string> sequence)
{
mapping.Add(new ValueDataNode(key), new SequenceDataNode(sequence));
mapping.Add(key, new SequenceDataNode(sequence));
return mapping;
}
}

View File

@@ -133,7 +133,8 @@ namespace Robust.Shared.Serialization.Markdown.Sequence
return newSequence;
}
public IEnumerator<DataNode> GetEnumerator() => _nodes.GetEnumerator();
public List<DataNode>.Enumerator GetEnumerator() => _nodes.GetEnumerator();
IEnumerator<DataNode> IEnumerable<DataNode>.GetEnumerator() => _nodes.GetEnumerator();
public override int GetHashCode()
{
@@ -192,6 +193,7 @@ namespace Robust.Shared.Serialization.Markdown.Sequence
return true;
}
[Obsolete("Use SerializationManager.PushComposition()")]
public override SequenceDataNode PushInheritance(SequenceDataNode node)
{
var newNode = Copy();

View File

@@ -58,7 +58,7 @@ namespace Robust.Shared.Serialization.Markdown.Value
public override bool IsEmpty => string.IsNullOrWhiteSpace(Value);
private static bool IsNullLiteral(string? value) => value != null && value.Trim().ToLower() is "null" ;
public static bool IsNullLiteral(string? value) => value != null && value.Trim().ToLower() is "null" ;
public override ValueDataNode Copy()
{
@@ -76,6 +76,7 @@ namespace Robust.Shared.Serialization.Markdown.Value
return node.Value == Value ? null : Copy();
}
[Obsolete("Use SerializationManager.PushComposition()")]
public override ValueDataNode PushInheritance(ValueDataNode node)
{
return Copy();

View File

@@ -11,30 +11,25 @@ using Robust.Shared.Serialization.TypeSerializers.Interfaces;
namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
/// <summary>
/// A custom type serializer for reading a set of types that inherit from some base type.
/// A custom type serializer for reading a set of types that inherit from some base type.
/// </summary>
public sealed class AbstractDictionarySerializer<TValue> : ITypeSerializer<Dictionary<Type, TValue>, MappingDataNode>
public sealed class AbstractDictionarySerializer<TValue> : ITypeSerializer<Dictionary<Type, TValue>, MappingDataNode>
where TValue : notnull
{
public ValidationNode Validate(ISerializationManager serializationManager, MappingDataNode node,
IDependencyCollection dependencies, ISerializationContext? context = null)
{
{
var mapping = new Dictionary<ValidationNode, ValidationNode>();
foreach (var (keyNode, valueNode) in node.Children)
foreach (var (key, valueNode) in node.Children)
{
if (keyNode is not ValueDataNode key)
{
mapping.Add(new ErrorNode(keyNode, $"Expected {nameof(ValueDataNode)} but was {keyNode.GetType()}"), new ValidatedValueNode(valueNode));
continue;
}
var type = serializationManager.ReflectionManager.YamlTypeTagLookup(typeof(TValue), key.Value);
var type = serializationManager.ReflectionManager.YamlTypeTagLookup(typeof(TValue), key);
if (type == null)
{
mapping.Add(new ErrorNode(keyNode, $"Could not resolve type: {key.Value}"), new ValidatedValueNode(valueNode));
mapping.Add(new ErrorNode(node.GetKeyNode(key), $"Could not resolve type: {key}"), new ValidatedValueNode(valueNode));
continue;
}
mapping.Add(new ValidatedValueNode(key), serializationManager.ValidateNode(type, valueNode, context));
mapping.Add(new ValidatedValueNode(node.GetKeyNode(key)), serializationManager.ValidateNode(type, valueNode, context));
}
return new ValidatedMappingNode(mapping);
@@ -44,10 +39,9 @@ public sealed class AbstractDictionarySerializer<TValue> : ITypeSerializer<Dicti
SerializationHookContext hookCtx, ISerializationContext? context = null, ISerializationManager.InstantiationDelegate<Dictionary<Type, TValue>>? instanceProvider = null)
{
var dict = instanceProvider != null ? instanceProvider() : new Dictionary<Type, TValue>();
foreach (var (keyNode, valueNode) in node.Children)
foreach (var (key, valueNode) in node.Children)
{
var key = (ValueDataNode) keyNode;
var type = serializationManager.ReflectionManager.YamlTypeTagLookup(typeof(TValue), key.Value)!;
var type = serializationManager.ReflectionManager.YamlTypeTagLookup(typeof(TValue), key)!;
var value = (TValue) serializationManager.Read(type, valueNode, hookCtx, context, notNullableOverride:true)!;
dict.Add(type, value);
}
@@ -62,8 +56,14 @@ public sealed class AbstractDictionarySerializer<TValue> : ITypeSerializer<Dicti
foreach (var (key, val) in value)
{
// TODO SERIALIZATION
// Add some way to directly return a string w/o allocating a ValueDataNode
var keyNode = serializationManager.WriteValue(key.Name, alwaysWrite, context, notNullableOverride: true);
if (keyNode is not ValueDataNode valueNode)
throw new NotSupportedException();
mappingNode.Add(
serializationManager.WriteValue(key.Name, alwaysWrite, context, notNullableOverride:true),
valueNode.Value,
serializationManager.WriteValue(key, val, alwaysWrite, context));
}

View File

@@ -32,13 +32,8 @@ namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Pro
foreach (var (key, val) in node.Children)
{
if (key is not ValueDataNode value)
{
mapping.Add(new ErrorNode(key, $"Cannot cast node {key} to ValueDataNode."), serializationManager.ValidateNode<TValue>(val, context));
continue;
}
mapping.Add(PrototypeSerializer.Validate(serializationManager, value, dependencies, context), serializationManager.ValidateNode<TValue>(val, context));
var keyNode = new ValueDataNode(key);
mapping.Add(PrototypeSerializer.Validate(serializationManager, keyNode, dependencies, context), serializationManager.ValidateNode<TValue>(val, context));
}
return new ValidatedMappingNode(mapping);

View File

@@ -30,8 +30,9 @@ namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Pro
{
var mapping = new Dictionary<ValidationNode, ValidationNode>();
foreach (var (key, val) in node.Children)
foreach (var (k, val) in node.Children)
{
var key = node.GetKeyNode(k);
if (val is not ValueDataNode value)
{
mapping.Add(new ErrorNode(val, $"Cannot cast node {val} to ValueDataNode."), serializationManager.ValidateNode<TValue>(key, context));

View File

@@ -1,3 +1,4 @@
using System;
using System.Collections.Frozen;
using System.Collections.Generic;
using System.Linq;
@@ -8,6 +9,7 @@ using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.Markdown.Mapping;
using Robust.Shared.Serialization.Markdown.Validation;
using Robust.Shared.Serialization.Markdown.Value;
using Robust.Shared.Serialization.TypeSerializers.Interfaces;
namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Generic;
@@ -59,7 +61,7 @@ public sealed class DictionarySerializer<TKey, TValue> :
var mapping = new Dictionary<ValidationNode, ValidationNode>();
foreach (var (key, val) in node.Children)
{
mapping.Add(serializationManager.ValidateNode<TKey>(key, context),
mapping.Add(serializationManager.ValidateNode<TKey>(node.GetKeyNode(key), context),
serializationManager.ValidateNode<TValue>(val, context));
}
@@ -79,8 +81,14 @@ public sealed class DictionarySerializer<TKey, TValue> :
var mappingNode = new MappingDataNode();
foreach (var (key, val) in value)
{
// TODO SERIALIZATION
// Add some way to directly return a string w/o allocating a ValueDataNode
var keyNode = serializationManager.WriteValue(key, alwaysWrite, context);
if (keyNode is not ValueDataNode valueNode)
throw new NotSupportedException("Yaml mapping keys must serialize to a ValueDataNode (i.e. a string)");
mappingNode.Add(
serializationManager.WriteValue(key, alwaysWrite, context),
valueNode.Value,
serializationManager.WriteValue(val, alwaysWrite, context));
}
@@ -128,9 +136,11 @@ public sealed class DictionarySerializer<TKey, TValue> :
{
var dict = instanceProvider != null ? instanceProvider() : new Dictionary<TKey, TValue>();
var keyNode = new ValueDataNode();
foreach (var (key, value) in node.Children)
{
dict.Add(serializationManager.Read<TKey>(key, hookCtx, context),
keyNode.Value = key;
dict.Add(serializationManager.Read<TKey>(keyNode, hookCtx, context),
serializationManager.Read<TValue>(value, hookCtx, context));
}
@@ -149,9 +159,11 @@ public sealed class DictionarySerializer<TKey, TValue> :
var array = new KeyValuePair<TKey, TValue>[node.Children.Count];
int i = 0;
var keyNode = new ValueDataNode();
foreach (var (key, value) in node.Children)
{
var k = serializationManager.Read<TKey>(key, hookCtx, context);
keyNode.Value = key;
var k = serializationManager.Read<TKey>(keyNode, hookCtx, context);
var v = serializationManager.Read<TValue>(value, hookCtx, context);
array[i++] = new(k,v);
}
@@ -174,9 +186,11 @@ public sealed class DictionarySerializer<TKey, TValue> :
var dict = new Dictionary<TKey, TValue>();
var keyNode = new ValueDataNode();
foreach (var (key, value) in node.Children)
{
dict.Add(serializationManager.Read<TKey>(key, hookCtx, context),
keyNode.Value = key;
dict.Add(serializationManager.Read<TKey>(keyNode, hookCtx, context),
serializationManager.Read<TValue>(value, hookCtx, context));
}
@@ -190,10 +204,12 @@ public sealed class DictionarySerializer<TKey, TValue> :
ISerializationManager.InstantiationDelegate<SortedDictionary<TKey, TValue>>? instanceProvider)
{
var dict = instanceProvider != null ? instanceProvider() : new SortedDictionary<TKey, TValue>();
var keyNode = new ValueDataNode();
foreach (var (key, value) in node.Children)
{
dict.Add(serializationManager.Read<TKey>(key, hookCtx, context),
keyNode.Value = key;
dict.Add(serializationManager.Read<TKey>(keyNode, hookCtx, context),
serializationManager.Read<TValue>(value, hookCtx, context));
}

View File

@@ -6,40 +6,67 @@ using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Manager.Attributes;
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.TypeSerializers.Interfaces;
namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Generic
{
[TypeSerializer]
public sealed class ValueTupleSerializer<T1, T2> : ITypeSerializer<ValueTuple<T1, T2>, MappingDataNode>, ITypeCopyCreator<ValueTuple<T1, T2>>
public sealed class ValueTupleSerializer<T1, T2> :
ITypeReader<ValueTuple<T1, T2>, MappingDataNode>,
ITypeSerializer<ValueTuple<T1, T2>, SequenceDataNode>,
ITypeCopyCreator<ValueTuple<T1, T2>>
{
public (T1, T2) Read(ISerializationManager serializationManager, MappingDataNode node,
public (T1, T2) Read(
ISerializationManager serializationManager,
MappingDataNode node,
IDependencyCollection dependencies,
SerializationHookContext hookCtx,
ISerializationContext? context = null, ISerializationManager.InstantiationDelegate<(T1, T2)>? val = null)
ISerializationContext? context = null,
ISerializationManager.InstantiationDelegate<(T1, T2)>? instanceProvider = null)
{
if (node.Children.Count != 1)
throw new InvalidMappingException("Less than or more than 1 mappings provided to ValueTupleSerializer");
var entry = node.Children.First();
var v1 = serializationManager.Read<T1>(entry.Key, hookCtx, context);
var v1 = serializationManager.Read<T1>(node.GetKeyNode(entry.Key), hookCtx, context);
var v2 = serializationManager.Read<T2>(entry.Value, hookCtx, context);
return (v1, v2);
}
public ValidationNode Validate(ISerializationManager serializationManager, MappingDataNode node,
public (T1, T2) Read(
ISerializationManager serializationManager,
SequenceDataNode node,
IDependencyCollection dependencies,
SerializationHookContext hookCtx,
ISerializationContext? context = null,
ISerializationManager.InstantiationDelegate<(T1, T2)>? val = null)
{
if (node.Count != 2)
throw new InvalidMappingException("Sequence must contain exactly 2 elements.");
var v1 = serializationManager.Read<T1>(node[0], hookCtx, context);
var v2 = serializationManager.Read<T2>(node[1], hookCtx, context);
return (v1, v2);
}
public ValidationNode Validate(
ISerializationManager serializationManager,
MappingDataNode node,
IDependencyCollection dependencies,
ISerializationContext? context = null)
{
if (node.Children.Count != 1) return new ErrorNode(node, "More or less than 1 Mapping for ValueTuple found.");
if (node.Children.Count != 1)
return new ErrorNode(node, "More or less than 1 Mapping for ValueTuple found.");
var entry = node.Children.First();
var dict = new Dictionary<ValidationNode, ValidationNode>
{
{
serializationManager.ValidateNode<T1>(entry.Key, context),
serializationManager.ValidateNode<T1>(node.GetKeyNode(entry.Key), context),
serializationManager.ValidateNode<T2>(entry.Value, context)
}
};
@@ -47,21 +74,44 @@ namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Generic
return new ValidatedMappingNode(dict);
}
public DataNode Write(ISerializationManager serializationManager, (T1, T2) value,
IDependencyCollection dependencies, bool alwaysWrite = false,
public ValidationNode Validate(
ISerializationManager serializationManager,
SequenceDataNode node,
IDependencyCollection dependencies,
ISerializationContext? context = null)
{
var mapping = new MappingDataNode();
if (node.Count != 2)
throw new InvalidMappingException("Sequence must contain exactly 2 elements.");
mapping.Add(
serializationManager.WriteValue<T1>(value.Item1, alwaysWrite, context),
serializationManager.WriteValue<T2>(value.Item2, alwaysWrite, context));
var seq = new List<ValidationNode>
{
serializationManager.ValidateNode<T1>(node[0], context),
serializationManager.ValidateNode<T2>(node[1], context)
};
return mapping;
return new ValidatedSequenceNode(seq);
}
public (T1, T2) CreateCopy(ISerializationManager serializationManager, (T1, T2) source,
IDependencyCollection dependencies, SerializationHookContext hookCtx, ISerializationContext? context = null)
public DataNode Write(
ISerializationManager serializationManager,
(T1, T2) value,
IDependencyCollection dependencies,
bool alwaysWrite = false,
ISerializationContext? context = null)
{
return new SequenceDataNode(new List<DataNode>
{
serializationManager.WriteValue(value.Item1, alwaysWrite, context),
serializationManager.WriteValue(value.Item2, alwaysWrite, context)
});
}
public (T1, T2) CreateCopy(
ISerializationManager serializationManager,
(T1, T2) source,
IDependencyCollection dependencies,
SerializationHookContext hookCtx,
ISerializationContext? context = null)
{
return (serializationManager.CreateCopy(source.Item1, hookCtx, context),
serializationManager.CreateCopy(source.Item2, hookCtx, context));

View File

@@ -69,10 +69,10 @@ public sealed partial class ManagerTests : SerializationTest
}, //ISelfSerialize
new object[]
{
new MappingDataNode(new Dictionary<DataNode, DataNode>
new MappingDataNode(new Dictionary<string, DataNode>
{
{ new ValueDataNode("one"), new ValueDataNode("valueOne") },
{ new ValueDataNode("two"), new SequenceDataNode("2", "3") },
{ "one", new ValueDataNode("valueOne") },
{ "two", new SequenceDataNode("2", "3") },
}){Tag = $"!type:{nameof(DataDefClass)}"},
() => (IDataDefBaseInterface)new DataDefClass
{
@@ -112,10 +112,10 @@ public sealed partial class ManagerTests : SerializationTest
}, //array
new object[]
{
new MappingDataNode(new Dictionary<DataNode, DataNode>
new MappingDataNode(new Dictionary<string, DataNode>
{
{ new ValueDataNode("one"), new ValueDataNode("valueOne") },
{ new ValueDataNode("two"), new SequenceDataNode("2", "3") },
{ "one", new ValueDataNode("valueOne") },
{ "two", new SequenceDataNode("2", "3") },
}),
() => new DataDefClass
{
@@ -204,10 +204,10 @@ public sealed partial class ManagerTests : SerializationTest
{
new object[]
{
new MappingDataNode(new Dictionary<DataNode, DataNode>()
new MappingDataNode(new Dictionary<string, DataNode>()
{
{ new ValueDataNode("one"), new ValueDataNode("valueOne") },
{ new ValueDataNode("two"), new SequenceDataNode("2", "3") },
{ "one", new ValueDataNode("valueOne") },
{ "two", new SequenceDataNode("2", "3") },
}),
() => new DataDefStruct
{

View File

@@ -77,7 +77,7 @@ public sealed partial class ReadValueProviderTests : SerializationTest
public void DataDefinitionMappingBaseTest()
{
var data = "someData";
var mapping = new MappingDataNode(new Dictionary<DataNode, DataNode>{{ new ValueDataNode("data"), new ValueDataNode(data) }})
var mapping = new MappingDataNode(new Dictionary<string, DataNode>{{ "data", new ValueDataNode(data) }})
{
Tag = $"!type:{nameof(DataDefinitionValueProviderTestDummy)}"
};