Created
January 23, 2025 20:23
-
-
Save panicoenlaxbox/1f7af48d6ceac3c3c0197303a7b248e6 to your computer and use it in GitHub Desktop.
OpenAPI Typescript generator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//< PackageReference Include = "Microsoft.OpenApi.Readers" Version = "1.6.23" /> | |
using Microsoft.OpenApi.Models; | |
using Microsoft.OpenApi.Readers; | |
using System.Net; | |
using System.Net.Mime; | |
using System.Text.RegularExpressions; | |
var document = await ReadOpenApiDocument(@"YOUR_SWAGGER_FILE"); | |
await GenerateModelsAsync(document); | |
var tags = GetTags(document); | |
foreach (var tag in tags) | |
{ | |
await GenerateServiceAsync(document, tag); | |
} | |
return; | |
IEnumerable<(string, string, OpenApiOperation)> GetOperationsByTag(OpenApiDocument document, string tag) | |
{ | |
var operations = new List<(string, string, OpenApiOperation)>(); | |
foreach (var path in document.Paths) | |
{ | |
operations.AddRange(path.Value.Operations | |
.Where(op => op.Value.Tags.Any(t => t.Name.Equals(tag, StringComparison.OrdinalIgnoreCase))) | |
.Select(op => (path.Key, op.Key.ToString(), op.Value)) | |
.ToList()); | |
} | |
return operations; | |
} | |
async Task<string> GenerateServiceAsync(OpenApiDocument document, string tag) | |
{ | |
var filePath = $"{CamelCaseToKebabCase(tag)}.service.ts"; | |
var code = $@" | |
// {filePath} | |
export class {tag}Service {{ | |
constructor(@Inject(API_BASE_URL) private baseUrl: string, private httpClient: HttpClient) {{ | |
}} | |
"; | |
var operations = GetOperationsByTag(document, tag); | |
foreach (var (path, verb, operation) in operations) | |
{ | |
var queryString = operation.Parameters | |
.Where(p => p.In == ParameterLocation.Query) | |
.Select(p => new { p.Name, Type = OpenApiTypeToTypescriptType(p.Schema.Type), p.Required }) | |
.ToList(); | |
var parameters = string.Join(", ", | |
queryString.Select(qs => $"{qs.Name}{(!qs.Required ? "?" : string.Empty)}: {qs.Type}")); | |
code += $@" | |
{PascalCaseToCamelCase(operation.OperationId)}({parameters}) {{"; | |
if (queryString.Any()) | |
{ | |
code += $@" | |
let params = new HttpParams(); | |
"; | |
foreach (var parameter in queryString) | |
{ | |
if (!parameter.Required) | |
{ | |
code += $@" | |
if ({parameter.Name}{(parameter.Type != "string" ? " !== undefined" : string.Empty)}) {{"; | |
} | |
code += $@" | |
params = params.set('{parameter.Name}', {parameter.Name}{(parameter.Type != "string" ? ".toString()" : string.Empty)}); | |
}}"; | |
} | |
code += Environment.NewLine; | |
} | |
var response = ""; | |
if (operation.Responses.TryGetValue(((int)HttpStatusCode.OK).ToString(), out var okResponse)) | |
{ | |
if (okResponse.Content.TryGetValue(MediaTypeNames.Application.Json, out var mediaType)) | |
{ | |
if (mediaType.Schema.Type == "array" && mediaType.Schema.Items.Reference != null) | |
{ | |
response = $"{mediaType.Schema.Items.Reference.Id}[]"; | |
} | |
else if (mediaType.Schema.Reference != null) | |
{ | |
response = mediaType.Schema.Reference.Id; | |
} | |
} | |
} | |
code += $@" | |
return this.httpClient.{verb.ToLower()}{(!string.IsNullOrWhiteSpace(response) ? $"<{response}>" : string.Empty)}(`${{this.baseUrl}}/todos`{(queryString.Any() ? ", { params }" : string.Empty)}); | |
}} | |
"; | |
} | |
code += @" | |
}"; | |
code = Dedent(code).TrimStart(); | |
Console.WriteLine(code); | |
await File.WriteAllTextAsync(filePath, code, System.Text.Encoding.UTF8); | |
return filePath; | |
} | |
static string PascalCaseToCamelCase(string text) | |
{ | |
return char.ToLowerInvariant(text[0]) + text[1..]; | |
} | |
IEnumerable<string> GetTags(OpenApiDocument document) | |
{ | |
var tags = new HashSet<string>(); | |
foreach (var path in document.Paths.Values) | |
{ | |
foreach (var operation in path.Operations.Values) | |
{ | |
foreach (var tag in operation.Tags) | |
{ | |
tags.Add(tag.Name); | |
} | |
} | |
} | |
return tags; | |
} | |
static async Task GenerateModelsAsync(OpenApiDocument document) | |
{ | |
foreach (var (schemaName, schema) in document.Components.Schemas) | |
{ | |
await GenerateModelAsync(schemaName, schema); | |
} | |
} | |
static string Dedent(string text) | |
{ | |
if (string.IsNullOrWhiteSpace(text)) | |
{ | |
return text; | |
} | |
var lines = text.Split(Environment.NewLine); | |
var minIndent = lines | |
.Where(line => !string.IsNullOrWhiteSpace(line)) | |
.Select(line => line.TakeWhile(char.IsWhiteSpace).Count()) | |
.DefaultIfEmpty(0) | |
.Min(); | |
return string.Join(Environment.NewLine, lines.Select(line => line.Length >= minIndent ? line[minIndent..] : line)); | |
} | |
static async Task<string> GenerateModelAsync(string name, OpenApiSchema schema) | |
{ | |
var path = $"{CamelCaseToKebabCase(name)}.ts"; | |
var code = $@" | |
// {path} | |
export interface {name} {{"; | |
foreach (var (propertyName, property) in schema.Properties) | |
{ | |
code += $@" | |
{propertyName}: {OpenApiTypeToTypescriptType(property.Type)};"; | |
} | |
code += @" | |
}"; | |
code = Dedent(code).TrimStart(); | |
Console.WriteLine(code); | |
await File.WriteAllTextAsync(path, code, System.Text.Encoding.UTF8); | |
return path; | |
} | |
static string OpenApiTypeToTypescriptType(string openApiType) => openApiType switch | |
{ | |
"integer" => "number", | |
_ => openApiType, | |
}; | |
static void WriteDiagnostics(OpenApiDiagnostic diagnostic) | |
{ | |
if (diagnostic.Warnings.Any()) | |
{ | |
Console.WriteLine(diagnostic.Warnings); | |
} | |
if (diagnostic.Errors.Any()) | |
{ | |
Console.WriteLine(diagnostic.Errors); | |
} | |
} | |
static async Task<OpenApiDocument> ReadOpenApiDocument(string path) | |
{ | |
var input = await File.ReadAllTextAsync(path); | |
var settings = new OpenApiReaderSettings(); | |
var reader = new OpenApiStringReader(settings); | |
var document = reader.Read(input, out var diagnostic); | |
WriteDiagnostics(diagnostic); | |
return document; | |
} | |
static string CamelCaseToKebabCase(string text) | |
{ | |
if (string.IsNullOrWhiteSpace(text)) | |
{ | |
return text; | |
} | |
text = text.Trim(); | |
return Regex.Replace(text, "(?<!^)([A-Z])", "-$1").ToLower(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment