Skip to content

Commit

Permalink
Fixes for sub entity & inline enums in openapi generator
Browse files Browse the repository at this point in the history
  • Loading branch information
aritchie committed Sep 24, 2024
1 parent a8ba8ae commit 11858f3
Show file tree
Hide file tree
Showing 4 changed files with 183 additions and 70 deletions.
2 changes: 2 additions & 0 deletions samples/Sample/Contracts/MyMessageContracts.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Text.Json.Serialization;

namespace Sample.Contracts;

public record MyMessageRequest(string Arg, bool FireAndForgetEvents) : IRequest<MyMessageResponse>;
Expand Down
18 changes: 18 additions & 0 deletions src/Shiny.Mediator.SourceGenerators/Extensions.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Linq;
using Microsoft.CodeAnalysis;

Expand All @@ -9,10 +10,27 @@ static class Extensions

public static string Pascalize(this string str)
{
if (str.All(x => char.IsUpper(x) || !char.IsLetter(x)))
{
if (str.Contains("_"))
{
var pascal = str.ToLower()
.Split(["_"], StringSplitOptions.RemoveEmptyEntries)
.Select(s => char.ToUpperInvariant(s[0]) + s.Substring(1, s.Length - 1))
.Aggregate(string.Empty, (s1, s2) => s1 + s2);

return pascal;
}

var result = char.ToUpper(str[0]) + str.Substring(1).ToLower();
return result;
}

if (char.IsUpper(str[0]))
return str;

var r = char.ToUpper(str[0]) + str.Substring(1);
r = r.Replace("_", "");
return r;
}

Expand Down
230 changes: 161 additions & 69 deletions src/Shiny.Mediator.SourceGenerators/Http/OpenApiContractGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,14 @@ namespace Shiny.Mediator.SourceGenerators.Http;

public class OpenApiContractGenerator(MediatorHttpItemConfig itemConfig, Action<string> output)

Check warning on line 13 in src/Shiny.Mediator.SourceGenerators/Http/OpenApiContractGenerator.cs

View workflow job for this annotation

GitHub Actions / build

Missing XML comment for publicly visible type or member 'OpenApiContractGenerator'

Check warning on line 13 in src/Shiny.Mediator.SourceGenerators/Http/OpenApiContractGenerator.cs

View workflow job for this annotation

GitHub Actions / build

Missing XML comment for publicly visible type or member 'OpenApiContractGenerator.OpenApiContractGenerator(MediatorHttpItemConfig, Action<string>)'
{

readonly StringBuilder typeBuilder = new();
readonly StringBuilder contractBuilder = new();

public string Generate(Stream stream)

Check warning on line 18 in src/Shiny.Mediator.SourceGenerators/Http/OpenApiContractGenerator.cs

View workflow job for this annotation

GitHub Actions / build

Missing XML comment for publicly visible type or member 'OpenApiContractGenerator.Generate(Stream)'
{
this.typeBuilder.Clear();
this.contractBuilder.Clear();

using var streamReader = new StreamReader(stream);
var reader = new OpenApiStreamReader();
var document = reader.Read(streamReader.BaseStream, out var diagnostic);
Expand All @@ -28,36 +33,32 @@ public string Generate(Stream stream)
}

var sb = new StringBuilder();
sb.AppendLine("/// <summary>");
sb.AppendLine("/// This file is generated by Shiny.Mediator source generation");
sb.AppendLine("/// Do not modify this file directly");
sb.AppendLine("/// </summary>");
sb.AppendLine($"namespace {itemConfig.Namespace};");
sb.AppendLine();
this.typeBuilder.AppendLine("/// <summary>");
this.typeBuilder.AppendLine("/// This file is generated by Shiny.Mediator source generation");
this.typeBuilder.AppendLine("/// Do not modify this file directly");
this.typeBuilder.AppendLine("/// </summary>");
this.typeBuilder.AppendLine($"namespace {itemConfig.Namespace};");
this.typeBuilder.AppendLine();

GenerateComponents(sb, document, itemConfig, output);
GenerateContracts(sb, document, itemConfig, output);
GenerateComponents(document);
GenerateContracts(document);

return sb.ToString();
var result = this.typeBuilder.ToString() + this.contractBuilder.ToString();
return result;
}


void GenerateComponents(StringBuilder sb, OpenApiDocument document, MediatorHttpItemConfig itemConfig, Action<string> output)
void GenerateComponents(OpenApiDocument document)
{
output("Generating Components for " + itemConfig.Namespace);
foreach (var schema in document.Components.Schemas)
{
var type = GenerateComplexType(schema, output);
if (type == null)
throw new InvalidOperationException("Invalid Schema Type");

sb.Append(type);
sb.AppendLine();
this.GenerateComplexType(schema);
}
}


