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

implements #60 with tests #65

Merged
merged 4 commits into from
Apr 25, 2019
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
1 change: 1 addition & 0 deletions JsonSubTypes.Tests/JsonSubTypes.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
<Compile Include="DynamicRegisterTests.cs" />
<Compile Include="HiearachyWithCollectionTests.cs" />
<Compile Include="JsonSubTypesTests.cs" />
<Compile Include="MultipleHierarchyLevelsTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="BaseIsAnInterfaceTests.cs" />
<Compile Include="DiscriminatorOfDifferentKindTests.cs" />
Expand Down
150 changes: 150 additions & 0 deletions JsonSubTypes.Tests/MultipleHierarchyLevelsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
using Newtonsoft.Json;
using NUnit.Framework;

namespace JsonSubTypes.Tests
{
[TestFixture]
public class MultipleHierarchyLevelsTests
{

[Test]
public void ShouldDeserializeNestedLevel()
{
Payload run = new Run();
var data = JsonConvert.SerializeObject(run);
Assert.IsInstanceOf<Run>(JsonConvert.DeserializeObject<Payload>(data));
}

public enum PayloadDiscriminator
{
COM = 0,
GAME = 1
}

public enum GameDiscriminator
{
RUN = 0,
WALK = 1
}

[JsonConverter(typeof(JsonSubtypes), PAYLOAD_KIND)]
[JsonSubtypes.KnownSubType(typeof(Game), PayloadDiscriminator.GAME)]
[JsonSubtypes.KnownSubType(typeof(Com), PayloadDiscriminator.COM)]
public abstract class Payload
{
public const string PAYLOAD_KIND = "$PayloadKind";

[JsonProperty(PAYLOAD_KIND)]
protected abstract PayloadDiscriminator PayloadKind { get; }
}

[JsonConverter(typeof(JsonSubtypes), GAME_KIND)]
[JsonSubtypes.KnownSubType(typeof(Run), GameDiscriminator.RUN)]
[JsonSubtypes.KnownSubType(typeof(Walk), GameDiscriminator.WALK)]
public abstract class Game : Payload
{
protected override PayloadDiscriminator PayloadKind => PayloadDiscriminator.GAME;

public const string GAME_KIND = "$GameKind";

[JsonProperty(GAME_KIND)]
protected abstract GameDiscriminator GameKind { get; }
}

public class Com : Payload
{
protected override PayloadDiscriminator PayloadKind => PayloadDiscriminator.COM;
}

public class Walk : Game
{
protected override GameDiscriminator GameKind => GameDiscriminator.WALK;
}

public class Run : Game
{
protected override GameDiscriminator GameKind => GameDiscriminator.RUN;
}
}

[TestFixture]
public class MultipleHierarchyLevelsDynamicRegistrationTests
{

[Test]
public void ShouldDeserializeNestedLevel()
{
var settings = new JsonSerializerSettings();
JsonConvert.DefaultSettings = () => settings;

settings.Converters.Add(JsonSubtypesConverterBuilder
.Of(typeof(Payload), Payload.PAYLOAD_KIND)
.SerializeDiscriminatorProperty()
.RegisterSubtype(typeof(Game), PayloadDiscriminator.GAME)
.RegisterSubtype(typeof(Com), PayloadDiscriminator.COM)
.Build());

settings.Converters.Add(JsonSubtypesConverterBuilder
.Of(typeof(Game), Game.GAME_KIND)
.SerializeDiscriminatorProperty()
.RegisterSubtype(typeof(Walk), GameDiscriminator.WALK)
.RegisterSubtype(typeof(Run), GameDiscriminator.RUN)
.Build());

Payload run = new Run();
var data = JsonConvert.SerializeObject(run, settings);
Assert.IsInstanceOf<Run>(JsonConvert.DeserializeObject<Payload>(data, settings));
}

public enum PayloadDiscriminator
{
COM = 0,
GAME = 1
}

public enum GameDiscriminator
{
RUN = 0,
WALK = 1
}

[JsonConverter(typeof(JsonSubtypes), PAYLOAD_KIND)]
[JsonSubtypes.KnownSubType(typeof(Game), PayloadDiscriminator.GAME)]
[JsonSubtypes.KnownSubType(typeof(Com), PayloadDiscriminator.COM)]
public abstract class Payload
{
public const string PAYLOAD_KIND = "$PayloadKind";

[JsonProperty(PAYLOAD_KIND)]
protected abstract PayloadDiscriminator PayloadKind { get; }
}

[JsonConverter(typeof(JsonSubtypes), GAME_KIND)]
[JsonSubtypes.KnownSubType(typeof(Run), GameDiscriminator.RUN)]
[JsonSubtypes.KnownSubType(typeof(Walk), GameDiscriminator.WALK)]
public abstract class Game : Payload
{
protected override PayloadDiscriminator PayloadKind => PayloadDiscriminator.GAME;

public const string GAME_KIND = "$GameKind";

[JsonProperty(GAME_KIND)]
protected abstract GameDiscriminator GameKind { get; }
}

public class Com : Payload
{
protected override PayloadDiscriminator PayloadKind => PayloadDiscriminator.COM;
}

public class Walk : Game
{
protected override GameDiscriminator GameKind => GameDiscriminator.WALK;
}

public class Run : Game
{
protected override GameDiscriminator GameKind => GameDiscriminator.RUN;
}
}
}
100 changes: 76 additions & 24 deletions JsonSubTypes/JsonSubtypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,14 +126,13 @@ private object ReadJson(JsonReader reader, Type objectType, JsonSerializer seria
default:
var lineNumber = 0;
var linePosition = 0;
var lineInfo = reader as IJsonLineInfo;
if (lineInfo != null && lineInfo.HasLineInfo())
if (reader is IJsonLineInfo lineInfo && lineInfo.HasLineInfo())
{
lineNumber = lineInfo.LineNumber;
linePosition = lineInfo.LinePosition;
}

throw new JsonReaderException(string.Format("Unrecognized token: {0}", reader.TokenType), reader.Path, lineNumber, linePosition, null);
throw new JsonReaderException($"Unrecognized token: {reader.TokenType}", reader.Path, lineNumber, linePosition, null);
}

