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 C# Source Generator #1726

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
3 changes: 3 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="NodaTime" Version="3.1.9" />
<PackageVersion Include="NSwag.Core.Yaml" Version="14.0.0-preview011" />
<PackageVersion Include="Parlot" Version="0.0.25.0" />
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Source generators do not support transient dependencies: dotnet/sdk#17775
Because of that need to explicitly install some transient packages.
For example, it'll be used here to include Parlot into NuGet package https://github.com/RicoSuter/NJsonSchema/pull/1726/files#diff-ead3877feb09b2619c61d0feb8b85e0c85c627756febe0dda0829dafe13d5325R39

<PackageVersion Include="Pro.NBench.xUnit" Version="2.0.0" />
<PackageVersion Include="System.ComponentModel.Annotations" Version="4.4.0" />
<PackageVersion Include="System.Text.Json" Version="4.7.2" />
<PackageVersion Include="System.Text.Encodings.Web" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Abstractions" Version="8.0.0" />
<PackageVersion Include="Verify.XUnit" Version="14.7.0" />
<PackageVersion Include="xunit" Version="2.8.0" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.0" />
Expand Down
15 changes: 14 additions & 1 deletion src/NJsonSchema.Demo/NJsonSchema.Demo.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="../NJsonSchema.SourceGenerators.CSharp/NJsonSchema.SourceGenerators.CSharp.props" />

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
Expand All @@ -9,14 +10,26 @@
<StartupObject />
<Nullable>disable</Nullable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\NJsonSchema.Benchmark\NJsonSchema.Benchmark.csproj" />
<ProjectReference Include="..\NJsonSchema\NJsonSchema.csproj" />
<ProjectReference Include="..\NJsonSchema.SourceGenerators.CSharp\NJsonSchema.SourceGenerators.CSharp.csproj"
ReferenceOutputAssembly="false"
OutputItemType="Analyzer" />
</ItemGroup>

<ItemGroup>
<None Update="Tests\**\*.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

<ItemGroup>
<AdditionalFiles
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this project you can see how Source Generator actually works

Include="schema.json"
NJsonSchema_GenerateOptionalPropertiesAsNullable="true"
NJsonSchema_Namespace="NJsonSchema.Demo.Generated"
NJsonSchema_TypeNameHint="PersonGenerated"
NJsonSchema_FileName="Person.g.cs" />
</ItemGroup>

