Skip to content

Commit

Permalink
Merge pull request #99 from BigDaddy1337/feature/CSHARP-197-enums
Browse files Browse the repository at this point in the history
OpenRpc AutoDoc improvements
  • Loading branch information
BigDaddy1337 authored Mar 27, 2024
2 parents 11a2b14 + 88b9c1f commit de80bb7
Show file tree
Hide file tree
Showing 2 changed files with 309 additions and 49 deletions.
145 changes: 98 additions & 47 deletions src/Tochka.JsonRpc.OpenRpc/Services/OpenRpcSchemaGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,79 +15,130 @@ public class OpenRpcSchemaGenerator : IOpenRpcSchemaGenerator
private readonly Dictionary<string, JsonSchema> registeredSchemas = new();
private readonly HashSet<string> registeredSchemaKeys = new();

private readonly Dictionary<Type, Format> defaultStringConvertedSimpleTypes = new()
{
{ typeof(DateTime), Formats.DateTime },
{ typeof(DateTimeOffset), Formats.DateTime },
{ typeof(DateOnly), Formats.Date },
{ typeof(TimeOnly), Formats.Time },
{ typeof(TimeSpan), Formats.Duration },
{ typeof(Guid), Formats.Uuid }
};

/// <inheritdoc />
public Dictionary<string, JsonSchema> GetAllSchemas() => new(registeredSchemas);

/// <inheritdoc />
public JsonSchema CreateOrRef(Type type, string methodName, JsonSerializerOptions jsonSerializerOptions)
public JsonSchema CreateOrRef(Type type, string methodName, JsonSerializerOptions jsonSerializerOptions) =>
CreateOrRefInternal(type, methodName, null, jsonSerializerOptions);

private JsonSchema CreateOrRefInternal(Type type, string methodName, string? propertySummary, JsonSerializerOptions jsonSerializerOptions)
{
var itemType = type.GetEnumerableItemType();
if (typeof(IEnumerable).IsAssignableFrom(type) && itemType != null)
{
// returning schema itself if it's collection
return new JsonSchemaBuilder()
.Type(SchemaValueType.Array)
.Items(CreateOrRef(itemType, methodName, jsonSerializerOptions))
.Build();
}

var typeName = type.Name;
if (!typeName.StartsWith($"{methodName} ", StringComparison.Ordinal))
// Unwrap nullable type
var clearType = Nullable.GetUnderlyingType(type) ?? type;

var clearTypeName = clearType.Name;
if (!clearTypeName.StartsWith($"{methodName} ", StringComparison.Ordinal))
{
// adding method name in case it uses not default serializer settings
typeName = $"{methodName} {typeName}";
clearTypeName = $"{methodName} {clearTypeName}";
}

if (!registeredSchemas.ContainsKey(typeName) && !registeredSchemaKeys.Contains(typeName))
{
var schema = BuildSchema(type, typeName, methodName, jsonSerializerOptions);
if (schema != null)
{
// returning schema itself if it's simple type
return schema;
}
}

// returning ref if it's enum or regular type with properties
return new JsonSchemaBuilder()
.Ref($"#/components/schemas/{typeName}")
.Build();
return BuildSchema(clearType, clearTypeName, methodName, propertySummary, jsonSerializerOptions);
}

private JsonSchema? BuildSchema(Type type, string typeName, string methodName, JsonSerializerOptions jsonSerializerOptions)
private JsonSchema BuildSchema(Type type, string typeName, string methodName, string? propertySummary, JsonSerializerOptions jsonSerializerOptions)
{
if (registeredSchemas.ContainsKey(typeName) || registeredSchemaKeys.Contains(typeName))
{
return CreateRefSchema(typeName, propertySummary);
}

var itemType = type.GetEnumerableItemType();
if (typeof(IEnumerable).IsAssignableFrom(type) && itemType != null)
{
var collectionScheme = new JsonSchemaBuilder()
.Type(SchemaValueType.Array)
.Items(CreateOrRefInternal(itemType, methodName, null, jsonSerializerOptions))
.TryAppendTitle(propertySummary)
.Build();
// returning schema itself if it's collection
return collectionScheme;
}

if (type.IsEnum)
{
// adding it just for the same logic as for normal types
registeredSchemaKeys.Add(typeName);
registeredSchemas[typeName] = new JsonSchemaBuilder()
.Enum(type.GetEnumNames().Select(jsonSerializerOptions.ConvertName))
.Build();
return null;
var enumSchema = new JsonSchemaBuilder()
.Enum(type.GetEnumNames().Select(jsonSerializerOptions.ConvertName))
.Build();
RegisterSchema(typeName, enumSchema);
// returning ref if it's enum or regular type with properties
return CreateRefSchema(typeName, propertySummary);
}

var simpleTypeSchema = new JsonSchemaBuilder()
.FromType(type)
.TryAppendTitle(propertySummary)
.Build();
// can't check type.GetProperties() here because simple types have properties too
var schema = new JsonSchemaBuilder()
.FromType(type)
.Build();
if (schema.GetProperties() == null)
if (simpleTypeSchema.GetProperties() == null)
{
// returning schema itself if it's simple type
// string, int, bool, etc...
return schema;
return simpleTypeSchema;
}

if (defaultStringConvertedSimpleTypes.TryGetValue(type, out var format))
{
var simpleStringSchema = new JsonSchemaBuilder()
.Type(SchemaValueType.String)
.Format(format)
.TryAppendTitle(propertySummary)
.Build();
return simpleStringSchema;
}

// required to break infinite recursion
// required to break infinite recursion by ref to same type in property
registeredSchemaKeys.Add(typeName);
registeredSchemas[typeName] = new JsonSchemaBuilder()
.Type(SchemaValueType.Object)
.Properties(BuildPropertiesSchemas(type, methodName, jsonSerializerOptions))
.Build();
return null;

var objectSchema = new JsonSchemaBuilder()
.Type(SchemaValueType.Object)
.Properties(BuildPropertiesSchemas(type, methodName, jsonSerializerOptions))
.Build();
RegisterSchema(typeName, objectSchema);
return CreateRefSchema(typeName, propertySummary);
}

private static JsonSchema CreateRefSchema(string typeName, string? propertySummary)
{
var refSchemaBuilder = new JsonSchemaBuilder()
.Ref($"#/components/schemas/{typeName}")
.TryAppendTitle(propertySummary);

return refSchemaBuilder.Build();
}

private void RegisterSchema(string key, JsonSchema schema)
{
registeredSchemaKeys.Add(key);
registeredSchemas[key] = schema;
}

private Dictionary<string, JsonSchema> BuildPropertiesSchemas(Type type, string methodName, JsonSerializerOptions jsonSerializerOptions) =>
type
.GetProperties()
.ToDictionary(p => jsonSerializerOptions.ConvertName(p.Name),
p => CreateOrRef(p.PropertyType, methodName, jsonSerializerOptions));
p => CreateOrRefInternal(p.PropertyType, methodName, p.GetXmlDocsSummary(), jsonSerializerOptions));
}

internal static class JsonSchemaBuilderExtensions
{
public static JsonSchemaBuilder TryAppendTitle(this JsonSchemaBuilder builder, string? propertySummary)
{
if (propertySummary is { Length: > 0 })
{
builder.Title(propertySummary);
}
return builder;
}
}
Loading

0 comments on commit de80bb7

Please sign in to comment.