Add JSON schema validation to source gen

This commit is contained in:
Liza 2024-06-15 23:32:58 +02:00
parent 53f20c9e20
commit caf8cfe7ef
Signed by: liza
GPG Key ID: 7199F8D727D55F67
5 changed files with 81 additions and 3 deletions

View File

@ -16,6 +16,9 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Json.More.Net" Version="2.0.1.2" GeneratePathProperty="true" />
<PackageReference Include="JsonPointer.Net" Version="5.0.0" GeneratePathProperty="true" />
<PackageReference Include="JsonSchema.Net" Version="7.0.4" GeneratePathProperty="true" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4"> <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@ -29,9 +32,16 @@
<ProjectReference Include="..\Questionable.Model\Questionable.Model.csproj" /> <ProjectReference Include="..\Questionable.Model\Questionable.Model.csproj" />
</ItemGroup> </ItemGroup>
<PropertyGroup>
<GetTargetPathDependsOn>$(GetTargetPathDependsOn);GetDependencyTargetPaths</GetTargetPathDependsOn>
</PropertyGroup>
<Target Name="GetDependencyTargetPaths" AfterTargets="ResolvePackageDependenciesForBuild"> <Target Name="GetDependencyTargetPaths" AfterTargets="ResolvePackageDependenciesForBuild">
<ItemGroup> <ItemGroup>
<TargetPathWithTargetPlatformMoniker Include="..\Questionable.Model\$(OutputPath)\*.dll" IncludeRuntimeDependency="false" /> <TargetPathWithTargetPlatformMoniker Include="..\Questionable.Model\$(OutputPath)\*.dll" IncludeRuntimeDependency="false" />
<TargetPathWithTargetPlatformMoniker Include="$(PkgJson_More_Net)\lib\netstandard2.0\Json.More.dll" IncludeRuntimeDependency="false" />
<TargetPathWithTargetPlatformMoniker Include="$(PkgJsonPointer_Net)\lib\netstandard2.0\JsonPointer.Net.dll" IncludeRuntimeDependency="false" />
<TargetPathWithTargetPlatformMoniker Include="$(PkgJsonSchema_Net)\lib\netstandard2.0\JsonSchema.Net.dll" IncludeRuntimeDependency="false" />
</ItemGroup> </ItemGroup>
</Target> </Target>
</Project> </Project>

View File

