diff --git a/YamlDotNet.Test/Serialization/BufferedDeserialization/AnchorAliasNestedSequenceTest.cs b/YamlDotNet.Test/Serialization/BufferedDeserialization/AnchorAliasNestedSequenceTest.cs new file mode 100644 index 000000000..0716f984f --- /dev/null +++ b/YamlDotNet.Test/Serialization/BufferedDeserialization/AnchorAliasNestedSequenceTest.cs @@ -0,0 +1,98 @@ +// This file is part of YamlDotNet - A .NET library for YAML. +// Copyright (c) Antoine Aubry and contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Xunit; +using YamlDotNet.Core; +using YamlDotNet.Core.Events; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +namespace YamlDotNet.Test.Serialization.BufferedDeserialization; + +public class AnchorAliasNestedSequenceTest +{ + [Fact] + public void AnchorAliasNestedSequence() + { + var deserializer = new DeserializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .Build(); + var executors = deserializer.Deserialize>(Document); + executors[0].Filters.Count.Should().Be(2); + executors[1].Filters.Count.Should().Be(3); + foreach (var (x, y) in executors[0].Filters.Zip(executors[1].Filters, (x, y) => (x, y))) + { + ReferenceEquals(x, y).Should().BeFalse(); + } + } + + public class FilterCollection : List, IYamlConvertible + { + public void Read(IParser parser, Type expectedType, ObjectDeserializer nestedObjectDeserializer) + { + parser.Consume(); + while (!parser.TryConsume(out _)) + { + if (parser.Accept(out _)) + { + AddRange((List)nestedObjectDeserializer.Invoke(typeof(List))!); + } + + Add((Filter)nestedObjectDeserializer.Invoke(typeof(Filter))!); + } + } + + public void Write(IEmitter emitter, ObjectSerializer nestedObjectSerializer) + { + throw new NotImplementedException(); + } + } + + public const string Document = @" +- name: a + filters: &shared + - type: foo + value: 1 + - type: bar + value: 2 +- name: b + filters: + - *shared + - type: extra + value: 3 +"; + + public class Executor + { + public string Name { get; set; } + public FilterCollection Filters { get; set; } + } + + public class Filter + { + public string Type { get; set; } + public int Value { get; set; } + } +} diff --git a/YamlDotNet.Test/Serialization/BufferedDeserialization/AnchorAliasObjectReferenceTest.cs b/YamlDotNet.Test/Serialization/BufferedDeserialization/AnchorAliasObjectReferenceTest.cs new file mode 100644 index 000000000..897976845 --- /dev/null +++ b/YamlDotNet.Test/Serialization/BufferedDeserialization/AnchorAliasObjectReferenceTest.cs @@ -0,0 +1,64 @@ +// This file is part of YamlDotNet - A .NET library for YAML. +// Copyright (c) Antoine Aubry and contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System.Collections.Generic; +using FluentAssertions; +using Xunit; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +namespace YamlDotNet.Test.Serialization.BufferedDeserialization; + +public class AnchorAliasObjectReferenceTest +{ + [Fact] + public void AnchorAliasObjectReference() + { + var deserializer = new DeserializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .Build(); + var executors = deserializer.Deserialize>(Document); + ReferenceEquals(executors[0].Filters, executors[1].Filters).Should().BeFalse(); + } + + public const string Document = @" +- name: a + filters: &shared + - type: foo + value: 1 + - type: bar + value: 2 +- name: b + filters: *shared +"; + + public class Executor + { + public string Name { get; set; } + public List Filters { get; set; } + } + + public class Filter + { + public string Type { get; set; } + public int Value { get; set; } + } +} diff --git a/YamlDotNet/Serialization/BufferedDeserialization/ParserAnchorBuffer.cs b/YamlDotNet/Serialization/BufferedDeserialization/ParserAnchorBuffer.cs new file mode 100644 index 000000000..bcec04eb3 --- /dev/null +++ b/YamlDotNet/Serialization/BufferedDeserialization/ParserAnchorBuffer.cs @@ -0,0 +1,70 @@ +// This file is part of YamlDotNet - A .NET library for YAML. +// Copyright (c) Antoine Aubry and contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System.Collections.Generic; +using YamlDotNet.Core; +using YamlDotNet.Core.Events; + +namespace YamlDotNet.Serialization.BufferedDeserialization; + +public class ParserAnchorBuffer : IParser +{ + private readonly LinkedList buffer; + + private LinkedListNode? current; + + public AnchorName Anchor { get; } + + public bool IsCycling { get; private set; } + + public ParserAnchorBuffer(AnchorName anchor, IParser parserToBuffer) + { + Anchor = anchor; + buffer = new LinkedList(); + var depth = -1; + do + { + var next = parserToBuffer.Consume(); + depth += next.NestingIncrease; + buffer.AddLast(next); + + if (next is AnchorAlias alias && alias.Value == Anchor) + { + IsCycling = true; + } + } while (depth >= 0); + + current = buffer.First; + } + + public ParsingEvent? Current => current?.Value; + + public bool MoveNext() + { + current = current?.Next; + return current != null; + } + + public void Reset() + { + current = buffer.First; + } +} diff --git a/YamlDotNet/Serialization/BufferedDeserialization/ParserBuffer.cs b/YamlDotNet/Serialization/BufferedDeserialization/ParserBuffer.cs index 2128f7f51..8f6ce2dc5 100644 --- a/YamlDotNet/Serialization/BufferedDeserialization/ParserBuffer.cs +++ b/YamlDotNet/Serialization/BufferedDeserialization/ParserBuffer.cs @@ -89,4 +89,4 @@ public void Reset() current = buffer.First; } } -} \ No newline at end of file +} diff --git a/YamlDotNet/Serialization/ValueDeserializers/AliasValueDeserializer.cs b/YamlDotNet/Serialization/ValueDeserializers/AliasValueDeserializer.cs index 8c82f3ad0..72a14342d 100644 --- a/YamlDotNet/Serialization/ValueDeserializers/AliasValueDeserializer.cs +++ b/YamlDotNet/Serialization/ValueDeserializers/AliasValueDeserializer.cs @@ -21,8 +21,10 @@ using System; using System.Collections.Generic; +using System.Linq; using YamlDotNet.Core; using YamlDotNet.Core.Events; +using YamlDotNet.Serialization.BufferedDeserialization; using YamlDotNet.Serialization.Utilities; namespace YamlDotNet.Serialization.ValueDeserializers @@ -36,11 +38,11 @@ public AliasValueDeserializer(IValueDeserializer innerDeserializer) this.innerDeserializer = innerDeserializer ?? throw new ArgumentNullException(nameof(innerDeserializer)); } - private sealed class AliasState : Dictionary, IPostDeserializationCallback + private sealed class AliasState : Dictionary, IPostDeserializationCallback { public void OnDeserialization() { - foreach (var promise in Values) + foreach (var promise in Values.OfType()) { if (!promise.HasValue) { @@ -51,6 +53,26 @@ public void OnDeserialization() } } + private sealed class BufferPromise : IValuePromise + { + public event Action? ValueAvailable; + + private readonly ParserAnchorBuffer buffer; + + public BufferPromise(ParserAnchorBuffer buffer) + { + this.buffer = buffer; + } + + public object? DeserializerValue(IValueDeserializer innerDeserializer, Type expectedType, SerializerState state, IValueDeserializer nestedObjectDeserializer) + { + buffer.Reset(); + var value = innerDeserializer.DeserializeValue(buffer, expectedType, state, nestedObjectDeserializer); + ValueAvailable?.Invoke(value); + return value; + } + } + private sealed class ValuePromise : IValuePromise { public event Action? ValueAvailable; @@ -80,6 +102,7 @@ public object? Value { throw new InvalidOperationException("Value not set"); } + return value; } set @@ -88,6 +111,7 @@ public object? Value { throw new InvalidOperationException("Value already set"); } + HasValue = true; this.value = value; @@ -98,7 +122,6 @@ public object? Value public object? DeserializeValue(IParser parser, Type expectedType, SerializerState state, IValueDeserializer nestedObjectDeserializer) { - object? value; if (parser.TryConsume(out var alias)) { var aliasState = state.Get(); @@ -107,41 +130,63 @@ public object? Value throw new AnchorNotFoundException(alias.Start, alias.End, $"Alias ${alias.Value} cannot precede anchor declaration"); } - return valuePromise.HasValue ? valuePromise.Value : valuePromise; + return valuePromise switch + { + BufferPromise buffered => buffered.DeserializerValue(innerDeserializer, expectedType, state, nestedObjectDeserializer), + ValuePromise cached => cached.HasValue ? cached.Value : cached, + _ => throw new InvalidCastException("Unknown anchor implementation") + }; } - var anchor = AnchorName.Empty; - if (parser.Accept(out var nodeEvent) && !nodeEvent.Anchor.IsEmpty) + if (!parser.Accept(out var nodeEvent) || nodeEvent.Anchor.IsEmpty) + { + return innerDeserializer.DeserializeValue(parser, expectedType, state, nestedObjectDeserializer); + } + + var anchor = nodeEvent.Anchor; + var start = parser.Current!.Start; + ParserAnchorBuffer buffer; + try + { + buffer = new ParserAnchorBuffer(anchor, parser); + } + catch (Exception exception) + { + throw new YamlException(start, parser.Current.End, "Failed to buffer yaml node", exception); + } + + if (buffer.IsCycling) { - anchor = nodeEvent.Anchor; var aliasState = state.Get(); if (!aliasState.ContainsKey(anchor)) { aliasState[anchor] = new ValuePromise(new AnchorAlias(anchor)); } - } - value = innerDeserializer.DeserializeValue(parser, expectedType, state, nestedObjectDeserializer); - - if (!anchor.IsEmpty) - { - var aliasState = state.Get(); + var value = innerDeserializer.DeserializeValue(buffer, expectedType, state, nestedObjectDeserializer); if (!aliasState.TryGetValue(anchor, out var valuePromise)) { aliasState.Add(anchor, new ValuePromise(value)); } - else if (!valuePromise.HasValue) + else if (valuePromise is ValuePromise { HasValue: false } cached) { - valuePromise.Value = value; + cached.Value = value; } else { aliasState[anchor] = new ValuePromise(value); } - } - return value; + return value; + } + else + { + var aliasState = state.Get(); + var buffered = new BufferPromise(buffer); + aliasState[anchor] = buffered; + return buffered.DeserializerValue(innerDeserializer, expectedType, state, nestedObjectDeserializer); + } } } }