Skip to content

Commit

Permalink
Json source generation collections (#62)
Browse files Browse the repository at this point in the history
* Initial prototype using dfs and checking for cycles

* Polish debugging and fix tests

* Update tests and change accessibility for variables for sourcegenerator helper

* Add unit tests for diagnostics for users

* Move info logs to only successful generation

* Get rid of cycle check for next pr

* Get rid of stack logic for cycle checks

* Cache property values for typewrapper methods

* Added more collections relying on T

* Fix tests along with adding dicitonary support

* Update nits and got rid of stacks and queues

* Make tests static and delete todos

* Delete todos for dictionary types json context
  • Loading branch information
kevinwkt authored Aug 25, 2020
1 parent 8a838e3 commit bf5a0a1
Show file tree
Hide file tree
Showing 12 changed files with 423 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,25 @@ public class IndexViewModel
public bool HasFeaturedCampaign => FeaturedCampaign != null;
}

public class JsonSerializerSourceGeneratorTests
[JsonSerializable]
public class WeatherForecastWithPOCOs
{
public DateTimeOffset Date { get; set; }
public int TemperatureCelsius { get; set; }
public string Summary { get; set; }
public string SummaryField;
public List<DateTimeOffset> DatesAvailable { get; set; }
public Dictionary<string, HighLowTemps> TemperatureRanges { get; set; }
public string[] SummaryWords { get; set; }
}

public class HighLowTemps
{
public int High { get; set; }
public int Low { get; set; }
}

public static class JsonSerializerSourceGeneratorTests
{
[Fact]
public static void RoundTripLocation()
Expand All @@ -71,7 +89,7 @@ public static void RoundTripLocation()
}

[Fact]
public void RoundTripIndexViewModel()
public static void RoundTripIndexViewModel()
{
IndexViewModel expected = CreateIndexViewModel();

Expand Down Expand Up @@ -103,6 +121,17 @@ public static void RoundTripActiveOrUpcomingEvent()
VerifyActiveOrUpcomingEvent(expected, obj);
}

[Fact]
public static void RoundTripCollectionsDictionary()
{
WeatherForecastWithPOCOs expected = CreateWeatherForecastWithPOCOs();

string json = JsonSerializer.Serialize(expected, JsonContext.Instance.SystemTextJsonSourceGenerationTestsWeatherForecastWithPOCOs);
WeatherForecastWithPOCOs obj = JsonSerializer.Deserialize(json, JsonContext.Instance.SystemTextJsonSourceGenerationTestsWeatherForecastWithPOCOs);

VerifyWeatherForecastWithPOCOs(expected, obj);
}

