Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Either helper class & serialization #5190

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
205 changes: 205 additions & 0 deletions src/SLCore.UnitTests/Protocol/EitherJsonConverterTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
/*
* SonarLint for Visual Studio
* Copyright (C) 2016-2024 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

using System;
using Newtonsoft.Json;
using SonarLint.VisualStudio.SLCore.Protocol;

namespace SonarLint.VisualStudio.SLCore.UnitTests.Protocol;

[TestClass]
public class EitherJsonConverterTests
{
[DataTestMethod]
[DataRow(typeof(object), false)]
[DataRow(typeof(Either<ConflictingObject.ConflictingLeft, ConflictingObject.ConflictingRight>), false)]
[DataRow(typeof(Either<SimpleObject.LeftOption, ConflictingObject.ConflictingRight>), false)]
[DataRow(typeof(Either<SimpleObject.LeftOption, SimpleObject.RightOption>), true)]
public void CanConvert_TypeHasToMatch(Type typeToCheck, bool isSupported)
{
var testSubject = new EitherJsonConverter<SimpleObject.LeftOption, SimpleObject.RightOption>();

testSubject.CanConvert(typeToCheck).Should().Be(isSupported);
}

[TestMethod]
public void SerializeObject_SerializesEitherAsSingleObject()
{
var left = new SimpleObject
{
Property = Either<SimpleObject.LeftOption, SimpleObject.RightOption>.CreateLeft(
new SimpleObject.LeftOption
{ Left = "lll" })
};
var right = new SimpleObject
{
Property = Either<SimpleObject.LeftOption, SimpleObject.RightOption>.CreateRight(
new SimpleObject.RightOption
{ Right = 10 })
};

JsonConvert.SerializeObject(left).Should().BeEquivalentTo("""{"Property":{"Left":"lll"}}""");
JsonConvert.SerializeObject(right).Should().BeEquivalentTo("""{"Property":{"Right":10}}""");
}

[TestMethod]
public void DeserializeObject_PrimitiveNotAnObject_Throws()
{
var str = """
{
"Property" : "ThisIsExpectedToBeAnObjectButItIsAString"
}
""";

Action act = () => JsonConvert.DeserializeObject<SimpleObject>(str);

act.Should().ThrowExactly<InvalidOperationException>().WithMessage("Expected Object, found String");
}

[TestMethod]
public void DeserializeObject_CollectionNotAnObject_Throws()
{
var str = """
{
"Property" : [1, 2, 3]
}
""";

Action act = () => JsonConvert.DeserializeObject<SimpleObject>(str);

act.Should().ThrowExactly<InvalidOperationException>().WithMessage("Expected Object, found Array");
}

[TestMethod]
public void DeserializeObject_UnableToMatch_Throws()
{
var str = """
{
"Property" :
{
"unknown" : "value"
}
}
""";

Action act = () => JsonConvert.DeserializeObject<SimpleObject>(str);

act.Should().ThrowExactly<InvalidOperationException>().WithMessage("Unable to make a definitive choice between Either options");
}

[TestMethod]
public void DeserializeObject_UnresolvableTypes_Throws()
{
var str = """
{
"Property" :
{
"SameProperty" : "value"
}
}
""";


Action act = () => JsonConvert.DeserializeObject<ConflictingObject>(str);

act
.Should()
.ThrowExactly<JsonException>()
.WithInnerExceptionExactly<ArgumentException>()
.WithMessage(
"Types SonarLint.VisualStudio.SLCore.UnitTests.Protocol.EitherJsonConverterTests+ConflictingObject+ConflictingLeft and SonarLint.VisualStudio.SLCore.UnitTests.Protocol.EitherJsonConverterTests+ConflictingObject+ConflictingRight have equivalent sets of properties and fields");
}

[TestMethod]
public void SerializeDeserializeObject_ComplexObjects_DeserializesToCorrectEitherVariant()
{
var left = new ComplexObject
{
dto = Either<ComplexObject.LeftOption, ComplexObject.RightOption>.CreateLeft(new ComplexObject.LeftOption())
};
var right = new ComplexObject
{
dto = Either<ComplexObject.LeftOption, ComplexObject.RightOption>.CreateRight(
new ComplexObject.RightOption())
};

JsonConvert.DeserializeObject<ComplexObject>(JsonConvert.SerializeObject(left)).Should().BeEquivalentTo(left);
JsonConvert.DeserializeObject<ComplexObject>(JsonConvert.SerializeObject(right)).Should().BeEquivalentTo(right);
}

public class SimpleObject
{
[JsonConverter(typeof(EitherJsonConverter<LeftOption, RightOption>))]
public Either<LeftOption, RightOption> Property { get; set; }

public class LeftOption
{
public string Left { get; set; }
}

public class RightOption
{
public int Right;
}
}

public class ConflictingObject
{
[JsonConverter(typeof(EitherJsonConverter<ConflictingLeft, ConflictingRight>))]
public Either<ConflictingLeft, ConflictingRight> Property { get; set; }

public class ConflictingLeft
{
public string SameProperty { get; set; }
}

public class ConflictingRight
{
public string SameProperty { get; set; }
}
}


public class ComplexObject
{
public string str = "aaaaa";

[JsonConverter(typeof(EitherJsonConverter<LeftOption, RightOption>))]
public Either<LeftOption, RightOption> dto { get; set; }

public class LeftOption
{
public object ACommon { get; set; } = new { lala = 10 };
public object ACommon2 = new { lala = 10 };
public object V1Obj { get; set; } = new { dada = 20 };
public object V1Str = "strstrstr";
public object XCommon { get; set; } = new { lala = 10 };
}

public class RightOption
{
public object ACommon { get; set; } = new { lala = 20 };
public object ACommon2 = new { lala = 20 };
public object V2Obj { get; set; } = new { dada = 40 };
public object V2Str = "strstrstr2";
public object XCommon { get; set; } = new { lala = 20 };
}
}
}
40 changes: 40 additions & 0 deletions src/SLCore/Protocol/Either.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* SonarLint for Visual Studio
* Copyright (C) 2016-2024 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

namespace SonarLint.VisualStudio.SLCore.Protocol
{
/// <summary>
/// Represents an option class where only one of the properties <see cref="Left"/> or <see cref="Right"/> is not null
/// </summary>
public sealed class Either<TLeft, TRight>
where TLeft : class
where TRight : class
{
private Either()
{
}

public TLeft Left { get; private set; }
public TRight Right { get; private set; }

public static Either<TLeft, TRight> CreateLeft(TLeft left) => new Either<TLeft, TRight> { Left = left };
public static Either<TLeft, TRight> CreateRight(TRight right) => new Either<TLeft, TRight> { Right = right };
}
}
116 changes: 116 additions & 0 deletions src/SLCore/Protocol/EitherJsonConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
* SonarLint for Visual Studio
* Copyright (C) 2016-2024 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace SonarLint.VisualStudio.SLCore.Protocol
{
/// <summary>
/// <see cref="JsonConverter" /> for <see cref="Either{TLeft,TRight}" /> type.
/// Left and Right are serialized to and deserialize from the same json property.
/// </summary>
/// <typeparam name="TLeft">A class that converts to json object. Primitive types and Arrays are not supported</typeparam>
/// <typeparam name="TRight">A class that converts to json object. Primitive types and Arrays are not supported</typeparam>
/// <remarks>
/// <list type="bullet">
/// <item><a href="https://www.newtonsoft.com/json/help/html/serializationguide.htm#PrimitiveTypes">Primitive types</a></item>
/// <item><a href="https://www.newtonsoft.com/json/help/html/serializationguide.htm#ComplexTypes">Arrays (IList, IEnumerable, IList&lt;T&gt;, Array)</a></item>
/// </list>
/// </remarks>
public class EitherJsonConverter<TLeft, TRight> : JsonConverter
where TLeft : class
where TRight : class
{
private readonly HashSet<string> leftProperties;
private readonly HashSet<string> rightProperties;

public EitherJsonConverter()
{
leftProperties = GetAllPropertyAndFieldNames(typeof(TLeft));
rightProperties = GetAllPropertyAndFieldNames(typeof(TRight));
var intersection = leftProperties.Intersect(rightProperties).ToArray();
leftProperties.ExceptWith(intersection);
rightProperties.ExceptWith(intersection);

if (!rightProperties.Any() && !leftProperties.Any())
{
throw new ArgumentException(
$"Types {typeof(TLeft)} and {typeof(TRight)} have equivalent sets of properties and fields");
}
}

private static HashSet<string> GetAllPropertyAndFieldNames(Type type)
{
const BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public;
return type.GetProperties(bindingFlags).Select(x => x.Name)
.Concat(type.GetFields(bindingFlags).Select(x => x.Name))
.ToHashSet();
}

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var either = (Either<TLeft, TRight>)value;

if (either.Left != null)
{
serializer.Serialize(writer, either.Left);
return;
}

serializer.Serialize(writer, either.Right);
}

public override bool CanConvert(Type objectType)
{
return objectType == typeof(Either<TLeft, TRight>);
}

public override object ReadJson(JsonReader reader, Type objectType, object existingValue,
JsonSerializer serializer)
{
var jToken = JToken.ReadFrom(reader);

if (jToken.Type != JTokenType.Object)
{
throw new InvalidOperationException($"Expected {JTokenType.Object}, found {jToken.Type}");
}

foreach (var jsonProperty in jToken.Children().Select(x => x.Path))
{
if (leftProperties.Contains(jsonProperty))
{
return Either<TLeft, TRight>.CreateLeft(jToken.ToObject<TLeft>());
}

if (rightProperties.Contains(jsonProperty))
{
return Either<TLeft, TRight>.CreateRight(jToken.ToObject<TRight>());
}
}

throw new InvalidOperationException("Unable to make a definitive choice between Either options");
}
}
}