Skip to content

Instantly share code, notes, and snippets.

@panicoenlaxbox
Created January 23, 2025 20:23
Show Gist options
  • Save panicoenlaxbox/1f7af48d6ceac3c3c0197303a7b248e6 to your computer and use it in GitHub Desktop.
Save panicoenlaxbox/1f7af48d6ceac3c3c0197303a7b248e6 to your computer and use it in GitHub Desktop.
OpenAPI Typescript generator
//< 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