diff --git a/DotnetClientGenerator/CSharpClientGenerator.cs b/DotnetClientGenerator/CSharpClientGenerator.cs index 85612b2..a795c6d 100644 --- a/DotnetClientGenerator/CSharpClientGenerator.cs +++ b/DotnetClientGenerator/CSharpClientGenerator.cs @@ -23,7 +23,7 @@ public string GenerateClient(ParsedApiSpec spec, ClientGeneratorOptions options) sb.AppendLine($"namespace {namespaceName};"); sb.AppendLine(); - GenerateModelClasses(sb, spec.Schemas); + GenerateModelClasses(sb, spec.Schemas, options.GenerateModelInterfaces); // Generate response types for each endpoint foreach (var endpoint in spec.Endpoints) @@ -301,21 +301,26 @@ private string ToPascalCase(string input) return char.ToUpperInvariant(input[0]) + input[1..]; } - private void GenerateModelClasses(StringBuilder sb, IDictionary schemas) + private void GenerateModelClasses(StringBuilder sb, IDictionary schemas, bool generateInterfaces) { foreach (var schema in schemas) { if (schema.Value != null) { - GenerateModelClass(sb, schema.Key, schema.Value); + if (generateInterfaces) + { + GenerateModelInterface(sb, schema.Key, schema.Value); + sb.AppendLine(); + } + GenerateModelClass(sb, schema.Key, schema.Value, generateInterfaces); sb.AppendLine(); } } } - private void GenerateModelClass(StringBuilder sb, string className, OpenApiSchema schema) + private void GenerateModelInterface(StringBuilder sb, string className, OpenApiSchema schema) { - sb.AppendLine($"public class {className}"); + sb.AppendLine($"public interface I{className}"); sb.AppendLine("{"); if (schema.Properties != null) @@ -325,7 +330,33 @@ private void GenerateModelClass(StringBuilder sb, string className, OpenApiSchem var propertyName = ToPascalCase(property.Key); var propertyType = GetCSharpTypeFromSchema(property.Value); var isRequired = schema.Required?.Contains(property.Key) == true; - + + if (!isRequired && !propertyType.EndsWith("?") && IsNullableType(property.Value)) + { + propertyType += "?"; + } + + sb.AppendLine($" {propertyType} {propertyName} {{ get; }}"); + } + } + + sb.AppendLine("}"); + } + + private void GenerateModelClass(StringBuilder sb, string className, OpenApiSchema schema, bool generateInterface) + { + string interfaceImplementation = generateInterface ? $" : I{className}" : ""; + sb.AppendLine($"public class {className}{interfaceImplementation}"); + sb.AppendLine("{"); + + if (schema.Properties != null) + { + foreach (var property in schema.Properties) + { + var propertyName = ToPascalCase(property.Key); + var propertyType = GetCSharpTypeFromSchema(property.Value); + var isRequired = schema.Required?.Contains(property.Key) == true; + if (!isRequired && !propertyType.EndsWith("?") && IsNullableType(property.Value)) { propertyType += "?"; diff --git a/DotnetClientGenerator/ClientGeneratorOptions.cs b/DotnetClientGenerator/ClientGeneratorOptions.cs index d3650eb..91b892c 100644 --- a/DotnetClientGenerator/ClientGeneratorOptions.cs +++ b/DotnetClientGenerator/ClientGeneratorOptions.cs @@ -5,4 +5,5 @@ public class ClientGeneratorOptions public string? ClassName { get; init; } public string? Namespace { get; init; } public bool GenerateInterface { get; init; } + public bool GenerateModelInterfaces { get; init; } } \ No newline at end of file diff --git a/DotnetClientGenerator/Program.cs b/DotnetClientGenerator/Program.cs index d689ddd..4d79dd5 100644 --- a/DotnetClientGenerator/Program.cs +++ b/DotnetClientGenerator/Program.cs @@ -35,6 +35,11 @@ Description = "Generate an interface for the client class" }; +Option modelInterfacesOption = new("--generate-model-interfaces", "-m") +{ + Description = "Generate interfaces for model classes" +}; + RootCommand rootCommand = new("A tool for generating C# API clients from OpenAPI specifications"); rootCommand.Options.Add(inputOption); rootCommand.Options.Add(outputOption); @@ -42,6 +47,7 @@ rootCommand.Options.Add(namespaceOption); rootCommand.Options.Add(watchOption); rootCommand.Options.Add(interfaceOption); +rootCommand.Options.Add(modelInterfacesOption); rootCommand.SetAction(async (parseResult, _) => { @@ -51,20 +57,21 @@ var namespaceName = parseResult.GetValue(namespaceOption)!; var watch = parseResult.GetValue(watchOption); var generateInterface = parseResult.GetValue(interfaceOption); - + var generateModelInterfaces = parseResult.GetValue(modelInterfacesOption); + try { - await GenerateClient(input, output, className, namespaceName, generateInterface); + await GenerateClient(input, output, className, namespaceName, generateInterface, generateModelInterfaces); if (watch) { Console.WriteLine($"👀 Watching {input} for changes..."); - + using FileSystemWatcher watcher = new FileSystemWatcher(Path.GetDirectoryName(Path.GetFullPath(input)) ?? ".", Path.GetFileName(input)); watcher.Changed += async (_, _) => { Console.WriteLine("🔄 File changed, regenerating..."); - await GenerateClient(input, output, className, namespaceName, generateInterface); + await GenerateClient(input, output, className, namespaceName, generateInterface, generateModelInterfaces); }; watcher.EnableRaisingEvents = true; @@ -81,7 +88,7 @@ return await rootCommand.Parse(args).InvokeAsync(); -static async Task GenerateClient(string input, string output, string className, string namespaceName, bool generateInterface) +static async Task GenerateClient(string input, string output, string className, string namespaceName, bool generateInterface, bool generateModelInterfaces) { Console.WriteLine("🚀 Generating C# API client..."); Console.WriteLine($"📥 Input: {input}"); @@ -93,12 +100,13 @@ static async Task GenerateClient(string input, string output, string className, Console.WriteLine($"🏗️ Generating code for {spec.Schemas.Count} models and {spec.Endpoints.Count} endpoints..."); CSharpClientGenerator generator = new(); - + ClientGeneratorOptions options = new() { ClassName = className, Namespace = namespaceName, - GenerateInterface = generateInterface + GenerateInterface = generateInterface, + GenerateModelInterfaces = generateModelInterfaces }; string clientCode = generator.GenerateClient(spec, options); diff --git a/README.md b/README.md index 6c7f69d..61f7a38 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,8 @@ dotnet-client-generator --input openapi.json --output ApiClient.cs --watch | `--output` | `-o` | Output file path for generated C# client | Yes | | `--class-name` | `-c` | Name of the generated client class | No (default: "ApiClient") | | `--namespace` | `-n` | Namespace for the generated client | No (default: "GeneratedClient") | +| `--generate-interface` | `-g` | Generate an interface for the client class | No | +| `--generate-model-interfaces` | `-m` | Generate interfaces for model classes | No | | `--watch` | `-w` | Watch input file for changes and regenerate | No | ## Generated Client Features @@ -77,6 +79,8 @@ The generated C# client includes: - **Query Parameters**: Automatic query string building - **Path Parameters**: Automatic URL path interpolation - **Request Bodies**: JSON serialization for POST/PUT operations +- **Interface Generation**: Optional generation of interfaces for dependency injection +- **Model Interfaces**: Optional generation of interfaces for model classes ## Example Generated Client @@ -131,6 +135,63 @@ var pets = await client.ListPets(limit: 10); await client.CreatePet(new { name = "Fluffy", tag = "cat" }); ``` +## Interface Generation + +### Client Interface + +Generate an interface for the client class using `--generate-interface`: + +```bash +dotnet-client-generator -i openapi.json -o ApiClient.cs --generate-interface +``` + +This generates: + +```csharp +public interface IApiClient +{ + Task GetUser(int userId); + // ... other methods +} + +public class ApiClient : IApiClient +{ + // ... implementation +} +``` + +### Model Interfaces + +Generate interfaces for model classes using `--generate-model-interfaces`: + +```bash +dotnet-client-generator -i openapi.json -o ApiClient.cs --generate-model-interfaces +``` + +This generates: + +```csharp +public interface IPet +{ + long Id { get; } + string Name { get; } + string? Tag { get; } +} + +public class Pet : IPet +{ + public long Id { get; set; } + public string Name { get; set; } + public string? Tag { get; set; } +} +``` + +Model interfaces are useful for: +- Dependency injection and mocking in tests +- Creating abstraction layers +- Enforcing read-only contracts +- Supporting polymorphism in domain models + ## Development ### Building