void GenerateContracts(StringBuilder sb, OpenApiDocument document, MediatorHttpItemConfig itemConfig, Action<string> output)
void GenerateContracts(OpenApiDocument document)
{
foreach (var path in document.Paths)
{
Expand All @@ -77,18 +78,18 @@ void GenerateContracts(StringBuilder sb, OpenApiDocument document, MediatorHttpI
var contractName = $"{itemConfig.ContractPrefix}{op.Value.OperationId.Pascalize()}{itemConfig.ContractPostfix}";
var httpMethod = op.Key.ToString();

sb.AppendLine($"[global::Shiny.Mediator.Http.HttpAttribute(global::Shiny.Mediator.Http.HttpVerb.{httpMethod}, \"{path.Key}\")]");
sb.AppendLine($"public partial class {contractName} : global::Shiny.Mediator.Http.IHttpRequest<{responseType}>");
sb.AppendLine("{");
this.contractBuilder.AppendLine($"[global::Shiny.Mediator.Http.HttpAttribute(global::Shiny.Mediator.Http.HttpVerb.{httpMethod}, \"{path.Key}\")]");
this.contractBuilder.AppendLine($"public partial class {contractName} : global::Shiny.Mediator.Http.IHttpRequest<{responseType}>");
this.contractBuilder.AppendLine("{");

foreach (var parameter in op.Value.Parameters)
{
var argType = parameter.In == ParameterLocation.Path ? "Path" : "Query";
var typeName = this.GetSchemaType(parameter.Schema);
var propertyName = parameter.Name.Pascalize();
sb.AppendLine($" [global::Shiny.Mediator.Http.HttpParameter(global::Shiny.Mediator.Http.HttpParameterType.{argType})]");
sb.AppendLine($" public {typeName} {propertyName} {{ get; set; }}");
sb.AppendLine();
this.contractBuilder.AppendLine($" [global::Shiny.Mediator.Http.HttpParameter(global::Shiny.Mediator.Http.HttpParameterType.{argType}, \"{parameter.Name}\")]");
this.contractBuilder.AppendLine($" public {typeName} {propertyName} {{ get; set; }}");
this.contractBuilder.AppendLine();
output($"PROPERTY: {propertyName} ({typeName} - {argType})");
}

Expand All @@ -101,13 +102,13 @@ void GenerateContracts(StringBuilder sb, OpenApiDocument document, MediatorHttpI
if (!body.Required)
bodyResponseType += "?";

sb.AppendLine($" [global::Shiny.Mediator.Http.HttpParameter(global::Shiny.Mediator.Http.HttpParameterType.Body)]");
sb.AppendLine($" public {bodyResponseType} Body {{ get; set; }}");
this.contractBuilder.AppendLine($" [global::Shiny.Mediator.Http.HttpParameter(global::Shiny.Mediator.Http.HttpParameterType.Body)]");
this.contractBuilder.AppendLine($" public {bodyResponseType} Body {{ get; set; }}");
output("BODY: " + bodyResponseType);
}
}
sb.AppendLine("}");
sb.AppendLine();
this.contractBuilder.AppendLine("}");
this.contractBuilder.AppendLine();
}
}
}
Expand Down Expand Up @@ -145,10 +146,17 @@ string GetResponseType(OpenApiOperation op)
switch (schema.Type)
{
case "string":
if (schema.Enum.Count > 0)
type = $"global::{itemConfig.Namespace}.{schema.Reference.Id}";
else
if (schema.Enum.Count == 0)
{
type = GetStringType(schema);
}
else
{
if (schema.Reference == null)
throw new InvalidOperationException("Enum reference is null");

type = $"global::{itemConfig.Namespace}.{schema.Reference.Id}";
}
break;

case "integer":
Expand Down Expand Up @@ -212,70 +220,154 @@ string GetResponseType(OpenApiOperation op)
_ => "string"
};


static string GetNumberType(string format) => format switch
{
"int32" => "int",
"int64" => "long",
"float" => "float",
"double" => "double",
null => "int",
_ => throw new InvalidOperationException("Invalid Number Format - " + format)
};

string GenerateComplexType(KeyValuePair<string, OpenApiSchema> schema, Action<string> output)

void GenerateComplexType(KeyValuePair<string, OpenApiSchema> schema)
{
output("COMPONENT: " + schema.Key);
var sb = new StringBuilder();
var className = schema.Key.Pascalize();

if (schema.Value.Enum.Count > 0)
{
output("ENUM COMPONENT GENERATING");
sb.AppendLine($"public enum {className}");
sb.AppendLine("{");
foreach (var ev in schema.Value.Enum.OfType<OpenApiString>())
{
sb.AppendLine($" {ev.Value},");
}
sb.AppendLine("}");
var add = this.GenerateEnum(schema.Value, className);
this.typeBuilder.AppendLine(add);
}
else
{
output("GENERATING CLASS COMPONENT");
sb.Append("public partial class " + className);
var add = this.GenerateObject(schema.Value, className);
this.typeBuilder.AppendLine(add);
}
}