</Project>
21 changes: 21 additions & 0 deletions src/NJsonSchema.Demo/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"$id": "https://example.com/person.schema.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Person",
"type": "object",
"properties": {
"firstName": {
"type": "string",
"description": "The person's first name."
},
"lastName": {
"type": "string",
"description": "The person's last name."
},
"age": {
"description": "Age in years which must be equal to or greater than zero.",
"type": "integer",
"optional": true
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
using Microsoft.CodeAnalysis;
using System.Collections.Generic;
using System.Linq;
using Xunit.Abstractions;
using static NJsonSchema.SourceGenerators.CSharp.GeneratorConfigurationKeys;

namespace NJsonSchema.SourceGenerators.CSharp.Tests
{
public class JsonSchemaSourceGeneratorTests : TestsBase
{
public JsonSchemaSourceGeneratorTests(ITestOutputHelper output) : base(output)
{
}

[Fact]
public void When_no_additional_files_specified_then_no_source_is_generated()
{
var (compilation, outputDiagnostics) = GetGeneratedOutput(null, []);

Assert.Empty(outputDiagnostics);
Assert.Empty(compilation.SyntaxTrees);
}

[Fact]
public void When_invalid_path_specified_then_nothing_is_generated()
{
var (compilation, outputDiagnostics) = GetGeneratedOutput(null, [new AdditionalTextStub("not_existing.json")]);

Assert.NotEmpty(outputDiagnostics);
Assert.Single(outputDiagnostics);
var outputDiagnostic = outputDiagnostics[0];
Assert.Equal("NJSG001", outputDiagnostic.Id);
Assert.Equal(DiagnosticSeverity.Error, outputDiagnostic.Severity);

Assert.Empty(compilation.SyntaxTrees);
}

[Fact]
public void When_without_config_then_generated_with_default_values()
{
var firstName = "Alex";
var defaultNamespace = "MyNamespace";

string source = $@"
namespace Example
{{
class Test
{{
public static string RunTest()
{{
var json = new {defaultNamespace}.Person()
{{
FirstName = ""{firstName}""
}};
return json.FirstName;
}}
}}
}}";
var (compilation, outputDiagnostics) = GetGeneratedOutput(source, [new AdditionalTextStub("References/schema.json")]);

Assert.Empty(outputDiagnostics);

Assert.Equal(2, compilation.SyntaxTrees.Count());

Assert.Equal(firstName, RunTest(compilation));
}

[Theory]
[InlineData(null, false)]
[InlineData("false", false)]
[InlineData("False", false)]
[InlineData("true", true)]
[InlineData("True", true)]
public void When_GenerateOptionalPropertiesAsNullable_in_global_options_then_generate_according_to_config(
string generateOptionalPropertiesAsNullable,
bool shouldBeNullable)
{
string source = $@"
namespace Example
{{
class Test
{{
public static string RunTest()
{{
var json = new MyNamespace.Person();
return System.Convert.ToString(json.Age);
}}
}}
}}";
var globalOptions = new Dictionary<string, string>
{
{ GenerateOptionalPropertiesAsNullable, generateOptionalPropertiesAsNullable }
};
var (compilation, outputDiagnostics) = GetGeneratedOutput(
source,
[new AdditionalTextStub("References/schema.json")],
globalOptions);

Assert.Empty(outputDiagnostics);

Assert.Equal(2, compilation.SyntaxTrees.Count());

var expectedOutput = shouldBeNullable ? string.Empty : "0";
Assert.Equal(expectedOutput, RunTest(compilation));
}

[Theory]
[InlineData(null, "true", true)]
[InlineData("false", "true", false)]
[InlineData("False", "true", false)]
[InlineData("true", "false", true)]
[InlineData("True", "false", true)]
public void When_GenerateOptionalPropertiesAsNullable_in_additional_files_then_generate_according_to_config_and_override_global_if_possible(
string generateOptionalPropertiesAsNullableAdditionalFiles,
string generateOptionalPropertiesAsNullableGlobalOptions,
bool shouldBeNullable)
{
string source = $@"
namespace Example
{{
class Test
{{
public static string RunTest()
{{
var json = new MyNamespace.Person();
return System.Convert.ToString(json.Age);
}}
}}
}}";
var globalOptions = new Dictionary<string, string>
{
{ GenerateOptionalPropertiesAsNullable, generateOptionalPropertiesAsNullableGlobalOptions }
};
var additionalFilesOptions = new Dictionary<string, string>
{
{ GenerateOptionalPropertiesAsNullable, generateOptionalPropertiesAsNullableAdditionalFiles }
};
var (compilation, outputDiagnostics) = GetGeneratedOutput(
source,
[new AdditionalTextStub("References/schema.json", additionalFilesOptions)],
globalOptions);

Assert.Empty(outputDiagnostics);

Assert.Equal(2, compilation.SyntaxTrees.Count());

var expectedOutput = shouldBeNullable ? string.Empty : "0";
Assert.Equal(expectedOutput, RunTest(compilation));
}

[Theory]
[InlineData(null, null, "MyNamespace")]
[InlineData("", null, "MyNamespace")]
[InlineData(null, "", "MyNamespace")]
[InlineData(null, "NamespaceFromGlobalOptions", "NamespaceFromGlobalOptions")]
[InlineData("NamespaceFromLocalOptions", null, "NamespaceFromLocalOptions")]
[InlineData("NamespaceFromLocalOptions", "NamespaceFromGlobalOptions", "NamespaceFromLocalOptions")]
public void When_Namespace_in_config_then_generate(
string namespaceAdditionalFiles,
string namespaceGlobalOptions,
string expectedNamespace)
{
string source = $@"
namespace Example
{{
class Test
{{
public static string RunTest()
{{
var json = new {expectedNamespace}.Person();
return ""compiled"";
}}
}}
}}";
var globalOptions = new Dictionary<string, string>
{
{ Namespace, namespaceGlobalOptions }
};
var additionalFilesOptions = new Dictionary<string, string>
{
{ Namespace, namespaceAdditionalFiles }
};
var (compilation, outputDiagnostics) = GetGeneratedOutput(
source,
[new AdditionalTextStub("References/schema.json", additionalFilesOptions)],
globalOptions);

Assert.Empty(outputDiagnostics);

Assert.Equal(2, compilation.SyntaxTrees.Count());

Assert.Equal("compiled", RunTest(compilation));
}

[Theory]
[InlineData(null, null, "Person")]
[InlineData(null, "", "Person")]
[InlineData("", null, "Person")]
[InlineData(null, "ShouldNotOverride", "Person")]
[InlineData("ShouldOverride", null, "ShouldOverride")]
public void When_TypeNameHint_in_config_then_generate_using_additional_files_only(
string typeNameHintAdditionalFiles,
string typeNameHintGlobalOptions,
string expectedTypeName)
{
string source = $@"
namespace Example
{{
class Test
{{
public static string RunTest()
{{
var json = new MyNamespace.{expectedTypeName}();
return ""compiled"";
}}
}}
}}";
var globalOptions = new Dictionary<string, string>
{
{ TypeNameHint, typeNameHintGlobalOptions }
};
var additionalFilesOptions = new Dictionary<string, string>
{
{ TypeNameHint, typeNameHintAdditionalFiles }
};
var (compilation, outputDiagnostics) = GetGeneratedOutput(
source,
[new AdditionalTextStub("References/schema.json", additionalFilesOptions)],
globalOptions);

Assert.Empty(outputDiagnostics);

Assert.Equal(2, compilation.SyntaxTrees.Count());

Assert.Equal("compiled", RunTest(compilation));
}

[Theory]
[InlineData(null, null, "NJsonSchemaGenerated.g.cs")]
[InlineData("", null, "NJsonSchemaGenerated.g.cs")]
[InlineData(null, "", "NJsonSchemaGenerated.g.cs")]
[InlineData(null, "ShouldNotOverride.g.cs", "NJsonSchemaGenerated.g.cs")]
[InlineData("ShouldOverride.g.cs", null, "ShouldOverride.g.cs")]
public void When_FileName_in_config_then_generate_using_additional_files_only(
string fileNameAdditionalFiles,
string fileNameGlobalOptions,
string expectedFileName)
{
var globalOptions = new Dictionary<string, string>
{
{ FileName, fileNameGlobalOptions }
};
var additionalFilesOptions = new Dictionary<string, string>
{
{ FileName, fileNameAdditionalFiles }
};
var (compilation, outputDiagnostics) = GetGeneratedOutput(
null,
[new AdditionalTextStub("References/schema.json", additionalFilesOptions)],
globalOptions);

Assert.Empty(outputDiagnostics);

Assert.Single(compilation.SyntaxTrees);
var syntaxTree = compilation.SyntaxTrees.First();
Assert.EndsWith(expectedFileName, syntaxTree.FilePath);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<Nullable>disable</Nullable>
<IsTestProject>true</IsTestProject>
<EnableNETAnalyzers>false</EnableNETAnalyzers>
</PropertyGroup>

<ItemGroup>
<Content Include="..\NJsonSchema.Demo\schema.json" Link="References\schema.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\NJsonSchema.SourceGenerators.CSharp\NJsonSchema.SourceGenerators.CSharp.csproj" />
</ItemGroup>

<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>

</Project>
Loading
Loading