This commit is contained in:
2025-04-24 18:31:27 +08:00
commit 9340f5253e
2796 changed files with 1387124 additions and 0 deletions

View File

@@ -0,0 +1,140 @@
using System.Linq;
using System.Reflection;
using System.Text;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Description;
using System.Xml.XPath;
using Swashbuckle.Application;
using Swashbuckle.Swagger.Annotations;
namespace Swashbuckle.Swagger.XmlComments
{
public class ApplyXmlActionComments : IOperationFilter
{
private const string MemberXPath = "/doc/members/member[@name='{0}']";
private const string SummaryXPath = "summary";
private const string RemarksXPath = "remarks";
private const string ParamXPath = "param[@name='{0}']";
private const string ResponseXPath = "response";
private readonly XPathDocument _xmlDoc;
public ApplyXmlActionComments(string filePath)
: this(new XPathDocument(filePath)) { }
public ApplyXmlActionComments(XPathDocument xmlDoc)
{
_xmlDoc = xmlDoc;
}
public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
{
var reflectedActionDescriptor = apiDescription.ActionDescriptor as ReflectedHttpActionDescriptor;
if (reflectedActionDescriptor == null) return;
XPathNavigator navigator;
lock (_xmlDoc)
{
navigator = _xmlDoc.CreateNavigator();
}
var commentId = XmlCommentsIdHelper.GetCommentIdForMethod(reflectedActionDescriptor.MethodInfo);
var methodNode = navigator.SelectSingleNode(string.Format(MemberXPath, commentId));
if (methodNode == null) return;
var summaryNode = methodNode.SelectSingleNode(SummaryXPath);
if (summaryNode != null)
operation.summary = summaryNode.ExtractContent();
var remarksNode = methodNode.SelectSingleNode(RemarksXPath);
if (remarksNode != null)
operation.description = remarksNode.ExtractContent();
ApplyParamComments(operation, methodNode, reflectedActionDescriptor.MethodInfo);
ApplyResponseComments(operation, methodNode);
ApplyDeveloperInfo(operation,apiDescription);
}
private static void ApplyParamComments(Operation operation, XPathNavigator methodNode, MethodInfo method)
{
if (operation.parameters == null) return;
foreach (var parameter in operation.parameters)
{
// Inspect method to find the corresponding action parameter
// NOTE: If a parameter binding is present (e.g. [FromUri(Name..)]), then the lookup needs
// to be against the "bound" name and not the actual parameter name
var actionParameter = method.GetParameters()
.FirstOrDefault(paramInfo =>
HasBoundName(paramInfo, parameter.name) || paramInfo.Name == parameter.name
);
if (actionParameter == null) continue;
var paramNode = methodNode.SelectSingleNode(string.Format(ParamXPath, actionParameter.Name));
if (paramNode != null)
parameter.description = paramNode.ExtractContent();
}
}
private static void ApplyResponseComments(Operation operation, XPathNavigator methodNode)
{
var responseNodes = methodNode.Select(ResponseXPath);
if (responseNodes.Count > 0)
{
var successResponse = operation.responses.First().Value;
operation.responses.Clear();
while (responseNodes.MoveNext())
{
var statusCode = responseNodes.Current.GetAttribute("code", "");
var description = responseNodes.Current.ExtractContent();
var response = new Response
{
description = description,
schema = statusCode.StartsWith("2") ? successResponse.schema : null
};
operation.responses[statusCode] = response;
}
}
}
private static bool HasBoundName(ParameterInfo paramInfo, string name)
{
var fromUriAttribute = paramInfo.GetCustomAttributes(false)
.OfType<FromUriAttribute>()
.FirstOrDefault();
return (fromUriAttribute != null && fromUriAttribute.Name == name);
}
/// <summary>
/// 加入开发者信息
/// </summary>
/// <param name="operation"></param>
/// <param name="apiDescription"></param>
private static void ApplyDeveloperInfo(Operation operation, ApiDescription apiDescription)
{
if (!SwaggerDocsConfig.ShowDeveloper)
{
return;
}
var authorInfo = apiDescription.GetControllerAndActionAttributes<ApiAuthorAttribute>().FirstOrDefault();
if (authorInfo == null)
{
operation.showDevStatus = false;
return;
}
operation.developer = authorInfo.Name;
operation.showDevStatus = authorInfo.Status != DevStatus.None;
operation.devStatus = authorInfo.Status.ToString();
operation.devStatusName = authorInfo.GetStatusName();
operation.modifyDate = authorInfo.Time;
}
}
}