return value;
Expand All @@ -159,7 +158,7 @@ private IList ReadArray(JsonReader reader, Type targetType, JsonSerializer seria

private static IList CreateCompatibleList(Type targetContainerType, Type elementType)
{
var typeInfo = GetTypeInfo(targetContainerType);
var typeInfo = ToTypeInfo(targetContainerType);
if (typeInfo.IsArray || typeInfo.IsAbstract)
{
return (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(elementType));
Expand All @@ -183,7 +182,7 @@ private object ReadObject(JsonReader reader, Type objectType, JsonSerializer ser
{
var jObject = JObject.Load(reader);

var targetType = GetType(jObject, objectType) ?? objectType;
var targetType = GetType(jObject, objectType, serializer) ?? objectType;

return ThreadStaticReadObject(reader, serializer, jObject, targetType);
}
Expand All @@ -201,6 +200,7 @@ private static JsonReader CreateAnotherReader(JToken jToken, JsonReader reader)
return jObjectReader;
}


private Type GetType(JObject jObject, Type parentType)
{
if (JsonDiscriminatorPropertyName == null)
Expand All @@ -211,14 +211,49 @@ private Type GetType(JObject jObject, Type parentType)
return GetTypeFromDiscriminatorValue(jObject, parentType);
}

private Type GetType(JObject jObject, Type parentType, JsonSerializer serializer)
{
Type targetType = parentType;
JsonSubtypes lastTypeResolver = null;
JsonSubtypes currentTypeResolver = this;

var jsonConverterCollection = serializer.Converters.OfType<JsonSubtypesConverter>();
while (currentTypeResolver != null && currentTypeResolver != lastTypeResolver)
{
targetType = currentTypeResolver.GetType(jObject, targetType);
lastTypeResolver = currentTypeResolver;
jsonConverterCollection = jsonConverterCollection.Where(c => c != currentTypeResolver);
currentTypeResolver = GetTypeResolver(ToTypeInfo(targetType), jsonConverterCollection);
}

return targetType;
}

private JsonSubtypes GetTypeResolver(TypeInfo targetType, IEnumerable<JsonSubtypesConverter> jsonConverterCollection)
{
if (targetType == null)
{
return null;
}

var jsonConverterAttribute = GetAttribute<JsonConverterAttribute>(targetType);
if (jsonConverterAttribute != null && ToTypeInfo(typeof(JsonSubtypes)).IsAssignableFrom(ToTypeInfo(jsonConverterAttribute.ConverterType)))
{
return (JsonSubtypes)Activator.CreateInstance(jsonConverterAttribute.ConverterType, jsonConverterAttribute.ConverterParameters);
}

return jsonConverterCollection
.FirstOrDefault(c => c.CanConvert(ToType(targetType)));
}

private static Type GetTypeByPropertyPresence(IDictionary<string, JToken> jObject, Type parentType)
{
var knownSubTypeAttributes = GetAttributes<KnownSubTypeWithPropertyAttribute>(parentType);
var knownSubTypeAttributes = GetAttributes<KnownSubTypeWithPropertyAttribute>(ToTypeInfo(parentType));

return knownSubTypeAttributes
.Select(knownType =>
{
if (TryGetValueInJson(jObject, knownType.PropertyName, out var _))
if (TryGetValueInJson(jObject, knownType.PropertyName, out JToken _))
return knownType.SubType;

return null;
Expand All @@ -241,7 +276,7 @@ private Type GetTypeFromDiscriminatorValue(IDictionary<string, JToken> jObject,
return GetTypeFromMapping(typeMapping, discriminatorValue);
}

return GetTypeByName(discriminatorValue.Value<string>(), parentType);
return GetTypeByName(discriminatorValue.Value<string>(), ToTypeInfo(parentType));
}

private static bool TryGetValueInJson(IDictionary<string, JToken> jObject, string propertyName, out JToken value)
Expand All @@ -264,12 +299,12 @@ private static bool TryGetValueInJson(IDictionary<string, JToken> jObject, strin
return true;
}

private static Type GetTypeByName(string typeName, Type parentType)
private static Type GetTypeByName(string typeName, TypeInfo parentType)
{
if (typeName == null)
return null;

var insideAssembly = GetTypeInfo(parentType).Assembly;
var insideAssembly = parentType.Assembly;

var typeByName = insideAssembly.GetType(typeName);
if (typeByName == null)
Expand All @@ -278,24 +313,29 @@ private static Type GetTypeByName(string typeName, Type parentType)
typeByName = insideAssembly.GetType(searchLocation + typeName, false, true);
}

return typeByName != null && GetTypeInfo(parentType).IsAssignableFrom(GetTypeInfo(typeByName)) ? typeByName : null;
var typeByNameInfo = ToTypeInfo(typeByName);
if (typeByNameInfo != null && parentType.IsAssignableFrom(typeByNameInfo))
{
return typeByName;
}

return null;
}

private static Type GetTypeFromMapping(Dictionary<object, Type> typeMapping, JToken discriminatorToken)
{
var targetlookupValueType = typeMapping.First().Key.GetType();
var lookupValue = discriminatorToken.ToObject(targetlookupValueType);
var targetLookupValueType = typeMapping.First().Key.GetType();
var lookupValue = discriminatorToken.ToObject(targetLookupValueType);

Type targetType;
if (typeMapping.TryGetValue(lookupValue, out targetType))
if (typeMapping.TryGetValue(lookupValue, out Type targetType))
return targetType;

return null;
}

protected virtual Dictionary<object, Type> GetSubTypeMapping(Type type)
{
return GetAttributes<KnownSubTypeAttribute>(type)
return GetAttributes<KnownSubTypeAttribute>(ToTypeInfo(type))
.ToDictionary(x => x.AssociatedValue, x => x.SubType);
}

Expand All @@ -313,29 +353,41 @@ private static object ThreadStaticReadObject(JsonReader reader, JsonSerializer s
}
}

private static IEnumerable<T> GetAttributes<T>(Type type) where T : Attribute
private static IEnumerable<T> GetAttributes<T>(TypeInfo typeInfo) where T : Attribute
{
return GetTypeInfo(type)
.GetCustomAttributes(false)
return typeInfo.GetCustomAttributes(false)
.OfType<T>();
}

private static T GetAttribute<T>(TypeInfo typeInfo) where T : Attribute
{
return GetAttributes<T>(typeInfo).FirstOrDefault();
}

private static IEnumerable<Type> GetGenericTypeArguments(Type type)
{
#if (NET35 || NET40)
var genericTypeArguments = type.GetGenericArguments();
return type.GetGenericArguments();
#else
var genericTypeArguments = type.GenericTypeArguments;
return type.GenericTypeArguments;
#endif
return genericTypeArguments;
}

private static TypeInfo GetTypeInfo(Type type)
private static TypeInfo ToTypeInfo(Type type)
{
#if (NET35 || NET40)
return type;
#else
return type.GetTypeInfo();
return type?.GetTypeInfo();
#endif
}

private static Type ToType(TypeInfo typeInfo)
{
#if (NET35 || NET40)
return typeInfo;
#else
return typeInfo?.AsType();
#endif
}
}
Expand Down
10 changes: 5 additions & 5 deletions appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ before_build:
- nuget restore
- ps: >-
if ($env:APPVEYOR_PULL_REQUEST_NUMBER) {
MSBuild.SonarQube.Runner.exe begin /k:"manuc66:JsonSubtypes" /d:"sonar.host.url=https://sonarqube.com" /d:"sonar.organization=manuc66-github" /d:"sonar.login=$env:sonar_token" /d:sonar.cs.opencover.reportsPaths=".\MyProject_coverage.xml" /d:sonar.cs.nunit.reportsPaths="nunitTestResult.xml" /d:"sonar.cpd.exclusions=**/*Tests.cs" /d:"sonar.language=cs" /d:"sonar.analysis.mode=preview" /d:"sonar.github.pullRequest=$env:APPVEYOR_PULL_REQUEST_NUMBER" /d:"sonar.github.repository=$env:APPVEYOR_REPO_NAME" /d:"sonar.github.oauth=$env:github_auth_token"
SonarScanner.MSBuild.exe begin /k:"manuc66:JsonSubtypes" /d:"sonar.host.url=https://sonarqube.com" /o:"manuc66-github" /d:"sonar.login=$env:sonar_token" /d:sonar.cs.opencover.reportsPaths=".\MyProject_coverage.xml" /d:sonar.cs.nunit.reportsPaths="nunitTestResult.xml" /d:"sonar.cpd.exclusions=**/*Tests.cs" /d:"sonar.language=cs" /d:"sonar.pullrequest.base=master" /d:"sonar.pullrequest.branch=$env:APPVEYOR_REPO_BRANCH" /d:"sonar.pullrequest.key=$env:APPVEYOR_PULL_REQUEST_NUMBER" /d:"sonar.pullrequest.provider=GitHub" /d:"sonar.pullrequest.github.repository=$env:APPVEYOR_REPO_NAME" /d:"sonar.github.oauth=$env:github_auth_token"
}
elseif ($env:APPVEYOR_REPO_BRANCH -eq "master") {
MSBuild.SonarQube.Runner.exe begin /k:"manuc66:JsonSubtypes" /d:"sonar.host.url=https://sonarqube.com" /d:"sonar.organization=manuc66-github" /d:"sonar.login=$env:sonar_token" /d:sonar.cs.opencover.reportsPaths=".\MyProject_coverage.xml" /d:sonar.cs.nunit.reportsPaths="nunitTestResult.xml" /d:"sonar.cpd.exclusions=**/*Tests.cs" /d:"sonar.language=cs"
SonarScanner.MSBuild.exe begin /k:"manuc66:JsonSubtypes" /d:"sonar.host.url=https://sonarqube.com" /o:"manuc66-github" /d:"sonar.login=$env:sonar_token" /d:sonar.cs.opencover.reportsPaths=".\MyProject_coverage.xml" /d:sonar.cs.nunit.reportsPaths="nunitTestResult.xml" /d:"sonar.cpd.exclusions=**/*Tests.cs" /d:"sonar.language=cs"
}
else {
MSBuild.SonarQube.Runner.exe begin /k:"manuc66:JsonSubtypes" /d:"sonar.host.url=https://sonarqube.com" /d:"sonar.organization=manuc66-github" /d:"sonar.login=$env:sonar_token" /d:"sonar.branch.name=$env:APPVEYOR_REPO_BRANCH" /d:"sonar.branch.target=master" /d:sonar.cs.opencover.reportsPaths=".\MyProject_coverage.xml" /d:sonar.cs.nunit.reportsPaths="nunitTestResult.xml" /d:"sonar.cpd.exclusions=**/*Tests.cs" /d:"sonar.language=cs"
else {
SonarScanner.MSBuild.exe begin /k:"manuc66:JsonSubtypes" /d:"sonar.host.url=https://sonarqube.com" /o:"manuc66-github" /d:"sonar.login=$env:sonar_token" /d:"sonar.branch.name=$env:APPVEYOR_REPO_BRANCH" /d:"sonar.branch.target=master" /d:sonar.cs.opencover.reportsPaths=".\MyProject_coverage.xml" /d:sonar.cs.nunit.reportsPaths="nunitTestResult.xml" /d:"sonar.cpd.exclusions=**/*Tests.cs" /d:"sonar.language=cs"
}

configuration: Release
Expand All @@ -40,7 +40,7 @@ test_script:
- codecov -f "MyProject_coverage.xml"

after_test:
- ps: MSBuild.SonarQube.Runner.exe end /d:"sonar.login=$env:sonar_token"
- ps: SonarScanner.MSBuild.exe end /d:"sonar.login=$env:sonar_token"

artifacts:
# pushing all *.nupkg files in build directory recursively
Expand Down