Skip to content

Commit

Permalink
Merge pull request #94 from neuroglia-io/fix-yaml-serialization
Browse files Browse the repository at this point in the history
Fixed a critical bug in both the EquatableDictionarySerializer and EquatableListSerializer
  • Loading branch information
cdavernas authored May 6, 2024
2 parents 8d65b07 + d1eefea commit 2e20de1
Show file tree
Hide file tree
Showing 6 changed files with 208 additions and 20 deletions.
31 changes: 30 additions & 1 deletion src/Neuroglia.Core/EquatableDictionary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@ namespace Neuroglia;
/// <typeparam name="TValue">The type of values contained by the dictionary</typeparam>
[CollectionDataContract]
public record EquatableDictionary<TKey, TValue>
: IDictionary<TKey, TValue>, IEnumerable<KeyValuePair<TKey, TValue>>
: IDictionary, IDictionary<TKey, TValue>, IEnumerable<KeyValuePair<TKey, TValue>>
where TKey : notnull
{

readonly object _syncRoot = new();

/// <summary>
/// Initializes a new <see cref="EquatableDictionary{TKey, TValue}"/>
/// </summary>
Expand Down Expand Up @@ -59,6 +61,23 @@ public EquatableDictionary() : this(new Dictionary<TKey, TValue>()) { }
/// <inheritdoc/>
public bool IsReadOnly => this.Items.IsReadOnly;

ICollection IDictionary.Keys => this.Items.Keys.ToList();

ICollection IDictionary.Values => this.Items.Values.ToList();

bool IDictionary.IsFixedSize => this.IsReadOnly;

bool IDictionary.IsReadOnly => this.IsReadOnly;

int ICollection.Count => this.Count;

bool ICollection.IsSynchronized => false;

object ICollection.SyncRoot => this._syncRoot;

/// <inheritdoc/>
public object? this[object key] { get => this[(TKey)key]; set => this[(TKey)key] = (TValue)value!; }

/// <inheritdoc/>
public void Add(TKey key, TValue value) => this.Items.Add(key, value);

Expand Down Expand Up @@ -114,4 +133,14 @@ public virtual bool Equals(EquatableDictionary<TKey, TValue>? other)

IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();

void IDictionary.Add(object key, object? value) => this.Add((TKey)key, (TValue)value!);

bool IDictionary.Contains(object key) => this.ContainsKey((TKey)key);

IDictionaryEnumerator IDictionary.GetEnumerator() => new Hashtable(this.ToDictionary(kvp => kvp.Key, kvp => kvp.Value)).GetEnumerator();

void IDictionary.Remove(object key) => this.Remove((TKey)key);

void ICollection.CopyTo(Array array, int index) => this.CopyTo((KeyValuePair<TKey, TValue>[])array, index);

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.

using System.Collections;
using System.Text;
using YamlDotNet.Core;
using YamlDotNet.Core.Events;
using YamlDotNet.Serialization;

namespace Neuroglia.Serialization.Yaml;
Expand All @@ -32,9 +35,39 @@ public class EquatableDictionarySerializer
/// <inheritdoc/>
public virtual void WriteYaml(IEmitter emitter, object? value, Type type)
{
if (value == null) return;
var node = Json.JsonSerializer.Default.SerializeToNode(value);
new JsonNodeTypeConverter(true).WriteYaml(emitter, node, type);
if (value == null || value is not IDictionary mapping) return;
emitter.Emit(new MappingStart(AnchorName.Empty, TagName.Empty, isImplicit: true, MappingStyle.Block));
foreach (DictionaryEntry kvp in mapping)
{
var keyYaml = YamlSerializer.Default.Serialize(kvp.Key);
var stream = new MemoryStream(Encoding.UTF8.GetBytes(keyYaml));
var streamReader = new StreamReader(stream);
var parser = new Parser(streamReader);
while (parser.MoveNext())
{
if (parser.Current == null || parser.Current is DocumentEnd) break;
if (parser.Current is StreamStart || parser.Current is DocumentStart) continue;
emitter.Emit(parser.Current);
}
streamReader.Dispose();
stream.Dispose();

var valueYaml = YamlSerializer.Default.Serialize(kvp.Value);
stream = new MemoryStream(Encoding.UTF8.GetBytes(valueYaml));
streamReader = new StreamReader(stream);
parser = new Parser(streamReader);
while (parser.MoveNext())
{
if (parser.Current == null || parser.Current is DocumentEnd) break;
if (parser.Current is StreamStart || parser.Current is DocumentStart) continue;
emitter.Emit(parser.Current);
}
streamReader.Dispose();
stream.Dispose();
}
emitter.Emit(new MappingEnd());
}



}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.

using System.Collections;
using System.Text;
using YamlDotNet.Core;
using YamlDotNet.Core.Events;
using YamlDotNet.Serialization;