View File

@@ -0,0 +1,68 @@
using System.Reflection;
using System.Xml.XPath;
namespace Swashbuckle.Swagger.XmlComments
{
public class ApplyXmlTypeComments : IModelFilter
{
private const string MemberXPath = "/doc/members/member[@name='{0}']";
private const string SummaryTag = "summary";
private readonly XPathDocument _xmlDoc;
private readonly XPathNavigator _navigator;
public ApplyXmlTypeComments(string filePath)
: this(new XPathDocument(filePath)) { }
public ApplyXmlTypeComments(XPathDocument xmlDoc)
{
_xmlDoc = xmlDoc;
_navigator = xmlDoc.CreateNavigator();
}
public XPathNavigator XmlNavigator
{
get { return _navigator; }
}
public void Apply(Schema model, ModelFilterContext context)
{
var commentId = XmlCommentsIdHelper.GetCommentIdForType(context.SystemType);
var typeNode = _navigator.SelectSingleNode(string.Format(MemberXPath, commentId));
if (typeNode != null)
{
var summaryNode = typeNode.SelectSingleNode(SummaryTag);
if (summaryNode != null)
model.description = summaryNode.ExtractContent();
}
if (model.properties != null)
{
foreach (var entry in model.properties)
{
var jsonProperty = context.JsonObjectContract.Properties[entry.Key];
if (jsonProperty == null) continue;
ApplyPropertyComments(_navigator, entry.Value, jsonProperty.PropertyInfo());
}
}
}
private static void ApplyPropertyComments(XPathNavigator navigator, Schema propertySchema, PropertyInfo propertyInfo)
{
if (propertyInfo == null) return;
var commentId = XmlCommentsIdHelper.GetCommentIdForProperty(propertyInfo);
var propertyNode = navigator.SelectSingleNode(string.Format(MemberXPath, commentId));
if (propertyNode == null) return;
var propSummaryNode = propertyNode.SelectSingleNode(SummaryTag);
if (propSummaryNode != null)
{
propertySchema.description = propSummaryNode.ExtractContent();
}
}
}
}

View File

@@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.XPath;
namespace Swashbuckle.Swagger.XmlComments
{
public static class XPathNavigatorExtensions
{
private static Regex ParamPattern = new Regex(@"<(see|paramref) (name|cref)=""([TPF]{1}:)?(?<display>.+?)"" />");
private static Regex ConstPattern = new Regex(@"<c>(?<display>.+?)</c>");
public static string ExtractContent(this XPathNavigator node)
{
if (node == null) return null;
return XmlTextHelper.NormalizeIndentation(
ConstPattern.Replace(
ParamPattern.Replace(node.InnerXml, GetParamRefName),
GetConstRefName)
);
}
private static string GetConstRefName(Match match)
{
if (match.Groups.Count != 2) return null;
return match.Groups["display"].Value;
}
private static string GetParamRefName(Match match)
{
if (match.Groups.Count != 5) return null;
return "{" + match.Groups["display"].Value + "}";
}
}
}

View File

