Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/OpenApiCodeGenerator.Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,12 @@ static async Task<int> RunAsync(string[] args)
bool immutableArrays = true;
bool immutableDictionaries = true;
var includeSchemas = new List<string>();
var excludeSchemas = new List<string>();
bool omitJsonPropertyNameAttributes = false;
bool inlinePrimitiveTypeAliases = false;
bool emitValidationAttributes = true;
bool emitObsoleteAttribute = true;
bool verbose = false;

for (int i = 0; i < args.Length; i++)
{
Expand All @@ -55,6 +57,9 @@ static async Task<int> 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;
Expand Down Expand Up @@ -85,6 +90,9 @@ static async Task<int> 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)
Expand Down Expand Up @@ -122,10 +130,12 @@ static async Task<int> RunAsync(string[] args)
UseImmutableDictionaries = immutableDictionaries,
AddDefaultValuesToProperties = addDefaultValuesToProperties,
IncludeSchemas = includeSchemas,
ExcludeSchemas = excludeSchemas,
OmitJsonPropertyNameAttributes = omitJsonPropertyNameAttributes,
InlinePrimitiveTypeAliases = inlinePrimitiveTypeAliases,
EmitValidationAttributes = emitValidationAttributes,
EmitObsoleteAttribute = emitObsoleteAttribute,
Verbose = verbose,
};

try
Expand Down Expand Up @@ -241,6 +251,7 @@ [output] Output file path (defaults to stdout)
-n, --namespace <name> C# namespace (default: GeneratedModels)
--model-prefix <prefix> Prefix every generated model type name
--include-schema <name> Include only the named schema and its dependencies (repeatable)
--exclude-schema <name> 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
Expand All @@ -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

Expand Down
31 changes: 30 additions & 1 deletion src/OpenApiCodeGenerator/CSharpSchemaGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,15 @@ public string GenerateFromSchemas(IDictionary<string, IOpenApiSchema> schemas)

private IDictionary<string, IOpenApiSchema> SelectSchemas(IDictionary<string, IOpenApiSchema> schemas)
{
// Apply ExcludeSchemas first
if (_options.ExcludeSchemas is { Count: > 0 } excludedSchemas)
{
var excludedSet = new HashSet<string>(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;
Expand Down Expand Up @@ -200,8 +209,28 @@ private static void CollectReferencedSchemaNames(IOpenApiSchema schema, Queue<st
}
}

private static void HandleDiagnostics(ReadResult result)
private void HandleDiagnostics(ReadResult result)
{
// Print diagnostics in verbose mode
if (_options.Verbose && result.Diagnostic is { } diag)
{
if (diag.Errors is { Count: > 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 })
Expand Down
23 changes: 23 additions & 0 deletions src/OpenApiCodeGenerator/GeneratorOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,16 @@ public sealed class GeneratorOptions
/// </summary>
public bool EmitObsoleteAttribute { get; init; } = true;

/// <summary>
/// When true, print OpenAPI diagnostic warnings and errors to stderr during generation.
/// </summary>
public bool Verbose { get; init; }

/// <summary>
/// Schemas to exclude from generation. When null or empty, no schemas are excluded.
/// </summary>
public IReadOnlyCollection<string>? ExcludeSchemas { get; init; }

/// <summary>
/// Validates the configured options before generation starts.
/// </summary>
Expand All @@ -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)
Expand Down
115 changes: 115 additions & 0 deletions tests/OpenApiCodeGenerator.Tests/CSharpSchemaGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
22 changes: 22 additions & 0 deletions tests/OpenApiCodeGenerator.Tests/GeneratorOptionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,26 @@ public void Validate_BlankIncludedSchema_ThrowsArgumentException()

Assert.Throws<ArgumentException>(() => options.Validate());
}

[Fact]
public void Validate_BlankExcludedSchema_ThrowsArgumentException()
{
var options = new GeneratorOptions
{
ExcludeSchemas = ["User", ""]
};

Assert.Throws<ArgumentException>(() => options.Validate());
}

[Fact]
public void Validate_ExcludeSchemas_Null_DoesNotThrow()
{
var options = new GeneratorOptions
{
ExcludeSchemas = null
};

options.Validate();
}
}