333
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 + "}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user