internal static Location CreateLocation()
{
return new Location
Expand Down Expand Up @@ -223,5 +252,65 @@ internal static void VerifyIndexViewModel(IndexViewModel expected, IndexViewMode
Assert.Equal(expected.HasFeaturedCampaign, obj.HasFeaturedCampaign);
Assert.Equal(expected.IsNewAccount, obj.IsNewAccount);
}

internal static WeatherForecastWithPOCOs CreateWeatherForecastWithPOCOs()
{
return new WeatherForecastWithPOCOs
{
Date = DateTime.Parse("2019-08-01T00:00:00-07:00"),
TemperatureCelsius = 25,
Summary = "Hot",
DatesAvailable = new List<DateTimeOffset>
{
DateTimeOffset.Parse("2019-08-01T00:00:00-07:00"),
DateTimeOffset.Parse("2019-08-02T00:00:00-07:00"),
},
TemperatureRanges = new Dictionary<string, HighLowTemps> {
{
"Cold",
new HighLowTemps
{
High = 20,
Low = -10,
}
},
{
"Hot",
new HighLowTemps
{
High = 60,
Low = 20,
}
},
},
SummaryWords = new string[] { "Cool", "Windy", "Humid" },
};
}

internal static void VerifyWeatherForecastWithPOCOs(WeatherForecastWithPOCOs expected, WeatherForecastWithPOCOs obj)
{
Assert.Equal(expected.Date, obj.Date);
Assert.Equal(expected.TemperatureCelsius, obj.TemperatureCelsius);
Assert.Equal(expected.Summary, obj.Summary);
Assert.Equal(expected.DatesAvailable.Count, obj.DatesAvailable.Count);
for (int i = 0; i < expected.DatesAvailable.Count; i++)
{
Assert.Equal(expected.DatesAvailable[i], obj.DatesAvailable[i]);
}
List<KeyValuePair<string, HighLowTemps>> expectedTemperatureRanges = expected.TemperatureRanges.OrderBy(kv => kv.Key).ToList();
List<KeyValuePair<string, HighLowTemps>> objTemperatureRanges = obj.TemperatureRanges.OrderBy(kv => kv.Key).ToList();
Assert.Equal(expectedTemperatureRanges.Count, objTemperatureRanges.Count);
for (int i = 0; i < expectedTemperatureRanges.Count; i++)
{
Assert.Equal(expectedTemperatureRanges[i].Key, objTemperatureRanges[i].Key);
Assert.Equal(expectedTemperatureRanges[i].Value.Low, objTemperatureRanges[i].Value.Low);
Assert.Equal(expectedTemperatureRanges[i].Value.High, objTemperatureRanges[i].Value.High);
}
Assert.Equal(expected.SummaryWords.Length, obj.SummaryWords.Length);
for (int i = 0; i < expected.SummaryWords.Length; i++)
{
Assert.Equal(expected.SummaryWords[i], obj.SummaryWords[i]);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ public static Compilation CreateCompilation(string source, MetadataReference[] a
{
// Bypass System.Runtime error.
Assembly systemRuntimeAssembly = Assembly.Load("System.Runtime, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a");
Assembly systemCollectionsAssembly = Assembly.Load("System.Collections, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a");
string systemRuntimeAssemblyPath = systemRuntimeAssembly.Location;
string systemCollecitonsAssemblyPath = systemCollectionsAssembly.Location;

List<MetadataReference> references = new List<MetadataReference> {
MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
Expand All @@ -28,6 +30,7 @@ public static Compilation CreateCompilation(string source, MetadataReference[] a
MetadataReference.CreateFromFile(typeof(Type).Assembly.Location),
MetadataReference.CreateFromFile(typeof(KeyValuePair).Assembly.Location),
MetadataReference.CreateFromFile(systemRuntimeAssemblyPath),
MetadataReference.CreateFromFile(systemCollecitonsAssemblyPath),
};

// Add additional references as needed.
Expand Down Expand Up @@ -131,5 +134,20 @@ public class ActiveOrUpcomingEvent

return CreateCompilation(source);
}

public static Compilation CreateReferencedHighLowTempsCompilation()
{
string source = @"
namespace ReferencedAssembly
{
public class HighLowTemps
{
public int High { get; set; }
public int Low { get; set; }
}
}";

return CreateCompilation(source);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ namespace JsonSourceGenerator
[JsonSerializable]
public class IndexViewModel
{
public Dictionary<string, ActiveOrUpcomingEvent> ActiveOrUpcomingEvents { get; set; }
public ISet<ActiveOrUpcomingEvent> ActiveOrUpcomingEvents { get; set; }
public CampaignSummaryViewModel FeaturedCampaign { get; set; }
public bool IsNewAccount { get; set; }
public bool HasFeaturedCampaign => FeaturedCampaign != null;
Expand All @@ -102,7 +102,7 @@ public class IndexViewModel
CompilationHelper.RunGenerators(compilation, out var generatorDiags, generator);

// Expected warning logs.
string[] expectedWarningDiagnostics = new string[] { "Failed in sourcegenerating nested type Dictionary`2 for root type IndexViewModel" };
string[] expectedWarningDiagnostics = new string[] { "Failed in sourcegenerating nested type ISet`1 for root type IndexViewModel" };

CheckDiagnosticMessages(generatorDiags, DiagnosticSeverity.Info, new string[] { });
CheckDiagnosticMessages(generatorDiags, DiagnosticSeverity.Warning, expectedWarningDiagnostics);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,50 @@ public class ShouldNotFind { }
CheckFieldsPropertiesMethods("NotMyType", ref generator, expectedFieldNamesNotMyType, expectedPropertyNamesNotMyType, expectedMethodNamesNotMyType );
}

[Fact]
public void CollectionDictionarySourceGeneration()
{
// Compile the referenced assembly first.
Compilation referencedCompilation = CompilationHelper.CreateReferencedHighLowTempsCompilation();

// Emit the image of the referenced assembly.
byte[] referencedImage = CompilationHelper.CreateAssemblyImage(referencedCompilation);

string source = @"
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using ReferencedAssembly;
namespace HelloWorld
{
[JsonSerializable]
public class WeatherForecastWithPOCOs
{
public DateTimeOffset Date { get; set; }
public int TemperatureCelsius { get; set; }
public string Summary { get; set; }
public string SummaryField;
public List<DateTimeOffset> DatesAvailable { get; set; }
public Dictionary<string, HighLowTemps> TemperatureRanges { get; set; }
public string[] SummaryWords { get; set; }
}
}";

MetadataReference[] additionalReferences = { MetadataReference.CreateFromImage(referencedImage) };

Compilation compilation = CompilationHelper.CreateCompilation(source, additionalReferences);

JsonSourceGenerator generator = new JsonSourceGenerator();

Compilation newCompilation = CompilationHelper.RunGenerators(compilation, out var generatorDiags, generator);

// Make sure compilation was successful.
CheckCompilationDiagnosticsErrors(generatorDiags);
CheckCompilationDiagnosticsErrors(newCompilation.GetDiagnostics());
}

private void CheckCompilationDiagnosticsErrors(ImmutableArray<Diagnostic> diagnostics)
{
Assert.Empty(diagnostics.Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,12 +179,16 @@ void GenerateForMembers(GenerationClassFrame currentFrame, Type newType, HashSet
}

// Check if current type is supported to be iterated over.
private static bool IsSupportedType(Type type)
private bool IsSupportedType(Type type)
{
if (type.IsArray)
{
return true;
}
if (type.IsIEnumerable())
{
// todo: Add more support to collections.
if (!type.IsIList())
if (!type.IsIList() && !type.IsIDictionary())
{
return false;
}
Expand Down Expand Up @@ -225,6 +229,8 @@ private void AddImportsToTypeClass(GenerationClassFrame currentFrame)

// Add base imports.
imports.Add("System");
imports.Add("System.Collections");
imports.Add("System.Collections.Generic");
imports.Add("System.Text.Json");
imports.Add("System.Text.Json.Serialization");
imports.Add("System.Text.Json.Serialization.Metadata");
Expand All @@ -251,8 +257,11 @@ private void AddImportsToTypeClass(GenerationClassFrame currentFrame)

foreach (string import in imports)
{
currentFrame.Source.Append($@"
if (import.Length > 0)
{
currentFrame.Source.Append($@"
using {import};");
}
}
}

Expand Down Expand Up @@ -298,7 +307,7 @@ private void TypeInfoGetterSetter(GenerationClassFrame currentFrame)
private bool InitializeTypeInfoProperties(GenerationClassFrame currentFrame)
{
Type propertyType;
Type genericType;
Type[] genericTypes;
string typeName;
string propertyName;

Expand All @@ -309,13 +318,24 @@ private bool InitializeTypeInfoProperties(GenerationClassFrame currentFrame)
propertyName = property.Name;
typeName = propertyType.FullName;

genericTypes = GetTypesToGenerate(propertyType);

// Check if Array.
if (propertyType.IsArray)
{
typeName = $"{genericTypes[0].FullName}[]";
}

// Check if IEnumerable.
if (propertyType.IsIEnumerable())
{
genericType = GetTypesToGenerate(propertyType).First();
if (propertyType.IsIList())
{
typeName = $"List<{genericType.FullName}>";
typeName = $"List<{genericTypes[0].FullName}>";
}
else if (propertyType.IsIDictionary())
{
typeName = $"Dictionary<{genericTypes[0].FullName}, {genericTypes[1].FullName}>";
}
else
{
Expand All @@ -342,23 +362,37 @@ private bool GenerateTypeInfoConstructor(GenerationClassFrame currentFrame)
var typeInfo = new JsonObjectInfo<{currentType.FullName}>(CreateObjectFunc, SerializeFunc, DeserializeFunc, context.GetOptions());
");


Type[] genericTypes;
Type propertyType;
Type genericType;
string typeClassInfoCall;

foreach (PropertyInfo property in currentFrame.Properties)
{
propertyType = property.PropertyType;
// Default classtype for values.
typeClassInfoCall = $"context.{propertyType.GetCompilableUniqueName()}";
genericTypes = GetTypesToGenerate(propertyType);

// Check if Array.
if (propertyType.IsArray)
{
typeClassInfoCall = $"KnownCollectionTypeInfos<{genericTypes[0].FullName}>.GetArray(context.{genericTypes[0].GetCompilableUniqueName()}, context)";
}
else
{
// Default classtype for values.
typeClassInfoCall = $"context.{propertyType.GetCompilableUniqueName()}";
}

// Check if IEnumerable.
if (propertyType.IsIEnumerable())
{
genericType = GetTypesToGenerate(propertyType).First();

if (propertyType.IsIList())
{
typeClassInfoCall = $"KnownCollectionTypeInfos<{genericType.FullName}>.GetList(context.{genericType.GetCompilableUniqueName()}, context)";
typeClassInfoCall = $"KnownCollectionTypeInfos<{genericTypes[0].FullName}>.GetList(context.{genericTypes[0].GetCompilableUniqueName()}, context)";
}
else if (propertyType.IsIDictionary())
{
typeClassInfoCall = $"KnownDictionaryTypeInfos<{genericTypes[0].FullName}, {genericTypes[1].FullName}>.GetDictionary(context.{genericTypes[1].GetCompilableUniqueName()}, context)";
}
else
{
Expand Down
Loading

0 comments on commit bf5a0a1

Please sign in to comment.