diff --git a/src/OpenApiCodeGenerator.Cli/Program.cs b/src/OpenApiCodeGenerator.Cli/Program.cs index 5c6dd4d..faf0e06 100644 --- a/src/OpenApiCodeGenerator.Cli/Program.cs +++ b/src/OpenApiCodeGenerator.Cli/Program.cs @@ -31,10 +31,12 @@ static async Task RunAsync(string[] args) bool immutableArrays = true; bool immutableDictionaries = true; var includeSchemas = new List(); + var excludeSchemas = new List(); bool omitJsonPropertyNameAttributes = false; bool inlinePrimitiveTypeAliases = false; bool emitValidationAttributes = true; bool emitObsoleteAttribute = true; + bool verbose = false; for (int i = 0; i < args.Length; i++) { @@ -55,6 +57,9 @@ static async Task RunAsync(string[] args) case "--include-schema": includeSchemas.Add(GetNextArg(args, ref i, "--include-schema")); break; + case "--exclude-schema": + excludeSchemas.Add(GetNextArg(args, ref i, "--exclude-schema")); + break; case "--no-doc-comments": docComments = false; break; @@ -85,6 +90,9 @@ static async Task RunAsync(string[] args) case "--no-deprecated-attributes": emitObsoleteAttribute = false; break; + case "--verbose": + verbose = true; + break; default: // Positional: first is input, second is output if (inputPath == null) @@ -122,10 +130,12 @@ static async Task RunAsync(string[] args) UseImmutableDictionaries = immutableDictionaries, AddDefaultValuesToProperties = addDefaultValuesToProperties, IncludeSchemas = includeSchemas, + ExcludeSchemas = excludeSchemas, OmitJsonPropertyNameAttributes = omitJsonPropertyNameAttributes, InlinePrimitiveTypeAliases = inlinePrimitiveTypeAliases, EmitValidationAttributes = emitValidationAttributes, EmitObsoleteAttribute = emitObsoleteAttribute, + Verbose = verbose, }; try @@ -241,6 +251,7 @@ [output] Output file path (defaults to stdout) -n, --namespace C# namespace (default: GeneratedModels) --model-prefix Prefix every generated model type name --include-schema Include only the named schema and its dependencies (repeatable) + --exclude-schema Exclude the named schema from generation (repeatable) --no-doc-comments Disable XML doc comment generation --no-header Disable auto-generated file header --no-default-non-nullable Don't treat defaults as non-nullable @@ -251,6 +262,7 @@ [output] Output file path (defaults to stdout) --inline-type-aliases Inline primitive aliases instead of emitting wrapper types --no-validation-attributes Skip validation attributes from OpenAPI constraints --no-deprecated-attributes Skip [Obsolete] on deprecated schemas and properties + --verbose Print OpenAPI diagnostics to stderr -v, --version Show version information -h, --help Show this help message diff --git a/src/OpenApiCodeGenerator/CSharpSchemaGenerator.cs b/src/OpenApiCodeGenerator/CSharpSchemaGenerator.cs index 4137563..7949533 100644 --- a/src/OpenApiCodeGenerator/CSharpSchemaGenerator.cs +++ b/src/OpenApiCodeGenerator/CSharpSchemaGenerator.cs @@ -97,6 +97,15 @@ public string GenerateFromSchemas(IDictionary schemas) private IDictionary SelectSchemas(IDictionary schemas) { + // Apply ExcludeSchemas first + if (_options.ExcludeSchemas is { Count: > 0 } excludedSchemas) + { + var excludedSet = new HashSet(excludedSchemas, StringComparer.Ordinal); + schemas = schemas + .Where(kvp => !excludedSet.Contains(kvp.Key)) + .ToDictionary(kvp => kvp.Key, kvp => kvp.Value, StringComparer.Ordinal); + } + if (_options.IncludeSchemas is not { Count: > 0 } includedSchemas) { return schemas; @@ -200,8 +209,28 @@ private static void CollectReferencedSchemaNames(IOpenApiSchema schema, Queue 0 } diagErrors) + { + foreach (OpenApiError error in diagErrors) + { + Console.Error.WriteLine($"[ERROR] {error.Message}"); + } + } + + if (diag.Warnings is { Count: > 0 } warnings) + { + foreach (OpenApiError warning in warnings) + { + Console.Error.WriteLine($"[WARN] {warning.Message}"); + } + } + } + // If the document parsed successfully with components/schemas, proceed // even if there are path-level or other non-schema validation errors. if (result.Document?.Components?.Schemas is { Count: > 0 }) diff --git a/src/OpenApiCodeGenerator/GeneratorOptions.cs b/src/OpenApiCodeGenerator/GeneratorOptions.cs index 5d07cbc..01d4d6d 100644 --- a/src/OpenApiCodeGenerator/GeneratorOptions.cs +++ b/src/OpenApiCodeGenerator/GeneratorOptions.cs @@ -76,6 +76,16 @@ public sealed class GeneratorOptions /// public bool EmitObsoleteAttribute { get; init; } = true; + /// + /// When true, print OpenAPI diagnostic warnings and errors to stderr during generation. + /// + public bool Verbose { get; init; } + + /// + /// Schemas to exclude from generation. When null or empty, no schemas are excluded. + /// + public IReadOnlyCollection? ExcludeSchemas { get; init; } + /// /// Validates the configured options before generation starts. /// @@ -101,6 +111,19 @@ public void Validate() throw new ArgumentException("IncludeSchemas must not contain null or blank schema names.", nameof(IncludeSchemas)); } } + + if (ExcludeSchemas is { Count: > 0 } excludedSchemas) + { + foreach (string schemaName in excludedSchemas) + { + if (!string.IsNullOrWhiteSpace(schemaName)) + { + continue; + } + + throw new ArgumentException("ExcludeSchemas must not contain null or blank schema names.", nameof(ExcludeSchemas)); + } + } } private static void ValidateNamespace(string? namespaceName) diff --git a/tests/OpenApiCodeGenerator.Tests/CSharpSchemaGeneratorTests.cs b/tests/OpenApiCodeGenerator.Tests/CSharpSchemaGeneratorTests.cs index c5537f6..845856b 100644 --- a/tests/OpenApiCodeGenerator.Tests/CSharpSchemaGeneratorTests.cs +++ b/tests/OpenApiCodeGenerator.Tests/CSharpSchemaGeneratorTests.cs @@ -2480,4 +2480,119 @@ public async Task Generate_DeprecatedSchema_CompilesSuccessfully() } #endregion + + #region ExcludeSchemas + + [Fact] + public void Generate_WithExcludeSchemas_RemovesExcludedSchemas() + { + string spec = """ + { + "openapi": "3.0.0", + "info": { "title": "Test", "version": "1.0" }, + "paths": {}, + "components": { + "schemas": { + "User": { + "type": "object", + "properties": { "name": { "type": "string" } } + }, + "Address": { + "type": "object", + "properties": { "city": { "type": "string" } } + }, + "IgnoreMe": { + "type": "object", + "properties": { "foo": { "type": "string" } } + } + } + } + } + """; + + var generator = new CSharpSchemaGenerator(new GeneratorOptions + { + ExcludeSchemas = ["IgnoreMe"] + }); + string result = generator.GenerateFromText(spec); + + Assert.Contains("public partial record User", result, StringComparison.Ordinal); + Assert.Contains("public partial record Address", result, StringComparison.Ordinal); + Assert.DoesNotContain("public partial record IgnoreMe", result, StringComparison.Ordinal); + } + + [Fact] + public void Generate_WithExcludeSchemas_AndIncludeSchemas_WorksTogether() + { + string spec = """ + { + "openapi": "3.0.0", + "info": { "title": "Test", "version": "1.0" }, + "paths": {}, + "components": { + "schemas": { + "User": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "address": { "$ref": "#/components/schemas/Address" } + } + }, + "Address": { + "type": "object", + "properties": { "city": { "type": "string" } } + }, + "IgnoreMe": { + "type": "object", + "properties": { "foo": { "type": "string" } } + } + } + } + } + """; + + var generator = new CSharpSchemaGenerator(new GeneratorOptions + { + IncludeSchemas = ["User"], + ExcludeSchemas = ["IgnoreMe"] + }); + string result = generator.GenerateFromText(spec); + + // User and its dependency Address should be included + Assert.Contains("public partial record User", result, StringComparison.Ordinal); + Assert.Contains("public partial record Address", result, StringComparison.Ordinal); + // IgnoreMe should be excluded + Assert.DoesNotContain("public partial record IgnoreMe", result, StringComparison.Ordinal); + } + + #endregion + + #region Verbose Diagnostics + + [Fact] + public void Generate_WithVerbose_DoesNotThrow() + { + string spec = """ + { + "openapi": "3.0.0", + "info": { "title": "Test", "version": "1.0" }, + "paths": {}, + "components": { + "schemas": { + "User": { + "type": "object", + "properties": { "name": { "type": "string" } } + } + } + } + } + """; + + var generator = new CSharpSchemaGenerator(new GeneratorOptions { Verbose = true }); + string result = generator.GenerateFromText(spec); + + Assert.Contains("public partial record User", result, StringComparison.Ordinal); + } + + #endregion } diff --git a/tests/OpenApiCodeGenerator.Tests/GeneratorOptionsTests.cs b/tests/OpenApiCodeGenerator.Tests/GeneratorOptionsTests.cs index 49d22cd..dbb9fdf 100644 --- a/tests/OpenApiCodeGenerator.Tests/GeneratorOptionsTests.cs +++ b/tests/OpenApiCodeGenerator.Tests/GeneratorOptionsTests.cs @@ -64,4 +64,26 @@ public void Validate_BlankIncludedSchema_ThrowsArgumentException() Assert.Throws(() => options.Validate()); } + + [Fact] + public void Validate_BlankExcludedSchema_ThrowsArgumentException() + { + var options = new GeneratorOptions + { + ExcludeSchemas = ["User", ""] + }; + + Assert.Throws(() => options.Validate()); + } + + [Fact] + public void Validate_ExcludeSchemas_Null_DoesNotThrow() + { + var options = new GeneratorOptions + { + ExcludeSchemas = null + }; + + options.Validate(); + } }