Skip to content

Commit

Permalink
Add tests and improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
georgii-borovinskikh-sonarsource committed Jan 24, 2024
1 parent 8a20f45 commit 638b85b
Show file tree
Hide file tree
Showing 3 changed files with 195 additions and 41 deletions.
194 changes: 167 additions & 27 deletions src/SLCore.UnitTests/Protocol/EitherJsonConverterTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* SonarLint for Visual Studio
* Copyright (C) 2016-2023 SonarSource SA
* Copyright (C) 2016-2024 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
Expand All @@ -18,6 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

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

Expand All @@ -26,40 +27,179 @@ namespace SonarLint.VisualStudio.SLCore.UnitTests.Protocol;
[TestClass]
public class EitherJsonConverterTests
{
[TestMethod]
public void SimpleTest()
[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 respLeft = new SomeResponse { dto = Either<V1Dto, V2Dto>.CreateLeft(new V1Dto()) };
var respRight = new SomeResponse { dto = Either<V1Dto, V2Dto>.CreateRight(new V2Dto()) };
var testSubject = new EitherJsonConverter<SimpleObject.LeftOption, SimpleObject.RightOption>();

JsonConvert.DeserializeObject<SomeResponse>(JsonConvert.SerializeObject(respLeft)).Should().BeEquivalentTo(respLeft);
JsonConvert.DeserializeObject<SomeResponse>(JsonConvert.SerializeObject(respRight)).Should().BeEquivalentTo(respRight);
testSubject.CanConvert(typeToCheck).Should().Be(isSupported);
}

// todo add more tests

public class SomeResponse
[TestMethod]
public void SerializeObject_SerializesEitherAsSingleObject()
{
public string str = "aaaaa";
[JsonConverter(typeof(EitherJsonConverter<V1Dto, V2Dto>))]
public Either<V1Dto, V2Dto> dto { get; set; }
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}}""");
}

public class V1Dto

[TestMethod]
public void DeserializeObject_PrimitiveNotAnObject_Throws()
{
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 };
var str = """
{
"Property" : "ThisIsExpectedToBeAnObjectButItIsAString"
}
""";

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

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

public class V2Dto

[TestMethod]
public void DeserializeObject_CollectionNotAnObject_Throws()
{
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 };
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 };
}
}
}
12 changes: 8 additions & 4 deletions src/SLCore/Protocol/Either.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* SonarLint for Visual Studio
* Copyright (C) 2016-2023 SonarSource SA
* Copyright (C) 2016-2024 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
Expand All @@ -23,14 +23,18 @@ 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 class Either<TLeft, TRight>
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 };
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 };
}
}
30 changes: 20 additions & 10 deletions src/SLCore/Protocol/EitherJsonConverter.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* SonarLint for Visual Studio
* Copyright (C) 2016-2023 SonarSource SA
* Copyright (C) 2016-2024 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
Expand Down Expand Up @@ -31,8 +31,14 @@ namespace SonarLint.VisualStudio.SLCore.Protocol
/// <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 (int, string, etc.) are not supported</typeparam>
/// <typeparam name="TRight">A class that converts to json object. Primitive types (int, string, etc.) are not supported</typeparam>
/// <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
Expand All @@ -42,14 +48,19 @@ public class EitherJsonConverter<TLeft, TRight> : JsonConverter

public EitherJsonConverter()
{
// todo check primitive types?
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;
Expand All @@ -60,8 +71,8 @@ private static HashSet<string> GetAllPropertyAndFieldNames(Type type)

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

var either = (Either<TLeft, TRight>)value;
if (either.Left != null)
{
serializer.Serialize(writer, either.Left);
Expand All @@ -83,8 +94,7 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist

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

foreach (var jsonProperty in jToken.Children().Select(x => x.Path))
Expand All @@ -100,7 +110,7 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist
}
}

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

0 comments on commit 638b85b

Please sign in to comment.