@@ -0,0 +1,102 @@
using System;
using System.Reflection;
using System.Text;
namespace Swashbuckle.Swagger.XmlComments
{
public class XmlCommentsIdHelper
{
public static string GetCommentIdForMethod(MethodInfo methodInfo)
{
var builder = new StringBuilder("M:");
AppendFullTypeName(methodInfo.DeclaringType, builder);
builder.Append(".");
AppendMethodName(methodInfo, builder);
return builder.ToString();
}
public static string GetCommentIdForType(Type type)
{
var builder = new StringBuilder("T:");
AppendFullTypeName(type, builder, expandGenericArgs: false);
return builder.ToString();
}
public static string GetCommentIdForProperty(PropertyInfo propertyInfo)
{
var builder = new StringBuilder("P:");
AppendFullTypeName(propertyInfo.DeclaringType, builder);
builder.Append(".");
AppendPropertyName(propertyInfo, builder);
return builder.ToString();
}
private static void AppendFullTypeName(Type type, StringBuilder builder, bool expandGenericArgs = false)
{
if (type.Namespace != null)
{
builder.Append(type.Namespace);
builder.Append(".");
}
AppendTypeName(type, builder, expandGenericArgs);
}
private static void AppendTypeName(Type type, StringBuilder builder, bool expandGenericArgs)
{
if (type.IsNested)
{
AppendTypeName(type.DeclaringType, builder, false);
builder.Append(".");
}
builder.Append(type.Name);
if (expandGenericArgs)
ExpandGenericArgsIfAny(type, builder);
}
public static void ExpandGenericArgsIfAny(Type type, StringBuilder builder)
{
if (type.IsGenericType)
{
var genericArgsBuilder = new StringBuilder("{");
var genericArgs = type.GetGenericArguments();
foreach (var argType in genericArgs)
{
AppendFullTypeName(argType, genericArgsBuilder, true);
genericArgsBuilder.Append(",");
}
genericArgsBuilder.Replace(",", "}", genericArgsBuilder.Length - 1, 1);
builder.Replace(string.Format("`{0}", genericArgs.Length), genericArgsBuilder.ToString());
}
else if (type.IsArray)
ExpandGenericArgsIfAny(type.GetElementType(), builder);
}
private static void AppendMethodName(MethodInfo methodInfo, StringBuilder builder)
{
builder.Append(methodInfo.Name);
var parameters = methodInfo.GetParameters();
if (parameters.Length == 0) return;
builder.Append("(");
foreach (var param in parameters)
{
AppendFullTypeName(param.ParameterType, builder, true);
builder.Append(",");
}
builder.Replace(",", ")", builder.Length - 1, 1);
}
private static void AppendPropertyName(PropertyInfo propertyInfo, StringBuilder builder)
{
builder.Append(propertyInfo.Name);
}
}
}

View File

@@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Swashbuckle.Swagger.XmlComments
{
public static class XmlTextHelper
{
/// <summary>
/// Messages text from an XML node produced by Visual Studio into a plainer plain text (leading whitespace normalized)
/// </summary>
/// <param name="xmlText">The content of an XML node - could contain other XML elements within the string</param>
/// <returns></returns>
public static string NormalizeIndentation(string xmlText)
{
if (null == xmlText)
throw new ArgumentNullException("xmlText");
string[] lines = xmlText.Split('\n');
string padding = GetCommonLeadingWhitespace(lines);
int padLen = padding == null ? 0 : padding.Length;
// remove leading padding from each line
for (int i = 0, l = lines.Length; i < l; ++i)
{
string line = lines[i].TrimEnd('\r'); // remove trailing '\r'
if (padLen != 0 && line.Length >= padLen && line.Substring(0, padLen) == padding)
line = line.Substring(padLen);
lines[i] = line;
}
// remove leading empty lines, but not all leading padding
// remove all trailing whitespace, regardless
return string.Join("\r\n", lines.SkipWhile(x => string.IsNullOrWhiteSpace(x))).TrimEnd();
}
/// <summary>
/// Finds the common padding prefix used on all non-empty lines.
/// </summary>
/// <param name="lines"></param>
/// <returns>The common padding found on all non-blank lines - returns null when no common prefix is found</returns>
static string GetCommonLeadingWhitespace(string[] lines)
{
if (null == lines)
throw new ArgumentException("lines");
if (lines.Length == 0)
return null;
string[] nonEmptyLines = lines
.Where(x => !string.IsNullOrWhiteSpace(x))
.ToArray();
if (nonEmptyLines.Length < 1)
return null;
int padLen = 0;
// use the first line as a seed, and see what is shared over all nonEmptyLines
string seed = nonEmptyLines[0];
for (int i = 0, l = seed.Length; i < l; ++i)
{
if (!char.IsWhiteSpace(seed, i))
break;
if (nonEmptyLines.Any(line => line[i] != seed[i]))
break;
++padLen;
}
if (padLen > 0)
return seed.Substring(0, padLen);
return null;
}
}
}