Skip to content

Commit

Permalink
Add C# Source Generator
Browse files Browse the repository at this point in the history
  • Loading branch information
valchetski committed Nov 29, 2024
1 parent 55fd77e commit de1a601
Show file tree
Hide file tree
Showing 17 changed files with 829 additions and 2 deletions.
3 changes: 3 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="NodaTime" Version="3.2.0" />
<PackageVersion Include="NSwag.Core.Yaml" Version="14.0.0" />
<PackageVersion Include="Parlot" Version="1.1.0" />
<PackageVersion Include="Pro.NBench.xUnit" Version="2.0.0" />
<PackageVersion Include="System.Text.Json" Version="8.0.5" />
<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="28.3.2" />
<PackageVersion Include="xunit" Version="2.9.2" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
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 @@ -10,14 +11,26 @@
<Nullable>disable</Nullable>
<NoWarn>$(NoWarn);CS1591;IDE0005</NoWarn>
</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
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,263 @@
using Microsoft.CodeAnalysis;
using Xunit.Abstractions;
using static NJsonSchema.SourceGenerators.CSharp.GeneratorConfigurationKeys;

namespace NJsonSchema.SourceGenerators.CSharp.Tests
{
public class JsonSchemaSourceGeneratorTests(ITestOutputHelper output) : TestsBase(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,35 @@
<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>
<NoWarn>$(NoWarn);1591</NoWarn>
</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

0 comments on commit de1a601

Please sign in to comment.