@ -1,8 +1,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Nodes;
using Json.Schema;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp.Syntax;
@ -17,8 +21,16 @@ namespace Questionable.QuestPathGenerator;
/// When using a simple text file as a baseline, we can create a non-incremental source generator. /// When using a simple text file as a baseline, we can create a non-incremental source generator.
/// </summary> /// </summary>
[Generator] [Generator]
[SuppressMessage("MicrosoftCodeAnalysisReleaseTracking", "RS2008")]
public class QuestSourceGenerator : ISourceGenerator public class QuestSourceGenerator : ISourceGenerator
{ {
private static readonly DiagnosticDescriptor InvalidJson = new("QSG0001",
"Invalid JSON",
"Invalid quest file {0}",
nameof(QuestSourceGenerator),
DiagnosticSeverity.Error,
true);
public void Initialize(GeneratorInitializationContext context) public void Initialize(GeneratorInitializationContext context)
{ {
// No initialization required for this generator. // No initialization required for this generator.
@ -28,10 +40,15 @@ public class QuestSourceGenerator : ISourceGenerator
{ {
List<(ushort, QuestData)> quests = []; List<(ushort, QuestData)> quests = [];
// Find schema definition
AdditionalText jsonSchemaFile =
context.AdditionalFiles.Single(x => Path.GetFileName(x.Path) == "quest-v1.json");
var questSchema = JsonSchema.FromText(jsonSchemaFile.GetText()!.ToString());
// Go through all files marked as an Additional File in file properties. // Go through all files marked as an Additional File in file properties.
foreach (var additionalFile in context.AdditionalFiles) foreach (var additionalFile in context.AdditionalFiles)
{ {
if (additionalFile == null) if (additionalFile == null || additionalFile == jsonSchemaFile)
continue; continue;
if (Path.GetExtension(additionalFile.Path) != ".json") if (Path.GetExtension(additionalFile.Path) != ".json")
@ -44,7 +61,21 @@ public class QuestSourceGenerator : ISourceGenerator
if (text == null) if (text == null)
continue; continue;
var quest = JsonSerializer.Deserialize<QuestData>(text.ToString())!; var questNode = JsonNode.Parse(text.ToString());
var evaluationResult = questSchema.Evaluate(questNode, new EvaluationOptions()
{
Culture = CultureInfo.InvariantCulture,
OutputFormat = OutputFormat.List
});
if (!evaluationResult.IsValid)
{
var error = Diagnostic.Create(InvalidJson,
null,
Path.GetFileName(additionalFile.Path));
context.ReportDiagnostic(error);
}
var quest = questNode.Deserialize<QuestData>()!;
quests.Add((id, quest)); quests.Add((id, quest));
} }

View File

@ -2,6 +2,34 @@
"version": 1, "version": 1,
"dependencies": { "dependencies": {
".NETStandard,Version=v2.0": { ".NETStandard,Version=v2.0": {
"Json.More.Net": {
"type": "Direct",
"requested": "[2.0.1.2, )",
"resolved": "2.0.1.2",
"contentHash": "uF3QeiaXEfH92emz0/BWUiNtMSfxIIvgynuB0Bf1vF4s8eWTcZitBx9l+g/FDaJk5XxqBv9buQXizXKQcXFG1w==",
"dependencies": {
"System.Text.Json": "8.0.0"
}
},
"JsonPointer.Net": {
"type": "Direct",
"requested": "[5.0.0, )",
"resolved": "5.0.0",
"contentHash": "fm4T5w20AY6C+p5/pJr0vrXRNGgtSfHl34I1LxC9zdPwS9S3j0GiR1Mz/CVPWKDXXGDpCt1APHpCq7kn5adCfA==",
"dependencies": {
"Humanizer.Core": "2.14.1",
"Json.More.Net": "2.0.1.2"
}
},
"JsonSchema.Net": {
"type": "Direct",
"requested": "[7.0.4, )",
"resolved": "7.0.4",
"contentHash": "R0Hk2Tr/np4Q1NO8CBjyQsoiD1iFJyEQP20Sw7JnZCNGJoaSBe+g4b+nZqnBXPQhiqY5LGZ8JZwZkRh/eKZhEQ==",
"dependencies": {
"JsonPointer.Net": "5.0.0"
}
},
"Microsoft.CodeAnalysis.Analyzers": { "Microsoft.CodeAnalysis.Analyzers": {
"type": "Direct", "type": "Direct",
"requested": "[3.3.4, )", "requested": "[3.3.4, )",

View File

@ -22,8 +22,10 @@
<None Remove="ARealmReborn"/> <None Remove="ARealmReborn"/>
<None Remove="Shadowbringers"/> <None Remove="Shadowbringers"/>
<None Remove="Endwalker"/> <None Remove="Endwalker"/>
<None Remove="quest-v1.json" />
<AdditionalFiles Include="ARealmReborn\**\*.json" /> <AdditionalFiles Include="ARealmReborn\**\*.json" />
<AdditionalFiles Include="Shadowbringers\**\*.json" /> <AdditionalFiles Include="Shadowbringers\**\*.json" />
<AdditionalFiles Include="Endwalker\**\*.json" /> <AdditionalFiles Include="Endwalker\**\*.json" />
<AdditionalFiles Include="quest-v1.json" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -400,7 +400,14 @@
"if": { "if": {
"properties": { "properties": {
"InteractionType": { "InteractionType": {
"const": "Interact" "anyOf": [
{
"const": "Interact"
},
{
"const": "SinglePlayerDuty"
}
]
} }
} }
}, },