From e3aaf63162d917ccc792f3bea70b75dad0903d93 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 3 Jun 2025 20:24:48 +0100 Subject: [PATCH] Added tool to add JsonExtensionData annotation to all model classes --- .../JsonExtensionDataGenerator.csproj | 10 ++ .../JsonExtensionDataGeneratorLauncher.cs | 144 ++++++++++++++++++ server-csharp.sln | 7 + 3 files changed, 161 insertions(+) create mode 100644 Tools/JsonExtensionDataGenerator/JsonExtensionDataGenerator.csproj create mode 100644 Tools/JsonExtensionDataGenerator/JsonExtensionDataGeneratorLauncher.cs diff --git a/Tools/JsonExtensionDataGenerator/JsonExtensionDataGenerator.csproj b/Tools/JsonExtensionDataGenerator/JsonExtensionDataGenerator.csproj new file mode 100644 index 00000000..85b49591 --- /dev/null +++ b/Tools/JsonExtensionDataGenerator/JsonExtensionDataGenerator.csproj @@ -0,0 +1,10 @@ + + + + Exe + net9.0 + enable + enable + + + diff --git a/Tools/JsonExtensionDataGenerator/JsonExtensionDataGeneratorLauncher.cs b/Tools/JsonExtensionDataGenerator/JsonExtensionDataGeneratorLauncher.cs new file mode 100644 index 00000000..c973b6b9 --- /dev/null +++ b/Tools/JsonExtensionDataGenerator/JsonExtensionDataGeneratorLauncher.cs @@ -0,0 +1,144 @@ +using System.Text.RegularExpressions; + +namespace JsonExtensionDataGenerator; + +public class JsonExtensionDataGeneratorLauncher +{ + private static readonly Regex _recordAndClassRegex = new("^(public record |public class )", RegexOptions.Multiline); + private static readonly Regex _endRecordClassRegex = new("^}", RegexOptions.Multiline); + private static readonly Regex _startRecordClassRegex = new("^{", RegexOptions.Multiline); + private const int StartRecordClassOffset = 3; + + private static readonly Regex _extensionFinding = + new( + // https://regexr.com/8f5gf + "^(public){0,1} (record|class) (\\w+(<(\\w+(,){0,1})+>){0,1})(\\(.*\\)){0,1}[\r\n ]*:[\r\n ]*(\\w+(<(\\w+(,){0,1})+>){0,1}([\r\n ]*,[\r\n ]*)*)+" + , RegexOptions.Multiline); + + private static readonly Regex _extensionCleanup = new(",.*"); + + private const string Insertion = + " [JsonExtensionData]\r\n public Dictionary ExtensionData { get; set; }\r\n\r\n"; + + private const string Using = "using System.Text.Json.Serialization;\r\n"; + + + public static void Main(string[] args) + { + var modelFiles = LoadModelFiles(); + foreach (var modelFile in modelFiles) + { + ProcessFile(modelFile); + } + } + + private static void ProcessFile(string modelFile) + { + Console.WriteLine($"Processing file: {modelFile}..."); + var fileName = Path.GetFileName(modelFile); + var content = File.ReadAllText(modelFile); + if (!content.Contains("public record ") && !content.Contains("public class ")) + { + Console.WriteLine($"File {fileName} doesnt contain any records or classes, skipping..."); + // probably an enum or interface + return; + } + + var classesAndRecordsToProcessCount = _recordAndClassRegex.Matches(content).Count; + Console.WriteLine($"Found {classesAndRecordsToProcessCount} records or classes for {fileName}"); + var firstTimeFlag = false; + var currentIndex = 0; + try + { + for (var i = 0; i < classesAndRecordsToProcessCount; i++) + { + var startIndex = FindNextClassStartIndex(content, currentIndex); + var endIndex = FindEndClassIndex(content, startIndex); + currentIndex = endIndex; + // Check if this class already has the tag anywhere + if (content.Substring(startIndex, endIndex - startIndex).Contains("[JsonExtensionData]")) + { + Console.WriteLine($"Class index {i} for {fileName} already contains [JsonExtensionData], skipping class..."); + continue; + } + + if (TryGetExtensions(content, startIndex, endIndex, out var extensions)) + { + if (extensions.Any(e => !e.StartsWith("I"))) + { + Console.WriteLine($"Class index {i} for {fileName} extends a parent class, skipping..."); + continue; + } + } + + // At this point we know for sure that we need to insert the [JsonExtensionData] + if (!firstTimeFlag) + { + if (!content.Contains("using System.Text.Json.Serialization;")) + { + Console.WriteLine($"Class index {i} for {fileName} doesnt contain using for Json.Serialization. Adding."); + // insert the using and adjust the indexes + content = Using + content; + startIndex += Using.Length; + endIndex += Using.Length; + currentIndex = endIndex; + } + + firstTimeFlag = true; + } + + // We need to add StartRecordClassOffset to offset the EOL + var insertionIndex = _startRecordClassRegex.Match(content, startIndex, endIndex - startIndex).Index + + StartRecordClassOffset; + content = content.Insert(insertionIndex, Insertion); + Console.WriteLine($"Class index {i} for {fileName} processed."); + currentIndex += Insertion.Length; + } + var stream = File.Open(modelFile, FileMode.Open); + stream.SetLength(0); + stream.Close(); + File.WriteAllText(modelFile, content); + } + catch (Exception e) + { + Console.WriteLine($"Error caught processing {modelFile} file\n{e}"); + } + } + + private static bool TryGetExtensions( + string content, + int startIndex, + int endIndex, + out IEnumerable extensions + ) + { + extensions = null; + var match = _extensionFinding.Match(content, startIndex, endIndex - startIndex); + if (match.Success) + { + var extensionsGroup = match.Groups[8]; + extensions = extensionsGroup.Captures.Select(c => _extensionCleanup.Replace(c.Value, "")); + return true; + } + + return false; + } + + private static int FindEndClassIndex(string content, int currentIndex) + { + // we do +3 cause thats the length of what we are searching for + return _endRecordClassRegex.Match(content, currentIndex).Index + 3; + } + + private static int FindNextClassStartIndex(string content, int currentIndex) + { + return _recordAndClassRegex.Match(content, currentIndex).Index; + } + + private static IEnumerable LoadModelFiles() + { + var projectDir = Directory.GetParent("./").Parent.Parent.Parent.Parent.Parent; + var modelsDir = Path.Combine(projectDir.FullName, "Libraries", "SPTarkov.Server.Core", "Models"); + return Directory.GetFiles(modelsDir, "*.cs", SearchOption.AllDirectories); + } +} diff --git a/server-csharp.sln b/server-csharp.sln index 6475a611..9ff3f9ea 100644 --- a/server-csharp.sln +++ b/server-csharp.sln @@ -36,6 +36,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "JsonExtensionData", "JsonEx EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonExtensionData.Fody", "Libraries\FodyWeavers\JsonExtensionData.Fody\JsonExtensionData.Fody.csproj", "{905FBA04-D73A-4A46-930B-1B0C3A7C4EB8}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonExtensionDataGenerator", "Tools\JsonExtensionDataGenerator\JsonExtensionDataGenerator.csproj", "{6F4670CD-6861-47A8-9A02-2B63AD73A929}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -86,6 +88,10 @@ Global {905FBA04-D73A-4A46-930B-1B0C3A7C4EB8}.Debug|Any CPU.Build.0 = Debug|Any CPU {905FBA04-D73A-4A46-930B-1B0C3A7C4EB8}.Release|Any CPU.ActiveCfg = Release|Any CPU {905FBA04-D73A-4A46-930B-1B0C3A7C4EB8}.Release|Any CPU.Build.0 = Release|Any CPU + {6F4670CD-6861-47A8-9A02-2B63AD73A929}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6F4670CD-6861-47A8-9A02-2B63AD73A929}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6F4670CD-6861-47A8-9A02-2B63AD73A929}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6F4670CD-6861-47A8-9A02-2B63AD73A929}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -101,5 +107,6 @@ Global {61D0AD50-5B86-41E6-8B19-F11944AC3EE4} = {F084DDFD-89F3-44F9-89C3-5CA11F4CDEEF} {2938742B-34FA-47F2-A50B-E0470FB1D807} = {61D0AD50-5B86-41E6-8B19-F11944AC3EE4} {905FBA04-D73A-4A46-930B-1B0C3A7C4EB8} = {2938742B-34FA-47F2-A50B-E0470FB1D807} + {6F4670CD-6861-47A8-9A02-2B63AD73A929} = {587959C2-5AFA-4B77-B327-566610F9A289} EndGlobalSection EndGlobal