Files
KinginfoGateway/framework/Foundation/ThingsGateway.Foundation/TouchSocket/WebApi.Swagger/Plugins/SwaggerPlugin.cs
2023-10-25 00:53:17 +08:00

701 lines
24 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#region copyright
//------------------------------------------------------------------------------
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
// 此代码版权除特别声明外的代码归作者本人Diego所有
// 源代码使用协议遵循本仓库的开源协议及附加协议
// Gitee源代码仓库https://gitee.com/diego2098/ThingsGateway
// Github源代码仓库https://github.com/kimdiego2098/ThingsGateway
// 使用文档https://diego2098.gitee.io/thingsgateway-docs/
// QQ群605534569
//------------------------------------------------------------------------------
#endregion
using Newtonsoft.Json;
using System.Collections;
using System.Diagnostics;
using System.Reflection;
using System.Text;
namespace ThingsGateway.Foundation.WebApi.Swagger
{
/// <summary>
/// SwaggerPlugin
/// </summary>
public sealed class SwaggerPlugin : PluginBase, IServerStartedPlugin
{
private readonly IContainer m_container;
private readonly ILog m_logger;
private readonly Dictionary<string, byte[]> m_swagger = new Dictionary<string, byte[]>();
/// <summary>
/// SwaggerPlugin
/// </summary>
public SwaggerPlugin(IContainer container, ILog logger)
{
this.m_container = container;
this.m_logger = logger;
}
/// <summary>
/// 是否在浏览器打开Swagger页面
/// </summary>
public bool LaunchBrowser { get; set; }
/// <summary>
/// 访问Swagger的前缀默认“swagger”
/// </summary>
public string Prefix { get; set; } = "swagger";
/// <inheritdoc/>
public async Task OnServerStarted(IService sender, ServiceStateEventArgs e)
{
if (e.ServerState != ServerState.Running)
{
return;
}
if (!this.m_container.IsRegistered(typeof(WebApiParserPlugin)))
{
this.m_logger.Warning($"该服务器中似乎没有添加{nameof(WebApiParserPlugin)}插件WebApi插件。");
return;
}
var webApiParserPlugin = this.m_container.Resolve<WebApiParserPlugin>();
if (webApiParserPlugin == null)
{
return;
}
var assembly = Assembly.GetExecutingAssembly();
var names = assembly.GetManifestResourceNames();
foreach (var item in names)
{
using (var stream = assembly.GetManifestResourceStream(item))
{
var bytes = new byte[stream.Length];
stream.Read(bytes, 0, bytes.Length);
var prefix = this.Prefix.IsNullOrEmpty() ? "/" : (this.Prefix.StartsWith("/") ? this.Prefix : $"/{this.Prefix}");
var name = item.Replace("ThingsGateway.Foundation.TouchSocket.WebApi.Swagger.api.", string.Empty);
if (name == "openapi.json")
{
try
{
bytes = this.BuildOpenApi(webApiParserPlugin);
}
catch (Exception ex)
{
this.m_logger.Exception(ex);
}
}
var key = prefix == "/" ? $"/{name}" : $"{prefix}/{name}";
this.m_swagger.Add(key, bytes);
}
}
await e.InvokeNext();
if (this.LaunchBrowser)
{
var iphost = (sender as ITcpService).Monitors.First().Option.IpHost;
string host;
if (iphost.IsLoopback || iphost.DnsSafeHost == "127.0.0.1" || iphost.DnsSafeHost == "0.0.0.0")
{
host = "127.0.0.1";
}
else
{
host = iphost.DnsSafeHost;
}
var scheme = iphost.Scheme == "https" ? iphost.Scheme : "http";
var prefix = this.Prefix.IsNullOrEmpty() ? "/" : (this.Prefix.StartsWith("/") ? this.Prefix : $"/{this.Prefix}");
var index = prefix == "/" ? $"/index.html" : $"{prefix}/index.html";
this.OpenWebLink($"{scheme}://{host}:{iphost.Port}{index}");
}
}
/// <summary>
/// 设置访问Swagger的前缀默认“swagger”
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public SwaggerPlugin SetPrefix(string value)
{
this.Prefix = value;
return this;
}
/// <summary>
/// 在浏览器打开Swagger页面
/// </summary>
/// <returns></returns>
public SwaggerPlugin UseLaunchBrowser()
{
this.LaunchBrowser = true;
return this;
}
/// <inheritdoc/>
protected override void Loaded(IPluginsManager pluginsManager)
{
base.Loaded(pluginsManager);
pluginsManager.Add<IHttpSocketClient, HttpContextEventArgs>(nameof(IHttpPlugin.OnHttpRequest), this.OnHttpRequest);
}
/// <summary>
/// 检查类型是否是 IFormFileCollection 类型
/// </summary>
/// <param name="type"><see cref="Type"/></param>
/// <returns><see cref="bool"/></returns>
private static bool IsFormFileCollection(Type type)
{
// 如果是 MultifileCollection 类型则直接返回
if (typeof(MultifileCollection).IsAssignableFrom(type))
{
return true;
}
// 处理 IFormFile 集合类型
if (typeof(IEnumerable).IsAssignableFrom(type))
{
// 检查是否是 IFormFile 数组类型
if (type.IsArray)
{
// 获取数组元素类型
var elementType = type.GetElementType();
// 检查元素类型是否是 IFormFile 类型
if (elementType != null
&& typeof(IFormFile).IsAssignableFrom(elementType))
{
return true;
}
}
// 检查是否是 IFormFile 集合类型
else
{
// 检查集合项类型是否是 IFormFile 类型
if (type.IsGenericType && type.GenericTypeArguments.Length == 1 && typeof(IFormFile).IsAssignableFrom(type.GenericTypeArguments[0]))
{
return true;
}
}
}
return false;
}
private void OpenWebLink(string url)
{
try
{
var psi = new ProcessStartInfo
{
FileName = url,
UseShellExecute = true
};
Process.Start(psi);
}
catch (Exception ex)
{
this.m_logger.Exception(ex);
}
}
#region Build
private void AddSchemaType(Type type, in List<Type> types)
{
if (type.IsArray)
{
type = type.GetElementType();
}
else if (typeof(IEnumerable).IsAssignableFrom(type) && type.IsGenericType && type.GenericTypeArguments.Length == 1)
{
type = type.GetGenericArguments()[0];
}
if (type.IsPrimitive || type == typeof(string))
{
return;
}
if (types.Contains(type))
{
return;
}
if (this.ParseDataTypes(type) != OpenApiDataTypes.Object)
{
return;
}
types.Add(type);
foreach (var item in type.GetProperties())
{
this.AddSchemaType(item.PropertyType, types);
}
}
private void BuildGet(WebApiParserPlugin webApiParserPlugin, in List<Type> schemaTypeList, in Dictionary<string, OpenApiPath> paths)
{
foreach (var item in webApiParserPlugin.GetRouteMap)
{
//解析get
var methodInstance = item.Value;
var openApiPath = new OpenApiPath();
var openApiPathValue = new OpenApiPathValue
{
Tags = this.GetTags(methodInstance),
Description = methodInstance.GetDescription(),
Summary = methodInstance.GetDescription()
};
var i = 0;
if (methodInstance.IncludeCallContext)
{
i = 1;
}
var parameters = new List<OpenApiParameter>();
for (; i < methodInstance.Parameters.Length; i++)
{
var parameter = methodInstance.Parameters[i];
var openApiParameter = this.GetParameter(parameter);
openApiParameter.In = "query";
this.AddSchemaType(parameter.ParameterType, schemaTypeList);
parameters.Add(openApiParameter);
}
openApiPathValue.Parameters = parameters.Count > 0 ? parameters : null;
this.BuildResponse(methodInstance, openApiPathValue, schemaTypeList);
openApiPath.Add("get", openApiPathValue);
paths.Add(item.Key, openApiPath);
}
}
private byte[] BuildOpenApi(WebApiParserPlugin webApiParserPlugin)
{
var openApiRoot = new OpenApiRoot();
openApiRoot.Info = new OpenApiInfo();
var paths = new Dictionary<string, OpenApiPath>();
var schemaTypeList = new List<Type>();
this.BuildGet(webApiParserPlugin, schemaTypeList, paths);
this.BuildPost(webApiParserPlugin, schemaTypeList, paths);
openApiRoot.Paths = paths;
openApiRoot.Components = this.GetComponents(schemaTypeList);
var jsonSetting = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore };
return JsonConvert.SerializeObject(openApiRoot, Formatting.Indented, jsonSetting).ToUTF8Bytes();
}
private void BuildPost(WebApiParserPlugin webApiParserPlugin, in List<Type> schemaTypeList, in Dictionary<string, OpenApiPath> paths)
{
foreach (var item in webApiParserPlugin.PostRouteMap)
{
//解析post
var methodInstance = item.Value;
var openApiPath = new OpenApiPath();
var openApiPathValue = new OpenApiPathValue
{
Tags = this.GetTags(methodInstance),
Description = methodInstance.GetDescription(),
Summary = methodInstance.GetDescription()
};
var i = 0;
if (methodInstance.IncludeCallContext)
{
i = 1;
}
var parameters = new List<OpenApiParameter>();
for (; i < methodInstance.Parameters.Length; i++)
{
var parameter = methodInstance.Parameters[i];
if (this.ParseDataTypes(parameter.ParameterType).IsPrimitive())
{
var openApiParameter = this.GetParameter(parameter);
openApiParameter.In = "query";
this.AddSchemaType(parameter.ParameterType, schemaTypeList);
parameters.Add(openApiParameter);
}
}
openApiPathValue.Parameters = parameters.Count > 0 ? parameters : default;
ParameterInfo parameterInfo = null;
if (methodInstance.IncludeCallContext)
{
if (methodInstance.Parameters.Length > 1)
{
parameterInfo = methodInstance.Parameters.Last();
}
}
else
{
if (methodInstance.Parameters.Length > 0)
{
parameterInfo = methodInstance.Parameters.Last();
}
}
if (parameterInfo != null && !this.ParseDataTypes(parameterInfo.ParameterType).IsPrimitive())
{
this.AddSchemaType(parameterInfo.ParameterType, schemaTypeList);
var body = new OpenApiRequestBody();
body.Content = new Dictionary<string, OpenApiContent>();
var content = new OpenApiContent();
content.Schema = this.CreateSchema(parameterInfo.ParameterType);
body.Content.Add("application/json", content);
openApiPathValue.RequestBody = body;
}
this.BuildResponse(methodInstance, openApiPathValue, schemaTypeList);
openApiPath.Add("post", openApiPathValue);
paths.Add(item.Key, openApiPath);
}
}
private void BuildResponse(MethodInstance methodInstance, in OpenApiPathValue openApiPathValue, in List<Type> schemaTypeList)
{
var openApiResponse = new OpenApiResponse();
openApiResponse.Description = "Success";
if (methodInstance.HasReturn)
{
openApiResponse.Content = new Dictionary<string, OpenApiContent>();
var openApiContent = new OpenApiContent();
openApiResponse.Content.Add("application/json", openApiContent);
openApiContent.Schema = this.CreateSchema(methodInstance.ReturnType);
this.AddSchemaType(methodInstance.ReturnType, schemaTypeList);
}
openApiPathValue.Responses = new Dictionary<string, OpenApiResponse>();
openApiPathValue.Responses.Add("200", openApiResponse);
}
private OpenApiProperty CreateProperty(Type type)
{
var openApiProperty = new OpenApiProperty();
var dataTypes = this.ParseDataTypes(type);
switch (dataTypes)
{
case OpenApiDataTypes.String:
case OpenApiDataTypes.Number:
case OpenApiDataTypes.Boolean:
case OpenApiDataTypes.Integer:
{
openApiProperty.Type = dataTypes;
break;
}
case OpenApiDataTypes.Binary:
break;
case OpenApiDataTypes.BinaryCollection:
break;
case OpenApiDataTypes.Record:
break;
case OpenApiDataTypes.Tuple:
break;
case OpenApiDataTypes.Array:
{
openApiProperty.Type = dataTypes;
openApiProperty.Items = this.CreateProperty(type.IsArray ? type.GetElementType() : type.GetGenericArguments()[0]);
}
break;
case OpenApiDataTypes.Object:
{
openApiProperty.Ref = $"#/components/schemas/{type.Name}";
break;
}
case OpenApiDataTypes.Struct:
break;
case OpenApiDataTypes.Any:
break;
default:
break;
}
openApiProperty.Format = this.GetSchemaFormat(type);
return openApiProperty;
}
private OpenApiSchema CreateSchema(Type type)
{
var schema = new OpenApiSchema();
var dataTypes = this.ParseDataTypes(type);
switch (dataTypes)
{
case OpenApiDataTypes.String:
case OpenApiDataTypes.Number:
case OpenApiDataTypes.Boolean:
case OpenApiDataTypes.Integer:
{
schema.Type = dataTypes;
break;
}
case OpenApiDataTypes.Binary:
break;
case OpenApiDataTypes.BinaryCollection:
break;
case OpenApiDataTypes.Record:
break;
case OpenApiDataTypes.Tuple:
break;
case OpenApiDataTypes.Array:
{
schema.Type = dataTypes;
schema.Items = this.CreateSchema(type.IsArray ? type.GetElementType() : type.GetGenericArguments()[0]);
}
break;
case OpenApiDataTypes.Object:
{
if (type.IsEnum)
{
schema.Enum = (int[])Enum.GetValues(type);
schema.Type = OpenApiDataTypes.Integer;
}
else
{
schema.Ref = $"#/components/schemas/{this.GetSchemaName(type)}";
}
break;
}
case OpenApiDataTypes.Struct:
break;
case OpenApiDataTypes.Any:
break;
default:
break;
}
schema.Format = this.GetSchemaFormat(type);
return schema;
}
private string GetSchemaName(Type type)
{
if (!type.IsGenericType)
{
return type.Name;
}
StringBuilder stringBuilder = new StringBuilder();
foreach (var item in type.GetGenericArguments())
{
stringBuilder.Append(GetSchemaName(item));
}
stringBuilder.Append(type.Name.Substring(0, type.Name.IndexOf('`')));
return stringBuilder.ToString();
}
private OpenApiComponent GetComponents(List<Type> types)
{
if (types.Count == 0)
{
return default;
}
var components = new OpenApiComponent();
components.Schemas = new Dictionary<string, OpenApiSchema>();
foreach (var type in types)
{
var schema = this.CreateSchema(type);
var properties = new Dictionary<string, OpenApiProperty>();
foreach (var propertyInfo in type.GetProperties(BindingFlags.Instance | BindingFlags.Public))
{
properties.Add(propertyInfo.Name, this.CreateProperty(propertyInfo.PropertyType));
}
schema.Properties = properties.Count == 0 ? default : properties;
components.Schemas.TryAdd(this.GetSchemaName(type), schema);
}
return components;
}
private string GetIn(Type type)
{
if (type.IsPrimitive || type == typeof(string))
{
return "query";
}
return null;
}
private OpenApiParameter GetParameter(ParameterInfo parameterInfo)
{
var openApiParameter = new OpenApiParameter();
openApiParameter.Name = parameterInfo.Name;
var openApiSchema = this.CreateSchema(parameterInfo.ParameterType);
openApiParameter.Schema = openApiSchema;
return openApiParameter;
}
private string GetSchemaFormat(Type type)
{
switch (Type.GetTypeCode(type))
{
case TypeCode.SByte:
case TypeCode.Byte:
case TypeCode.Int16:
case TypeCode.UInt16:
case TypeCode.Int32:
case TypeCode.UInt32:
return "int32";
case TypeCode.Int64:
case TypeCode.UInt64:
return "int64";
case TypeCode.Single:
return "float";
case TypeCode.Double:
case TypeCode.Decimal:
return "double";
case TypeCode.DateTime:
return "date-time";
case TypeCode.Boolean:
case TypeCode.Char:
case TypeCode.String:
return default;
default:
return "object";
}
}
private IEnumerable<string> GetTags(MethodInstance methodInstance)
{
var tags = new List<string>();
foreach (var item in methodInstance.ServerFromType.GetCustomAttributes(true))
{
if (item is SwaggerDescriptionAttribute descriptionAttribute)
{
if (descriptionAttribute.Groups != null)
{
tags.AddRange(descriptionAttribute.Groups);
}
}
}
foreach (var item in methodInstance.Info.GetCustomAttributes(true))
{
if (item is SwaggerDescriptionAttribute descriptionAttribute)
{
if (descriptionAttribute.Groups != null)
{
tags.AddRange(descriptionAttribute.Groups);
}
}
}
if (tags.Count == 0)
{
tags.Add(methodInstance.ServerFromType.Name);
}
return tags;
}
private Task OnHttpRequest(IHttpSocketClient client, HttpContextEventArgs e)
{
var context = e.Context;
var request = context.Request;
//var response = context.Response;
if (this.m_swagger.TryGetValue(request.RelativeURL, out var bytes))
{
e.Handled = true;
context.Response
.SetStatus()
.SetContentTypeByExtension(Path.GetExtension(request.RelativeURL))
.SetContent(bytes);
context.Response.Answer();
return EasyTask.CompletedTask;
}
return e.InvokeNext();
}
private OpenApiDataTypes ParseDataTypes(Type type)
{
// 空检查
if (type is null)
{
return OpenApiDataTypes.Any;
}
return type switch
{
// 字符串
_ when typeof(string).IsAssignableFrom(type)
|| typeof(char).IsAssignableFrom(type) => OpenApiDataTypes.String,
// 整数
_ when type.IsInteger() => OpenApiDataTypes.Integer,
// 数值
_ when type.IsDecimal() => OpenApiDataTypes.Number,
// 布尔值
_ when typeof(bool).IsAssignableFrom(type) => OpenApiDataTypes.Boolean,
// 日期
_ when typeof(DateTime).IsAssignableFrom(type)
|| typeof(DateTimeOffset).IsAssignableFrom(type) => OpenApiDataTypes.String,
// 二进制
_ when typeof(IFormFile).IsAssignableFrom(type) => OpenApiDataTypes.Binary,
// 二进制集合
_ when IsFormFileCollection(type) => OpenApiDataTypes.BinaryCollection,
// 记录值
_ when type.IsDictionary() => OpenApiDataTypes.Record,
// 元组值
_ when type.IsValueTuple() => OpenApiDataTypes.Tuple,
// 数组
_ when type.IsArray || (typeof(IEnumerable).IsAssignableFrom(type)
&& type.IsGenericType
&& type.GenericTypeArguments.Length == 1) => OpenApiDataTypes.Array,
// 对象
_ when type.IsEnum || (type != typeof(object)
&& type.IsClass
&& Type.GetTypeCode(type) == TypeCode.Object) => OpenApiDataTypes.Object,
// 结构
_ when type.IsValueType
&& !type.IsPrimitive
&& !type.IsEnum => OpenApiDataTypes.Struct,
// 缺省值
_ => OpenApiDataTypes.Any
};
}
#endregion Build
}
}