// TODO: this will be 2 when discriminators are present
//if (schema.Value.AllOf.Count > 1)
//{
// Debugger.Launch();
//}

if (schema.Value.AllOf.Count == 1)
{
// add inheritance
var baseType = this.GetSchemaType(schema.Value.AllOf.Single());
sb.AppendLine($" : {baseType}");
string GenerateEnum(OpenApiSchema schema, string enumName)
{
// https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/customize-properties?pivots=dotnet-8-0
//[JsonConverter(typeof(JsonStringEnumConverter<TYPE>))]
output("ENUM COMPONENT GENERATING - " + enumName);

output($"INHERITED: {className} ({baseType})");
}
sb.AppendLine();
sb.AppendLine("{");
var sb = new StringBuilder();
sb.AppendLine($"public enum {enumName}");
sb.AppendLine("{");

foreach (var ev in schema.Enum.OfType<OpenApiString>())
{
// TODO: pascal case the value
output("ENUM VALUE: " + ev.Value);
sb.AppendLine($" {ev.Value},");
}
sb.AppendLine("}");
output("DONE ENUM COMPONENT GENERATING - " + enumName);

return sb.ToString();
}


string GenerateObject(OpenApiSchema schema, string className)
{
output("===GENERATING CLASS COMPONENT: " + className);
var sb = new StringBuilder();
sb.Append("public partial class " + className);

// TODO: this will be 2 when discriminators are present
//if (schema.Value.AllOf.Count > 1)
//{
// Debugger.Launch();
//}

if (schema.AllOf.Count == 1)
{
// add inheritance
var baseType = this.GetSchemaType(schema.AllOf.Single());
sb.AppendLine($" : {baseType}");

output($"INHERITED: {className} ({baseType})");
}
sb.AppendLine();
sb.AppendLine("{");

foreach (var prop in schema.Properties)
{
var propertyName = prop.Key.Pascalize();
string? typeName = null;

foreach (var prop in schema.Value.Properties)
if (prop.Value.Type == "object" && prop.Value.Properties != null && prop.Value.Reference == null)
{
var propertyName = prop.Key.Pascalize();
if (prop.Value != null)
{
var typeName = this.GetSchemaType(prop.Value);
sb.AppendLine($" [System.Text.Json.Serialization.JsonPropertyName(\"{prop.Key}\")]");
sb.AppendLine( " public " + typeName + " " + propertyName + " { get; set; }");
sb.AppendLine();

output($"PROPERTY: {propertyName} ({typeName})");
}
typeName = className + propertyName.Pascalize();
var add = this.GenerateObject(prop.Value, typeName);
this.typeBuilder.AppendLine(add);
}
else if (prop.Value.Type == "string" && prop.Value.Enum.Count > 0 && prop.Value.Reference == null)
{
typeName = className + propertyName.Pascalize();
var add = this.GenerateEnum(prop.Value, typeName);
this.typeBuilder.AppendLine(add);
}
else
{
typeName = this.GetSchemaType(prop.Value);
}

// TODO: null when OneOf setup
if (typeName != null)
{
// throw new InvalidOperationException("TypeName is null");
sb.AppendLine($" [System.Text.Json.Serialization.JsonPropertyName(\"{prop.Key}\")]");
sb.AppendLine(" public " + typeName + " " + propertyName + " { get; set; }");
sb.AppendLine();
output($"PROPERTY: {propertyName} ({typeName})");
}
sb.AppendLine("}");
}

sb.AppendLine("}");

output("===DONE GENERATING CLASS COMPONENT: " + className);
return sb.ToString();
}
}


// TODO: properties within properties
/*
properties:
STANDBY:
type: object
properties:
waitTime:
type: number
SINGLE_RIDER:
type: object
properties:
waitTime:
type: number
nullable: true
required:
- waitTime
RETURN_TIME:
type: object
properties:
state:
$ref: '#/components/schemas/ReturnTimeState'
returnStart:
type: string
format: date-time
nullable: true
returnEnd:
type: string
format: date-time
nullable: true
required:
- state
- returnStart
- returnEnd
*/
3 changes: 2 additions & 1 deletion tests/Shiny.Mediator.Tests/Http/HttpRequestGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ public void Tests(string path, string nameSpace)
output.WriteLine(code);
}


[Theory]
[InlineData("", "TestApi")]
[InlineData("https://api.themeparks.wiki/docs/v1.yaml", "ThemeParksApi")]
public async Task RemoteTests(string uri, string nameSpace)
{
var http = new HttpClient();
Expand Down

0 comments on commit 11858f3

Please sign in to comment.