diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 14ef088..cbef130 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: 8.0.x + dotnet-version: 10.0.x - name: Restore dependencies run: dotnet restore diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 71b3260..a88d165 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -46,7 +46,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: 8.0.x + dotnet-version: 10.0.x - name: Restore dependencies run: dotnet restore @@ -79,6 +79,9 @@ jobs: Compress-Archive -Path artifacts/osx-x64/DotSchema -DestinationPath artifacts/dotschema-osx-x64-${{ steps.tag.outputs.tag }}.zip Compress-Archive -Path artifacts/osx-arm64/DotSchema -DestinationPath artifacts/dotschema-osx-arm64-${{ steps.tag.outputs.tag }}.zip + - name: Push to Azure Artifacts + run: dotnet nuget push ./artifacts/*.nupkg --api-key ${{ secrets.AZURE_ARTIFACTS_PAT }} --source https://pkgs.dev.azure.com/gauss-dev/Magnet/_packaging/Engineering/nuget/v3/index.json + - name: Create Release uses: softprops/action-gh-release@v2 with: @@ -87,4 +90,3 @@ jobs: artifacts/*.nupkg artifacts/*.zip generate_release_notes: true - diff --git a/DotSchema.Tests/Analyzers/SchemaAnalyzerTests.cs b/DotSchema.Tests/Analyzers/SchemaAnalyzerTests.cs index e51a0b4..263735c 100644 --- a/DotSchema.Tests/Analyzers/SchemaAnalyzerTests.cs +++ b/DotSchema.Tests/Analyzers/SchemaAnalyzerTests.cs @@ -17,16 +17,16 @@ private static SchemaInput LoadEmbeddedSchema(string filename) using var stream = assembly.GetManifestResourceStream(resourceName) ?? throw new InvalidOperationException($"Embedded resource not found: {resourceName}"); + using var reader = new StreamReader(stream); return SchemaInput.FromContent(filename, reader.ReadToEnd()); } - private static List GetTestSchemas() => - [ - LoadEmbeddedSchema("windows.schema.json"), - LoadEmbeddedSchema("linux.schema.json") - ]; + private static List GetTestSchemas() + { + return [LoadEmbeddedSchema("windows.schema.json"), LoadEmbeddedSchema("linux.schema.json")]; + } [Fact] public async Task AnalyzeAsync_DetectsSharedTypes() diff --git a/DotSchema.Tests/CodePostProcessorTests.cs b/DotSchema.Tests/CodePostProcessorTests.cs index 09e2217..f4f5bae 100644 --- a/DotSchema.Tests/CodePostProcessorTests.cs +++ b/DotSchema.Tests/CodePostProcessorTests.cs @@ -2,6 +2,8 @@ namespace DotSchema.Tests; public class CodePostProcessorTests { + private static readonly HashSet EmptySet = []; + [Fact] public void Process_SharedMode_RemovesVariantSpecificTypes() { @@ -25,9 +27,9 @@ public partial class VariantOnlyType code, GenerationMode.Shared, "", - [], + EmptySet, variantTypes, - [], + EmptySet, "Config"); Assert.Contains("SharedType", result); @@ -57,8 +59,8 @@ public partial class ConflictingType code, GenerationMode.Shared, "", - [], - [], + EmptySet, + EmptySet, conflictingTypes, "Config"); @@ -90,8 +92,8 @@ public partial class WindowsConfig GenerationMode.Variant, "Windows", sharedTypes, - [], - [], + EmptySet, + EmptySet, "Config"); Assert.DoesNotContain("public sealed class SharedType", result); @@ -114,11 +116,10 @@ public partial class WindowsConfig code, GenerationMode.Variant, "Windows", - [], - [], - [], - "Config", - true); + EmptySet, + EmptySet, + EmptySet, + "Config"); Assert.Contains("WindowsConfig : IConfig", result); } @@ -139,9 +140,9 @@ public partial class WindowsConfig code, GenerationMode.Variant, "Windows", - [], - [], - [], + EmptySet, + EmptySet, + EmptySet, "Config", false); @@ -164,9 +165,9 @@ public partial class MyType code, GenerationMode.All, "", - [], - [], - [], + EmptySet, + EmptySet, + EmptySet, "Config"); Assert.Contains("public sealed class MyType", result); diff --git a/DotSchema.Tests/DotSchema.Tests.csproj b/DotSchema.Tests/DotSchema.Tests.csproj index eb73e12..9ea1ff8 100644 --- a/DotSchema.Tests/DotSchema.Tests.csproj +++ b/DotSchema.Tests/DotSchema.Tests.csproj @@ -1,29 +1,30 @@ - - net8.0 - enable - enable - false - + + net10.0 + enable + enable + false + - - - - - - + + + + + + + - - - + + + - - - + + + - - - + + + - \ No newline at end of file + diff --git a/DotSchema.Tests/Generators/CleanTypeNameGeneratorTests.cs b/DotSchema.Tests/Generators/CleanTypeNameGeneratorTests.cs index 9f3e79b..58d5752 100644 --- a/DotSchema.Tests/Generators/CleanTypeNameGeneratorTests.cs +++ b/DotSchema.Tests/Generators/CleanTypeNameGeneratorTests.cs @@ -38,7 +38,7 @@ public void Generate_PreservesSimpleNames(string input, string expected) [Fact] public void Generate_RenamesRootTypeWithVariant() { - var generator = new CleanTypeNameGenerator("Windows", "Config", []); + var generator = new CleanTypeNameGenerator("Windows", "Config", new HashSet()); var schema = new JsonSchema(); var result = generator.Generate(schema, "Config", []); diff --git a/DotSchema.Tests/TestData/linux.schema.json b/DotSchema.Tests/TestData/linux.schema.json index dacabbf..7748e36 100644 --- a/DotSchema.Tests/TestData/linux.schema.json +++ b/DotSchema.Tests/TestData/linux.schema.json @@ -17,21 +17,31 @@ "SharedType": { "type": "object", "properties": { - "name": { "type": "string" }, - "value": { "type": "integer" } + "name": { + "type": "string" + }, + "value": { + "type": "integer" + } } }, "ProcessConfig": { "type": "object", "properties": { - "exe_path": { "type": "string" }, - "systemd_unit": { "type": "string" } + "exe_path": { + "type": "string" + }, + "systemd_unit": { + "type": "string" + } } }, "LinuxOnlyType": { "type": "object", "properties": { - "systemd": { "type": "string" } + "systemd": { + "type": "string" + } } } } diff --git a/DotSchema.Tests/TestData/windows.schema.json b/DotSchema.Tests/TestData/windows.schema.json index 4db53af..8d02e7a 100644 --- a/DotSchema.Tests/TestData/windows.schema.json +++ b/DotSchema.Tests/TestData/windows.schema.json @@ -17,21 +17,31 @@ "SharedType": { "type": "object", "properties": { - "name": { "type": "string" }, - "value": { "type": "integer" } + "name": { + "type": "string" + }, + "value": { + "type": "integer" + } } }, "ProcessConfig": { "type": "object", "properties": { - "exe_path": { "type": "string" }, - "registry_key": { "type": "string" } + "exe_path": { + "type": "string" + }, + "registry_key": { + "type": "string" + } } }, "WindowsOnlyType": { "type": "object", "properties": { - "registry": { "type": "string" } + "registry": { + "type": "string" + } } } } diff --git a/DotSchema/Analyzers/SchemaAnalyzer.cs b/DotSchema/Analyzers/SchemaAnalyzer.cs index a081c7a..ce41067 100644 --- a/DotSchema/Analyzers/SchemaAnalyzer.cs +++ b/DotSchema/Analyzers/SchemaAnalyzer.cs @@ -19,9 +19,9 @@ namespace DotSchema.Analyzers; /// The parsed primary schema (avoids re-parsing). /// The root type name extracted from the schema's title field. public sealed record SchemaAnalysisResult( - HashSet SharedTypes, - HashSet VariantTypes, - HashSet ConflictingTypes, + IReadOnlySet SharedTypes, + IReadOnlySet VariantTypes, + IReadOnlySet ConflictingTypes, string PrimarySchemaPath, JsonSchema PrimarySchema, string RootTypeName); @@ -35,14 +35,20 @@ public sealed record SchemaInput public string? FilePath { get; init; } public string? Content { get; init; } - public static SchemaInput FromFile(string filePath) => - new() { Name = Path.GetFileName(filePath), FilePath = filePath }; + public static SchemaInput FromFile(string filePath) + { + return new SchemaInput { Name = Path.GetFileName(filePath), FilePath = filePath }; + } - public static SchemaInput FromContent(string name, string content) => - new() { Name = name, Content = content }; + public static SchemaInput FromContent(string name, string content) + { + return new SchemaInput { Name = name, Content = content }; + } - public async Task ReadContentAsync(CancellationToken cancellationToken = default) => - Content ?? await File.ReadAllTextAsync(FilePath!, cancellationToken); + public async Task ReadContentAsync(CancellationToken cancellationToken = default) + { + return Content ?? await File.ReadAllTextAsync(FilePath!, cancellationToken).ConfigureAwait(false); + } } /// @@ -69,6 +75,7 @@ public Task AnalyzeAsync( CancellationToken cancellationToken = default) { var inputs = schemaPaths.Select(SchemaInput.FromFile).ToList(); + return AnalyzeAsync(inputs, currentVariant, cancellationToken); } @@ -87,7 +94,8 @@ public async Task AnalyzeAsync( var primarySchemaName = DeterminePrimarySchemaName(schemaNames, currentVariant); // Parse all schemas and extract type hashes - var (parsedSchemas, allSchemaTypes) = await ParseSchemasAsync(schemaInputs, cancellationToken); + var (parsedSchemas, allSchemaTypes) = await ParseSchemasAsync(schemaInputs, cancellationToken) + .ConfigureAwait(false); // Extract root type name from the first schema's title var rootTypeName = parsedSchemas.Values.FirstOrDefault()?.Title ?? Constants.DefaultRootTypeName; @@ -98,7 +106,13 @@ public async Task AnalyzeAsync( // With only one schema, we can't determine what's shared if (schemaInputs.Count < 2) { - return new SchemaAnalysisResult([], [], [], primarySchemaName, primarySchema, rootTypeName); + return new SchemaAnalysisResult( + new HashSet(), + new HashSet(), + new HashSet(), + primarySchemaName, + primarySchema, + rootTypeName); } // Categorize types as shared or conflicting @@ -113,7 +127,13 @@ public async Task AnalyzeAsync( conflictingTypes, rootTypeName); - return new SchemaAnalysisResult(sharedTypes, variantTypes, conflictingTypes, primarySchemaName, primarySchema, rootTypeName); + return new SchemaAnalysisResult( + sharedTypes, + variantTypes, + conflictingTypes, + primarySchemaName, + primarySchema, + rootTypeName); } /// @@ -127,8 +147,8 @@ private static string DeterminePrimarySchemaName(List schemaNames, strin } var variantSchema = schemaNames.FirstOrDefault(name => name.Contains( - currentVariant, - StringComparison.OrdinalIgnoreCase)); + currentVariant, + StringComparison.OrdinalIgnoreCase)); return variantSchema ?? schemaNames[0]; } @@ -146,8 +166,8 @@ private static string DeterminePrimarySchemaName(List schemaNames, strin { cancellationToken.ThrowIfCancellationRequested(); - var schemaJson = await input.ReadContentAsync(cancellationToken); - var schema = await JsonSchema.FromJsonAsync(schemaJson, cancellationToken); + var schemaJson = await input.ReadContentAsync(cancellationToken).ConfigureAwait(false); + var schema = await JsonSchema.FromJsonAsync(schemaJson, cancellationToken).ConfigureAwait(false); parsedSchemas[input.Name] = schema; var types = ExtractTypeHashes(schema); @@ -216,8 +236,8 @@ private HashSet DetermineVariantTypes( string rootTypeName) { var currentVariantSchemaName = schemaNames.FirstOrDefault(name => name.Contains( - currentVariant, - StringComparison.OrdinalIgnoreCase)) + currentVariant, + StringComparison.OrdinalIgnoreCase)) ?? schemaNames[0]; var currentTypes = ExtractTypeHashes(parsedSchemas[currentVariantSchemaName]); @@ -423,7 +443,7 @@ private void ExtractInlineTypeHashes(JsonSchema schema, Dictionary 0 || propSchema.OneOf.Count > 0) { - var pascalCaseName = char.ToUpperInvariant(propName[0]) + propName[1..]; + var pascalCaseName = char.ToUpperInvariant(propName[0]) + (propName.Length > 1 ? propName[1..] : ""); var typeName = _typeNameGenerator.Generate(propSchema, pascalCaseName, []); types[typeName] = ComputeSchemaHash(propSchema); } diff --git a/DotSchema/CodePostProcessor.cs b/DotSchema/CodePostProcessor.cs index 33bbf5b..17fd24c 100644 --- a/DotSchema/CodePostProcessor.cs +++ b/DotSchema/CodePostProcessor.cs @@ -1,3 +1,4 @@ +using System.Text; using System.Text.RegularExpressions; namespace DotSchema; @@ -8,11 +9,6 @@ namespace DotSchema; /// public static partial class CodePostProcessor { - /// - /// Maximum number of recursive iterations to prevent infinite loops. - /// - private const int MaxRemoveIterations = 100; - // Compiled regex patterns for performance [GeneratedRegex( @"//----------------------\s*\n// .*?\s*\n//----------------------\s*\n", @@ -37,9 +33,9 @@ public static string Process( string code, GenerationMode mode, string variant, - HashSet sharedTypes, - HashSet variantTypes, - HashSet conflictingTypes, + IReadOnlySet sharedTypes, + IReadOnlySet variantTypes, + IReadOnlySet conflictingTypes, string rootTypeName, bool generateInterface = true) { @@ -53,8 +49,8 @@ public static string Process( private static string CleanupSharedCode( string code, - HashSet variantSpecificTypes, - HashSet conflictingTypes, + IReadOnlySet variantSpecificTypes, + IReadOnlySet conflictingTypes, string rootTypeName) { code = RemoveAdditionalPropertiesBoilerplate(code); @@ -75,7 +71,7 @@ private static string CleanupSharedCode( private static string CleanupVariantCode( string code, - HashSet sharedTypes, + IReadOnlySet sharedTypes, string variant, string rootTypeName, bool generateInterface) @@ -206,7 +202,7 @@ private static string RemoveAdditionalPropertiesBoilerplate(string code) /// Removes all specified types from the code in a single pass by finding all type boundaries /// first, then building the result string without the removed sections. /// - private static string RemoveTypesBatch(string code, HashSet typeNames) + private static string RemoveTypesBatch(string code, IReadOnlySet typeNames) { if (typeNames.Count == 0) { @@ -233,7 +229,7 @@ private static string RemoveTypesBatch(string code, HashSet typeNames) var mergedRanges = MergeOverlappingRanges(rangesToRemove); // Build result string by copying non-removed sections - var result = new System.Text.StringBuilder(code.Length); + var result = new StringBuilder(code.Length); var currentPos = 0; foreach (var (start, end) in mergedRanges) diff --git a/DotSchema/Constants.cs b/DotSchema/Constants.cs index a2a81cb..af4546b 100644 --- a/DotSchema/Constants.cs +++ b/DotSchema/Constants.cs @@ -67,23 +67,6 @@ public static string GetVariantFileName(string variant, string rootTypeName) return $"{variant}{rootTypeName}.cs"; } - /// - /// Constants for JetBrains cleanup tool. - /// - public static class JetBrains - { - public const string DotnetExecutable = "dotnet"; - public const string CleanupProfile = "Built-in: Full Cleanup"; - } - - /// - /// Constants for file patterns. - /// - public static class FilePatterns - { - public const string SolutionPattern = "*.sln"; - } - /// /// Extracts a variant name from a schema filename. /// Handles various patterns: @@ -162,4 +145,21 @@ private static string ToPascalCase(string name) return char.ToUpperInvariant(name[0]) + name[1..].ToLowerInvariant(); } + + /// + /// Constants for JetBrains cleanup tool. + /// + public static class JetBrains + { + public const string DotnetExecutable = "dotnet"; + public const string CleanupProfile = "Built-in: Full Cleanup"; + } + + /// + /// Constants for file patterns. + /// + public static class FilePatterns + { + public const string SolutionPattern = "*.sln"; + } } diff --git a/DotSchema/DotSchema.csproj b/DotSchema/DotSchema.csproj index 6f29046..cfc3dc5 100644 --- a/DotSchema/DotSchema.csproj +++ b/DotSchema/DotSchema.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net10.0 enable enable default diff --git a/DotSchema/Generators/CleanTypeNameGenerator.cs b/DotSchema/Generators/CleanTypeNameGenerator.cs index c6e1639..659f033 100644 --- a/DotSchema/Generators/CleanTypeNameGenerator.cs +++ b/DotSchema/Generators/CleanTypeNameGenerator.cs @@ -10,7 +10,7 @@ namespace DotSchema.Generators; /// public sealed class CleanTypeNameGenerator : ITypeNameGenerator { - private readonly HashSet _conflictingTypes; + private readonly IReadOnlySet _conflictingTypes; private readonly string _rootTypeName; private readonly string? _variant; @@ -20,7 +20,7 @@ public sealed class CleanTypeNameGenerator : ITypeNameGenerator public CleanTypeNameGenerator() { _variant = null; - _conflictingTypes = []; + _conflictingTypes = new HashSet(); _rootTypeName = Constants.DefaultRootTypeName; } @@ -28,11 +28,11 @@ public CleanTypeNameGenerator() /// Creates a generator that renames the root type to "{variant}{rootTypeName}" /// and prefixes conflicting types with the variant name. /// - public CleanTypeNameGenerator(string variant, string rootTypeName, HashSet? conflictingTypes = null) + public CleanTypeNameGenerator(string variant, string rootTypeName, IReadOnlySet? conflictingTypes = null) { _variant = variant; _rootTypeName = rootTypeName; - _conflictingTypes = conflictingTypes ?? []; + _conflictingTypes = conflictingTypes ?? new HashSet(); } public string Generate(JsonSchema schema, string? typeNameHint, IEnumerable reservedTypeNames) diff --git a/DotSchema/Generators/PascalCasePropertyNameGenerator.cs b/DotSchema/Generators/PascalCasePropertyNameGenerator.cs index 87c160d..e4cd59e 100644 --- a/DotSchema/Generators/PascalCasePropertyNameGenerator.cs +++ b/DotSchema/Generators/PascalCasePropertyNameGenerator.cs @@ -22,6 +22,9 @@ private static string ToPascalCase(string name) var parts = name.Split('_'); - return string.Concat(parts.Select(p => string.IsNullOrEmpty(p) ? "" : char.ToUpperInvariant(p[0]) + p[1..])); + return string.Concat( + parts.Select(p => string.IsNullOrEmpty(p) + ? "" + : char.ToUpperInvariant(p[0]) + (p.Length > 1 ? p[1..] : ""))); } } diff --git a/DotSchema/Generators/SchemaGenerator.cs b/DotSchema/Generators/SchemaGenerator.cs index 058bc73..8938498 100644 --- a/DotSchema/Generators/SchemaGenerator.cs +++ b/DotSchema/Generators/SchemaGenerator.cs @@ -26,11 +26,11 @@ public static async Task RunAsync( if (options.Mode == GenerationMode.All) { - result = await GenerateAllAsync(options, generatedFiles, logger, cancellationToken); + result = await GenerateAllAsync(options, generatedFiles, logger, cancellationToken).ConfigureAwait(false); } else { - result = await GenerateAsync(options, generatedFiles, logger, cancellationToken); + result = await GenerateAsync(options, generatedFiles, logger, cancellationToken).ConfigureAwait(false); } if (result != 0) @@ -41,7 +41,7 @@ public static async Task RunAsync( // Run JetBrains cleanup on all generated files at the end (if enabled) if (options.RunCleanup) { - await JetBrainsCleanupRunner.RunAsync(generatedFiles, logger, cancellationToken); + await JetBrainsCleanupRunner.RunAsync(generatedFiles, logger, cancellationToken).ConfigureAwait(false); } logger.LogInformation("Done!"); @@ -62,7 +62,9 @@ private static async Task GenerateAllAsync( if (schemas.Count < 2) { - logger.LogError("All mode requires at least 2 schemas to detect shared vs variant-specific types"); + logger.LogError( + "All mode requires at least 2 schemas to detect shared vs variant-specific types, but {SchemaCount} provided", + schemas.Count); return 1; } @@ -87,7 +89,7 @@ private static async Task GenerateAllAsync( } // Extract root type name from the first schema's title - var rootTypeName = await ExtractRootTypeNameAsync(schemas[0], cancellationToken); + var rootTypeName = await ExtractRootTypeNameAsync(schemas[0], cancellationToken).ConfigureAwait(false); // Extract variant names from schema filenames (e.g., "windows.config.schema.json" -> "Windows") var variants = schemas @@ -100,7 +102,15 @@ private static async Task GenerateAllAsync( if (options.GenerateInterface) { var interfacePath = Path.Combine(outputDir, Constants.GetInterfaceFileName(rootTypeName)); - await GenerateInterfaceAsync(interfacePath, rootTypeName, variants, options.Namespace, options.DryRun, logger, cancellationToken); + await GenerateInterfaceAsync( + interfacePath, + rootTypeName, + variants, + options.Namespace, + options.DryRun, + logger, + cancellationToken) + .ConfigureAwait(false); if (!options.DryRun) { @@ -116,7 +126,8 @@ private static async Task GenerateAllAsync( Output = Path.Combine(outputDir, Constants.GetSharedFileName(rootTypeName)) }; - var result = await GenerateAsync(sharedOptions, generatedFiles, logger, cancellationToken); + var result = await GenerateAsync(sharedOptions, generatedFiles, logger, cancellationToken) + .ConfigureAwait(false); if (result != 0) { @@ -135,7 +146,8 @@ private static async Task GenerateAllAsync( Output = Path.Combine(outputDir, Constants.GetVariantFileName(variant, rootTypeName)) }; - result = await GenerateAsync(variantOptions, generatedFiles, logger, cancellationToken); + result = await GenerateAsync(variantOptions, generatedFiles, logger, cancellationToken) + .ConfigureAwait(false); if (result != 0) { @@ -153,8 +165,8 @@ private static async Task ExtractRootTypeNameAsync( string schemaPath, CancellationToken cancellationToken) { - var schemaJson = await File.ReadAllTextAsync(schemaPath, cancellationToken); - var schema = await JsonSchema.FromJsonAsync(schemaJson, cancellationToken); + var schemaJson = await File.ReadAllTextAsync(schemaPath, cancellationToken).ConfigureAwait(false); + var schema = await JsonSchema.FromJsonAsync(schemaJson, cancellationToken).ConfigureAwait(false); return schema.Title ?? Constants.DefaultRootTypeName; } @@ -187,12 +199,15 @@ public interface {{interfaceName}}; if (dryRun) { - logger.LogInformation("[DRY RUN] Would write {InterfaceName} interface to: {OutputPath}", interfaceName, outputPath); + logger.LogInformation( + "[DRY RUN] Would write {InterfaceName} interface to: {OutputPath}", + interfaceName, + outputPath); } else { logger.LogInformation("Writing {InterfaceName} interface to: {OutputPath}", interfaceName, outputPath); - await File.WriteAllTextAsync(outputPath, interfaceCode, cancellationToken); + await File.WriteAllTextAsync(outputPath, interfaceCode, cancellationToken).ConfigureAwait(false); } } @@ -211,8 +226,9 @@ public static async Task GenerateAsync( if (options.Mode is GenerationMode.Shared or GenerationMode.Variant && schemas.Count < 2) { logger.LogError( - "Mode '{Mode}' requires at least 2 schemas to detect shared vs variant-specific types", - options.Mode.ToString().ToLowerInvariant()); + "Mode '{Mode}' requires at least 2 schemas to detect shared vs variant-specific types, but {SchemaCount} provided", + options.Mode.ToString().ToLowerInvariant(), + schemas.Count); return 1; } @@ -230,7 +246,8 @@ public static async Task GenerateAsync( // Analyze schemas to detect shared vs variant-specific types var analyzer = new SchemaAnalyzer(logger); - var analysisResult = await analyzer.AnalyzeAsync(schemas, options.Variant, cancellationToken); + var analysisResult = await analyzer.AnalyzeAsync(schemas, options.Variant, cancellationToken) + .ConfigureAwait(false); logger.LogInformation( "Detected {SharedCount} shared types, {ConflictingCount} conflicting types, {VariantCount} variant-specific types", @@ -279,7 +296,9 @@ public static async Task GenerateAsync( if (options.DryRun) { logger.LogInformation("[DRY RUN] Would write generated code to: {OutputPath}", options.OutputPath); - logger.LogDebug("Generated code preview:\n{Code}", code[..Math.Min(code.Length, 500)] + (code.Length > 500 ? "\n..." : "")); + logger.LogDebug( + "Generated code preview:\n{Code}", + code[..Math.Min(code.Length, 500)] + (code.Length > 500 ? "\n..." : "")); } else { @@ -292,7 +311,7 @@ public static async Task GenerateAsync( } logger.LogInformation("Writing generated code to: {OutputPath}", options.OutputPath); - await File.WriteAllTextAsync(options.OutputPath, code, cancellationToken); + await File.WriteAllTextAsync(options.OutputPath, code, cancellationToken).ConfigureAwait(false); generatedFiles.Add(options.OutputPath); } diff --git a/DotSchema/JetBrainsCleanupRunner.cs b/DotSchema/JetBrainsCleanupRunner.cs index b5777c9..afc5917 100644 --- a/DotSchema/JetBrainsCleanupRunner.cs +++ b/DotSchema/JetBrainsCleanupRunner.cs @@ -24,22 +24,12 @@ public static async Task RunAsync( var absolutePaths = filePaths.Select(Path.GetFullPath).ToList(); - // Get the solution directory (where .config/dotnet-tools.json and .sln are) - // Go up from Generated -> DotSchema -> solution root - var solutionDir = Path.GetDirectoryName(Path.GetDirectoryName(Path.GetDirectoryName(absolutePaths[0]))); + // Find solution directory by walking up from the first file + var (solutionDir, slnFile) = FindSolutionDirectory(absolutePaths[0]); - if (solutionDir == null) + if (solutionDir == null || slnFile == null) { - logger.LogWarning("Could not determine solution directory for jb cleanupcode"); - - return; - } - - var slnFile = Directory.GetFiles(solutionDir, Constants.FilePatterns.SolutionPattern).FirstOrDefault(); - - if (slnFile == null) - { - logger.LogWarning("No .sln file found in {SolutionDir}", solutionDir); + logger.LogWarning("Could not find solution directory containing .sln file"); return; } @@ -91,7 +81,7 @@ public static async Task RunAsync( process.BeginOutputReadLine(); process.BeginErrorReadLine(); - await process.WaitForExitAsync(cancellationToken); + await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false); if (process.ExitCode != 0) { @@ -111,4 +101,27 @@ public static async Task RunAsync( logger.LogWarning("jb cleanupcode failed: {Message}", ex.Message); } } + + /// + /// Walks up the directory tree from the starting path to find a directory containing a .sln file. + /// + /// A tuple of (solution directory, solution file path) or (null, null) if not found. + private static (string? SolutionDir, string? SlnFile) FindSolutionDirectory(string startPath) + { + var dir = Path.GetDirectoryName(startPath); + + while (dir != null) + { + var slnFiles = Directory.GetFiles(dir, Constants.FilePatterns.SolutionPattern); + + if (slnFiles.Length > 0) + { + return (dir, slnFiles[0]); + } + + dir = Path.GetDirectoryName(dir); + } + + return (null, null); + } } diff --git a/DotSchema/Program.cs b/DotSchema/Program.cs index 0cf197b..8f3503b 100644 --- a/DotSchema/Program.cs +++ b/DotSchema/Program.cs @@ -1,10 +1,11 @@ using CommandLine; -using DotSchema; using DotSchema.Generators; using Microsoft.Extensions.Logging; +namespace DotSchema; + public static class Program { public static async Task Main(string[] args) @@ -19,9 +20,11 @@ public static async Task Main(string[] args) private static async Task RunAsync(GenerateOptions options) { // Determine log level based on verbose/quiet flags - var logLevel = options.Verbose ? LogLevel.Debug - : options.Quiet ? LogLevel.Error - : LogLevel.Information; + var logLevel = options.Verbose + ? LogLevel.Debug + : options.Quiet + ? LogLevel.Error + : LogLevel.Information; using var loggerFactory = LoggerFactory.Create(builder => { diff --git a/README.md b/README.md index e62363a..10ae9a8 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,16 @@ # DotSchema -A .NET tool that generates C# code from JSON schemas, with support for detecting shared and variant-specific types across multiple schema files. +A .NET tool that generates C# code from JSON schemas, with support for detecting shared and variant-specific types +across multiple schema files. ## Features - **JSON Schema to C# code generation** using NJsonSchema - **Multi-schema analysis** to detect shared vs variant-specific types - **Three generation modes:** - - `All` - Generates shared types + variant-specific types for all variants - - `Shared` - Generates only types that exist in all provided schemas - - `Variant` - Generates only types unique to a specific variant + - `All` - Generates shared types + variant-specific types for all variants + - `Shared` - Generates only types that exist in all provided schemas + - `Variant` - Generates only types unique to a specific variant - **Automatic interface generation** for variant types - **JetBrains code cleanup integration** (optional) @@ -51,15 +52,15 @@ dotnet run -- [options] ### Options -| Option | Short | Required | Description | -|--------|-------|----------|-------------| -| `--schemas` | `-s` | Yes | One or more JSON schema files to process | -| `--output` | `-o` | Yes | Output file path (Shared/Variant) or directory (All mode) | -| `--namespace` | `-n` | Yes | Namespace for generated types | -| `--mode` | `-m` | No | Generation mode: `All`, `Shared`, or `Variant` (default: `All`) | -| `--variant` | `-v` | No | Variant name for single-variant generation | -| `--no-interface` | | No | Skip generating the marker interface | -| `--no-cleanup` | | No | Skip running JetBrains code cleanup | +| Option | Short | Required | Description | +|------------------|-------|----------|-----------------------------------------------------------------| +| `--schemas` | `-s` | Yes | One or more JSON schema files to process | +| `--output` | `-o` | Yes | Output file path (Shared/Variant) or directory (All mode) | +| `--namespace` | `-n` | Yes | Namespace for generated types | +| `--mode` | `-m` | No | Generation mode: `All`, `Shared`, or `Variant` (default: `All`) | +| `--variant` | `-v` | No | Variant name for single-variant generation | +| `--no-interface` | | No | Skip generating the marker interface | +| `--no-cleanup` | | No | Skip running JetBrains code cleanup | ### Examples @@ -93,5 +94,6 @@ In `All` mode, the tool generates: - [CommandLineParser](https://github.com/commandlineparser/commandline) - Command line argument parsing - [NJsonSchema.CodeGeneration.CSharp](https://github.com/RicoSuter/NJsonSchema) - JSON Schema to C# code generation -- [Microsoft.Extensions.Logging](https://docs.microsoft.com/en-us/dotnet/core/extensions/logging) - Logging infrastructure +- [Microsoft.Extensions.Logging](https://docs.microsoft.com/en-us/dotnet/core/extensions/logging) - Logging + infrastructure