namespace Neuroglia.Serialization.Yaml;
Expand All @@ -32,9 +35,22 @@ public class EquatableListSerializer
/// <inheritdoc/>
public virtual void WriteYaml(IEmitter emitter, object? value, Type type)
{
if (value == null) return;
var node = Json.JsonSerializer.Default.SerializeToNode(value);
new JsonNodeTypeConverter().WriteYaml(emitter, node, type);
if (value == null || value is not IEnumerable collection) return;
emitter.Emit(new SequenceStart(null, null, false, SequenceStyle.Block));
foreach (var item in collection)
{
var keyYaml = YamlSerializer.Default.Serialize(item);
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(keyYaml));
using var streamReader = new StreamReader(stream);
var parser = new Parser(streamReader);
while (parser.MoveNext())
{
if (parser.Current == null || parser.Current is DocumentEnd) break;
if (parser.Current is StreamStart || parser.Current is DocumentStart) continue;
emitter.Emit(parser.Current);
}
}
emitter.Emit(new SequenceEnd());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,18 @@ namespace Neuroglia.Serialization.Yaml;
/// <summary>
/// Represents the <see cref="INodeDeserializer"/> used to deserialize <see cref="JsonObject"/>s
/// </summary>
public class JsonObjectDeserializer
: INodeDeserializer
/// <remarks>
/// Initializes a new <see cref="JsonObjectDeserializer"/>
/// </remarks>
/// <param name="inner">The inner <see cref="INodeDeserializer"/></param>
public class JsonObjectDeserializer(INodeDeserializer inner)
: INodeDeserializer
{

/// <summary>
/// Initializes a new <see cref="JsonObjectDeserializer"/>
/// </summary>
/// <param name="inner">The inner <see cref="INodeDeserializer"/></param>
public JsonObjectDeserializer(INodeDeserializer inner)
{
this.Inner = inner;
}

/// <summary>
/// Gets the inner <see cref="INodeDeserializer"/>
/// </summary>
protected INodeDeserializer Inner { get; }
protected INodeDeserializer Inner { get; } = inner;

/// <inheritdoc/>
public virtual bool Deserialize(IParser reader, Type expectedType, Func<IParser, Type, object?> nestedObjectDeserializer, out object? value)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
// limitations under the License.

using Neuroglia.Serialization.Json;
using Neuroglia.UnitTests.Data.Events;

namespace Neuroglia.UnitTests.Cases.Serialization;

Expand Down
116 changes: 116 additions & 0 deletions test/Neuroglia.UnitTests/Cases/Serialization/YamlSerializerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Copyright © 2021-Present Neuroglia SRL. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"),
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using Neuroglia.Serialization.Yaml;
using YamlDotNet.Core;
using YamlDotNet.Serialization;

namespace Neuroglia.UnitTests.Cases.Serialization;

public class YamlSerializerTests
{

[Fact]
public void Serialize_Deserialize_Equatable_List_Should_Work()
{
//arrange
var toSerialize = new EquatableList<Fruit>()
{
new(){ Name = "apple" },
new(){ Name = "banana" },
new(){ Name = "orange" },
new(){ Name = "cherry" }
};

//act
var yaml = YamlSerializer.Default.Serialize(toSerialize);
var deserialized = YamlSerializer.Default.Deserialize<EquatableList<Fruit>>(yaml);

//assert
deserialized.Should().Equal(toSerialize);
}

[Fact]
public void Serialize_Deserialize_Equatable_Dictionary_Should_Work()
{
//arrange
var toSerialize = new EquatableDictionary<string, Fruit>()
{
new("green", new(){ Name = "apple" }),
new("yellow",new(){ Name = "banana" }),
new("orange",new(){ Name = "orange" }),
new("red",new(){ Name = "cherry" })
};

//act
var yaml = YamlSerializer.Default.Serialize(toSerialize);
var deserialized = YamlSerializer.Default.Deserialize<EquatableDictionary<string, Fruit>>(yaml);

//assert
deserialized.Should().Equal(toSerialize);
}

[Fact]
public void Serialize_Deserialize_Equatable_List_With_ScalarType_should_Work()
{
//arrange
var toSerialize = new EquatableList<Software>()
{
new() { Version = "1.0.0" },
new() { Version = "1.2.0" },
new() { Version = "1.2.3" }
};

//act
var yaml = YamlSerializer.Default.Serialize(toSerialize);
var deserialized = YamlSerializer.Default.Deserialize<EquatableList<Software>>(yaml);

//assert
deserialized.Should().Equal(toSerialize);
}

[Fact]
public void Serialize_Deserialize_Equatable_Dictionary_With_ScalarType_should_Work()
{
//arrange
var toSerialize = new EquatableDictionary<string, Software>()
{
new("fake-1", new() { Version = "1.0.0" }),
new("fake-2", new() { Version = "1.2.0" }),
new("fake-3", new() { Version = "1.2.3" })
};

//act
var yaml = YamlSerializer.Default.Serialize(toSerialize);
var deserialized = YamlSerializer.Default.Deserialize<EquatableDictionary<string, Software>>(yaml);

//assert
deserialized.Should().Equal(toSerialize);
}

record Fruit
{

public required string Name {get; set;}

}

record Software
{

[YamlMember(ScalarStyle = ScalarStyle.SingleQuoted)]
public required string Version { get; set; }

}

}

0 comments on commit 2e20de1

Please sign in to comment.