Skip to content

Commit

Permalink
#604 Support for deserialization of interface types
Browse files Browse the repository at this point in the history
Fixes #601
+semver:feature
  • Loading branch information
aaubry committed Apr 8, 2021
2 parents 35db04b + 3219bbb commit b722b07
Show file tree
Hide file tree
Showing 5 changed files with 258 additions and 9 deletions.
131 changes: 131 additions & 0 deletions YamlDotNet.Test/Serialization/DeserializerTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// 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
{
public class DeserializerTest
{
[Fact]
public void Deserialize_YamlWithInterfaceTypeAndMapping_ReturnsModel()
{
var yaml = @"
name: Jack
cars:
- name: Mercedes
year: 2018
- name: Honda
year: 2021
";

var sut = new DeserializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.WithTypeMapping<ICar, Car>()
.Build();

var person = sut.Deserialize<Person>(yaml);
person.Name.Should().Be("Jack");
person.Cars.Should().HaveCount(2);
person.Cars[0].Name.Should().Be("Mercedes");
person.Cars[0].Spec.Should().BeNull();
person.Cars[1].Name.Should().Be("Honda");
person.Cars[1].Spec.Should().BeNull();
}

[Fact]
public void Deserialize_YamlWithTwoInterfaceTypesAndMappings_ReturnsModel()
{
var yaml = @"
name: Jack
cars:
- name: Mercedes
year: 2018
spec:
engineType: V6
driveType: AWD
- name: Honda
year: 2021
spec:
engineType: V4
driveType: FWD
";

var sut = new DeserializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.WithTypeMapping<ICar, Car>()
.WithTypeMapping<IModelSpec, ModelSpec>()
.Build();

var person = sut.Deserialize<Person>(yaml);
person.Name.Should().Be("Jack");
person.Cars.Should().HaveCount(2);
person.Cars[0].Name.Should().Be("Mercedes");
person.Cars[0].Spec.EngineType.Should().Be("V6");
person.Cars[0].Spec.DriveType.Should().Be("AWD");
person.Cars[1].Name.Should().Be("Honda");
person.Cars[1].Spec.EngineType.Should().Be("V4");
person.Cars[1].Spec.DriveType.Should().Be("FWD");
}

public class Person
{
public string Name { get; private set; }

public IList<ICar> Cars { get; private set; }
}

public class Car : ICar
{
public string Name { get; private set; }

public int Year { get; private set; }

public IModelSpec Spec { get; private set; }
}

public interface ICar
{
string Name { get; }

int Year { get; }
IModelSpec Spec { get; }
}

public class ModelSpec : IModelSpec
{
public string EngineType { get; private set; }

public string DriveType { get; private set; }
}

public interface IModelSpec
{
string EngineType { get; }

string DriveType { get; }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ public Lazy(T value)
valueState = ValueState.Created;
}

public Lazy(Func<T> valueFactory)
{
this.valueFactory = valueFactory;
this.isThreadSafe = false;
valueState = ValueState.NotCreated;
}

public Lazy(Func<T> valueFactory, bool isThreadSafe)
{
this.valueFactory = valueFactory;
Expand Down
50 changes: 43 additions & 7 deletions YamlDotNet/Serialization/DeserializerBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,11 @@ namespace YamlDotNet.Serialization
/// </summary>
public sealed class DeserializerBuilder : BuilderSkeleton<DeserializerBuilder>
{
private IObjectFactory objectFactory = new DefaultObjectFactory();
private Lazy<IObjectFactory> objectFactory;
private readonly LazyComponentRegistrationList<Nothing, INodeDeserializer> nodeDeserializerFactories;
private readonly LazyComponentRegistrationList<Nothing, INodeTypeResolver> nodeTypeResolverFactories;
private readonly Dictionary<TagName, Type> tagMappings;
private readonly Dictionary<Type, Type> typeMappings;
private bool ignoreUnmatched;

/// <summary>
Expand All @@ -53,6 +54,9 @@ public sealed class DeserializerBuilder : BuilderSkeleton<DeserializerBuilder>
public DeserializerBuilder()
: base(new StaticTypeResolver())
{
typeMappings = new Dictionary<Type, Type>();
objectFactory = new Lazy<IObjectFactory>(() => new DefaultObjectFactory(typeMappings), true);

tagMappings = new Dictionary<TagName, Type>
{
{ FailsafeSchema.Tags.Map, typeof(Dictionary<object, object>) },
Expand All @@ -71,20 +75,21 @@ public DeserializerBuilder()

nodeDeserializerFactories = new LazyComponentRegistrationList<Nothing, INodeDeserializer>
{
{ typeof(YamlConvertibleNodeDeserializer), _ => new YamlConvertibleNodeDeserializer(objectFactory) },
{ typeof(YamlSerializableNodeDeserializer), _ => new YamlSerializableNodeDeserializer(objectFactory) },
{ typeof(YamlConvertibleNodeDeserializer), _ => new YamlConvertibleNodeDeserializer(objectFactory.Value) },
{ typeof(YamlSerializableNodeDeserializer), _ => new YamlSerializableNodeDeserializer(objectFactory.Value) },
{ typeof(TypeConverterNodeDeserializer), _ => new TypeConverterNodeDeserializer(BuildTypeConverters()) },
{ typeof(NullNodeDeserializer), _ => new NullNodeDeserializer() },
{ typeof(ScalarNodeDeserializer), _ => new ScalarNodeDeserializer() },
{ typeof(ArrayNodeDeserializer), _ => new ArrayNodeDeserializer() },
{ typeof(DictionaryNodeDeserializer), _ => new DictionaryNodeDeserializer(objectFactory) },
{ typeof(CollectionNodeDeserializer), _ => new CollectionNodeDeserializer(objectFactory) },
{ typeof(DictionaryNodeDeserializer), _ => new DictionaryNodeDeserializer(objectFactory.Value) },
{ typeof(CollectionNodeDeserializer), _ => new CollectionNodeDeserializer(objectFactory.Value) },
{ typeof(EnumerableNodeDeserializer), _ => new EnumerableNodeDeserializer() },
{ typeof(ObjectNodeDeserializer), _ => new ObjectNodeDeserializer(objectFactory, BuildTypeInspector(), ignoreUnmatched) }
{ typeof(ObjectNodeDeserializer), _ => new ObjectNodeDeserializer(objectFactory.Value, BuildTypeInspector(), ignoreUnmatched) }
};

nodeTypeResolverFactories = new LazyComponentRegistrationList<Nothing, INodeTypeResolver>
{
{ typeof(MappingNodeTypeResolver), _ => new MappingNodeTypeResolver(typeMappings) },
{ typeof(YamlConvertibleTypeResolver), _ => new YamlConvertibleTypeResolver() },
{ typeof(YamlSerializableTypeResolver), _ => new YamlSerializableTypeResolver() },
{ typeof(TagNodeTypeResolver), _ => new TagNodeTypeResolver(tagMappings) },
Expand All @@ -100,7 +105,12 @@ public DeserializerBuilder()
/// </summary>
public DeserializerBuilder WithObjectFactory(IObjectFactory objectFactory)
{
this.objectFactory = objectFactory ?? throw new ArgumentNullException(nameof(objectFactory));
if (objectFactory == null)
{
throw new ArgumentNullException(nameof(objectFactory));
}

this.objectFactory = new Lazy<IObjectFactory>(() => objectFactory, true);
return this;
}

Expand Down Expand Up @@ -301,6 +311,32 @@ public override DeserializerBuilder WithTagMapping(TagName tag, Type type)
return this;
}

/// <summary>
/// Registers a type mapping using the default object factory.
/// </summary>
public DeserializerBuilder WithTypeMapping<TInterface, TConcrete>()
where TConcrete : TInterface
{
var interfaceType = typeof(TInterface);
var concreteType = typeof(TConcrete);

if (!interfaceType.IsAssignableFrom(concreteType))
{
throw new InvalidOperationException($"The type '{concreteType.Name}' does not implement interface '{interfaceType.Name}'.");
}

if (typeMappings.ContainsKey(interfaceType))
{
typeMappings[interfaceType] = concreteType;
}
else
{
typeMappings.Add(interfaceType, concreteType);
}

return this;
}

/// <summary>
/// Unregisters an existing tag mapping.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// 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 YamlDotNet.Core.Events;

namespace YamlDotNet.Serialization.NodeTypeResolvers
{
public class MappingNodeTypeResolver : INodeTypeResolver
{
private readonly IDictionary<Type, Type> _mappings;

public MappingNodeTypeResolver(IDictionary<Type, Type> mappings)
{
if (mappings == null) throw new ArgumentNullException(nameof(mappings));

foreach (var pair in mappings)
{
if (!pair.Key.IsAssignableFrom(pair.Value))
{
throw new InvalidOperationException($"Type '{pair.Value}' does not implement type '{pair.Key}'.");
}
}

_mappings = mappings;
}

public bool Resolve(NodeEvent? nodeEvent, ref Type currentType)
{
if (_mappings.TryGetValue(currentType, out var concreteType))
{
currentType = concreteType;
return true;
}

return false;
}
}
}
21 changes: 19 additions & 2 deletions YamlDotNet/Serialization/ObjectFactories/DefaultObjectFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,22 +30,39 @@ namespace YamlDotNet.Serialization.ObjectFactories
/// </summary>
public sealed class DefaultObjectFactory : IObjectFactory
{
private static readonly Dictionary<Type, Type> DefaultGenericInterfaceImplementations = new Dictionary<Type, Type>
private readonly Dictionary<Type, Type> DefaultGenericInterfaceImplementations = new Dictionary<Type, Type>
{
{ typeof(IEnumerable<>), typeof(List<>) },
{ typeof(ICollection<>), typeof(List<>) },
{ typeof(IList<>), typeof(List<>) },
{ typeof(IDictionary<,>), typeof(Dictionary<,>) }
};

private static readonly Dictionary<Type, Type> DefaultNonGenericInterfaceImplementations = new Dictionary<Type, Type>
private readonly Dictionary<Type, Type> DefaultNonGenericInterfaceImplementations = new Dictionary<Type, Type>
{
{ typeof(IEnumerable), typeof(List<object>) },
{ typeof(ICollection), typeof(List<object>) },
{ typeof(IList), typeof(List<object>) },
{ typeof(IDictionary), typeof(Dictionary<object, object>) }
};

public DefaultObjectFactory()
{
}

public DefaultObjectFactory(IDictionary<Type, Type> mappings)
{
foreach (var pair in mappings)
{
if (!pair.Key.IsAssignableFrom(pair.Value))
{
throw new InvalidOperationException($"Type '{pair.Value}' does not implement type '{pair.Key}'.");
}

DefaultNonGenericInterfaceImplementations.Add(pair.Key, pair.Value);
}
}

public object Create(Type type)
{
if (type.IsInterface())
Expand Down

0 comments on commit b722b07

Please sign in to comment.