295 lines
8.7 KiB
C#
295 lines
8.7 KiB
C#
using System.Collections;
|
|
using System.Reflection;
|
|
using EonaCat.DoxaApi.Models;
|
|
|
|
namespace EonaCat.DoxaApi.Generation
|
|
{
|
|
public sealed class SchemaBuilder
|
|
{
|
|
private readonly Dictionary<string, SchemaModel> _registry;
|
|
private readonly HashSet<Type> _inProgress = new();
|
|
|
|
public SchemaBuilder(Dictionary<string, SchemaModel> registry)
|
|
{
|
|
_registry = registry;
|
|
}
|
|
|
|
public SchemaModel Build(Type type)
|
|
{
|
|
|
|
type = UnwrapAsyncAndActionResult(type);
|
|
|
|
var underlying = Nullable.GetUnderlyingType(type);
|
|
bool nullable = underlying is not null;
|
|
if (underlying is not null)
|
|
{
|
|
type = underlying;
|
|
}
|
|
|
|
var schema = BuildCore(type);
|
|
schema.Nullable = nullable || schema.Nullable;
|
|
return schema;
|
|
}
|
|
|
|
private static Type UnwrapAsyncAndActionResult(Type type)
|
|
{
|
|
if (type == typeof(void))
|
|
{
|
|
return type;
|
|
}
|
|
|
|
if (type.IsGenericType)
|
|
{
|
|
var def = type.GetGenericTypeDefinition();
|
|
if (def == typeof(Task<>) || def.Name.StartsWith("ValueTask`"))
|
|
{
|
|
return UnwrapAsyncAndActionResult(type.GetGenericArguments()[0]);
|
|
}
|
|
|
|
if (def.Name is "ActionResult`1")
|
|
{
|
|
return UnwrapAsyncAndActionResult(type.GetGenericArguments()[0]);
|
|
}
|
|
}
|
|
|
|
if (type == typeof(Task))
|
|
{
|
|
return typeof(void);
|
|
}
|
|
|
|
return type;
|
|
}
|
|
|
|
private SchemaModel BuildCore(Type type)
|
|
{
|
|
if (type == typeof(void))
|
|
{
|
|
return new SchemaModel { Type = "void" };
|
|
}
|
|
|
|
if (type == typeof(string) || type == typeof(char))
|
|
{
|
|
return new SchemaModel { Type = "string" };
|
|
}
|
|
|
|
if (type == typeof(bool))
|
|
{
|
|
return new SchemaModel { Type = "boolean" };
|
|
}
|
|
|
|
if (type == typeof(byte) || type == typeof(sbyte) || type == typeof(short) ||
|
|
type == typeof(ushort) || type == typeof(int) || type == typeof(uint) ||
|
|
type == typeof(long) || type == typeof(ulong))
|
|
{
|
|
return new SchemaModel { Type = "integer", Format = type.Name.ToLowerInvariant() };
|
|
}
|
|
|
|
if (type == typeof(float) || type == typeof(double) || type == typeof(decimal))
|
|
{
|
|
return new SchemaModel { Type = "number", Format = type.Name.ToLowerInvariant() };
|
|
}
|
|
|
|
if (type == typeof(DateTime) || type == typeof(DateTimeOffset))
|
|
{
|
|
return new SchemaModel { Type = "string", Format = "date-time" };
|
|
}
|
|
|
|
if (type == typeof(DateOnly))
|
|
{
|
|
return new SchemaModel { Type = "string", Format = "date" };
|
|
}
|
|
|
|
if (type == typeof(TimeOnly) || type == typeof(TimeSpan))
|
|
{
|
|
return new SchemaModel { Type = "string", Format = "time" };
|
|
}
|
|
|
|
if (type == typeof(Guid))
|
|
{
|
|
return new SchemaModel { Type = "string", Format = "uuid" };
|
|
}
|
|
|
|
if (type == typeof(Uri))
|
|
{
|
|
return new SchemaModel { Type = "string", Format = "uri" };
|
|
}
|
|
|
|
if (type == typeof(object))
|
|
{
|
|
return new SchemaModel { Type = "object" };
|
|
}
|
|
|
|
if (type.IsEnum)
|
|
{
|
|
return new SchemaModel
|
|
{
|
|
Type = "enum",
|
|
EnumValues = Enum.GetNames(type).ToList()
|
|
};
|
|
}
|
|
|
|
if (IsDictionary(type, out var valueType))
|
|
{
|
|
return new SchemaModel
|
|
{
|
|
Type = "object",
|
|
Items = Build(valueType!)
|
|
};
|
|
}
|
|
|
|
var elementType = GetEnumerableElementType(type);
|
|
if (elementType is not null)
|
|
{
|
|
return new SchemaModel
|
|
{
|
|
Type = "array",
|
|
Items = Build(elementType)
|
|
};
|
|
}
|
|
|
|
var refName = FriendlyTypeName(type);
|
|
|
|
if (_registry.ContainsKey(refName))
|
|
{
|
|
return new SchemaModel { Type = "object", RefName = refName };
|
|
}
|
|
|
|
if (_inProgress.Contains(type))
|
|
{
|
|
return new SchemaModel { Type = "object", RefName = refName };
|
|
}
|
|
|
|
_inProgress.Add(type);
|
|
|
|
var props = new Dictionary<string, SchemaModel>();
|
|
var required = new List<string>();
|
|
|
|
foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
|
|
{
|
|
if (prop.GetIndexParameters().Length > 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!prop.CanRead)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var propSchema = Build(prop.PropertyType);
|
|
props[ToCamelCase(prop.Name)] = propSchema;
|
|
|
|
if (!propSchema.Nullable && Nullable.GetUnderlyingType(prop.PropertyType) is null
|
|
&& (prop.PropertyType.IsValueType || IsNonNullableReferenceProperty(prop)))
|
|
{
|
|
required.Add(ToCamelCase(prop.Name));
|
|
}
|
|
}
|
|
|
|
_registry[refName] = new SchemaModel
|
|
{
|
|
Type = "object",
|
|
Properties = props,
|
|
Required = required.Count > 0 ? required : null
|
|
};
|
|
|
|
_inProgress.Remove(type);
|
|
|
|
return new SchemaModel { Type = "object", RefName = refName };
|
|
}
|
|
|
|
private static bool IsNonNullableReferenceProperty(PropertyInfo prop)
|
|
{
|
|
|
|
try
|
|
{
|
|
var ctx = new NullabilityInfoContext();
|
|
var info = ctx.Create(prop);
|
|
return info.ReadState == NullabilityState.NotNull;
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static Type? GetEnumerableElementType(Type type)
|
|
{
|
|
if (type == typeof(string))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (type.IsArray)
|
|
{
|
|
return type.GetElementType();
|
|
}
|
|
|
|
if (type.IsGenericType)
|
|
{
|
|
var def = type.GetGenericTypeDefinition();
|
|
if (def == typeof(List<>) || def == typeof(IList<>) || def == typeof(ICollection<>) ||
|
|
def == typeof(IEnumerable<>) || def == typeof(IReadOnlyList<>) || def == typeof(IReadOnlyCollection<>) ||
|
|
def == typeof(HashSet<>) || def == typeof(Queue<>) || def == typeof(Stack<>))
|
|
{
|
|
return type.GetGenericArguments()[0];
|
|
}
|
|
}
|
|
|
|
foreach (var iface in type.GetInterfaces())
|
|
{
|
|
if (iface.IsGenericType && iface.GetGenericTypeDefinition() == typeof(IEnumerable<>))
|
|
{
|
|
return iface.GetGenericArguments()[0];
|
|
}
|
|
}
|
|
|
|
if (typeof(IEnumerable).IsAssignableFrom(type) && type != typeof(string))
|
|
{
|
|
return typeof(object);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static bool IsDictionary(Type type, out Type? valueType)
|
|
{
|
|
valueType = null;
|
|
if (!type.IsGenericType)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var def = type.GetGenericTypeDefinition();
|
|
if (def == typeof(Dictionary<,>) || def == typeof(IDictionary<,>) || def == typeof(IReadOnlyDictionary<,>))
|
|
{
|
|
valueType = type.GetGenericArguments()[1];
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private static string FriendlyTypeName(Type type)
|
|
{
|
|
if (!type.IsGenericType)
|
|
{
|
|
return type.Name;
|
|
}
|
|
|
|
var name = type.Name[..type.Name.IndexOf('`')];
|
|
var args = string.Join(",", type.GetGenericArguments().Select(FriendlyTypeName));
|
|
return $"{name}<{args}>";
|
|
}
|
|
|
|
private static string ToCamelCase(string name)
|
|
{
|
|
if (string.IsNullOrEmpty(name) || char.IsLower(name[0]))
|
|
{
|
|
return name;
|
|
}
|
|
|
|
return char.ToLowerInvariant(name[0]) + name[1..];
|
|
}
|
|
}
|
|
}
|