-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: Fix
IncludeXmlCommentsWithRemarks
- Loading branch information
Showing
14 changed files
with
591 additions
and
190 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
188 changes: 188 additions & 0 deletions
188
src/Unchase.Swashbuckle.AspNetCore.Extensions/Extensions/XmlCommentsExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,188 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Reflection; | ||
using System.Xml.XPath; | ||
using Microsoft.OpenApi.Any; | ||
using Microsoft.OpenApi.Models; | ||
using Swashbuckle.AspNetCore.SwaggerGen; | ||
|
||
namespace Unchase.Swashbuckle.AspNetCore.Extensions.Extensions | ||
{ | ||
internal static class XmlCommentsExtensions | ||
{ | ||
private const string SummaryTag = "summary"; | ||
private const string RemarksTag = "remarks"; | ||
private const string ExampleTag = "example"; | ||
|
||
internal static Type GetTargetRecursive(this Type type, Dictionary<string, string> inheritedDocs, string cref) | ||
{ | ||
var target = GetTarget(type, cref); | ||
|
||
if (target == null) | ||
{ | ||
return null; | ||
} | ||
|
||
string targetMemberName = XmlCommentsNodeNameHelper.GetMemberNameForType(target); | ||
|
||
if (inheritedDocs.ContainsKey(targetMemberName)) | ||
{ | ||
return GetTarget(target, inheritedDocs[targetMemberName]); | ||
} | ||
|
||
return target; | ||
} | ||
|
||
private static Type GetTarget(Type type, string cref) | ||
{ | ||
var targets = type.GetInterfaces(); | ||
if (type.BaseType != typeof(object)) | ||
{ | ||
targets = targets.Append(type.BaseType).ToArray(); | ||
} | ||
|
||
// Try to find the target, if one is declared. | ||
if (!string.IsNullOrEmpty(cref)) | ||
{ | ||
var crefTarget = targets.SingleOrDefault(t => XmlCommentsNodeNameHelper.GetMemberNameForType(t) == cref); | ||
|
||
if (crefTarget != null) | ||
{ | ||
return crefTarget; | ||
} | ||
} | ||
|
||
// We use the last since that will be our base class or the "nearest" implemented interface. | ||
return targets.LastOrDefault(); | ||
} | ||
|
||
internal static MemberInfo GetTargetRecursive(this MemberInfo memberInfo, Dictionary<string, string> inheritedDocs, string cref) | ||
{ | ||
var target = GetTarget(memberInfo, cref); | ||
|
||
if (target == null) | ||
{ | ||
return null; | ||
} | ||
|
||
string targetMemberName = XmlCommentsNodeNameHelper.GetMemberNameForFieldOrProperty(target); | ||
|
||
if (inheritedDocs.ContainsKey(targetMemberName)) | ||
{ | ||
return GetTarget(target, inheritedDocs[targetMemberName]); | ||
} | ||
|
||
return target; | ||
} | ||
|
||
private static MemberInfo GetTarget(MemberInfo memberInfo, string cref) | ||
{ | ||
var type = memberInfo.DeclaringType ?? memberInfo.ReflectedType; | ||
|
||
if (type == null) | ||
{ | ||
return null; | ||
} | ||
|
||
// Find all matching members in all interfaces and the base class. | ||
var targets = type.GetInterfaces() | ||
.Append(type.BaseType) | ||
.SelectMany( | ||
x => x.FindMembers( | ||
memberInfo.MemberType, | ||
BindingFlags.Instance | BindingFlags.Public, | ||
(info, _) => info.Name == memberInfo.Name, | ||
null)) | ||
.ToList(); | ||
|
||
// Try to find the target, if one is declared. | ||
if (!string.IsNullOrEmpty(cref)) | ||
{ | ||
var crefTarget = targets.SingleOrDefault(t => XmlCommentsNodeNameHelper.GetMemberNameForFieldOrProperty(t) == cref); | ||
|
||
if (crefTarget != null) | ||
{ | ||
return crefTarget; | ||
} | ||
} | ||
|
||
// We use the last since that will be our base class or the "nearest" implemented interface. | ||
return targets.LastOrDefault(); | ||
} | ||
|
||
internal static void ApplyPropertyComments( | ||
this OpenApiSchema schema, | ||
MemberInfo memberInfo, | ||
List<XPathDocument> documents, | ||
Dictionary<string, string> inheritedDocs, | ||
bool includeRemarks = false, | ||
params Type[] excludedTypes) | ||
{ | ||
string memberName = XmlCommentsNodeNameHelper.GetMemberNameForFieldOrProperty(memberInfo); | ||
|
||
if (!inheritedDocs.ContainsKey(memberName)) | ||
{ | ||
return; | ||
} | ||
|
||
if (excludedTypes.Any() && excludedTypes.ToList() | ||
.Contains(((PropertyInfo)memberInfo).PropertyType)) | ||
{ | ||
return; | ||
} | ||
|
||
string cref = inheritedDocs[memberName]; | ||
var target = memberInfo.GetTargetRecursive(inheritedDocs, cref); | ||
if (target == null) | ||
{ | ||
return; | ||
} | ||
|
||
var targetXmlNode = GetMemberXmlNode(XmlCommentsNodeNameHelper.GetMemberNameForFieldOrProperty(target), documents); | ||
|
||
if (targetXmlNode == null) | ||
{ | ||
return; | ||
} | ||
|
||
var summaryNode = targetXmlNode.SelectSingleNode(SummaryTag); | ||
if (summaryNode != null) | ||
{ | ||
schema.Description = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml); | ||
|
||
if (includeRemarks) | ||
{ | ||
var remarksNode = targetXmlNode.SelectSingleNode(RemarksTag); | ||
if (remarksNode != null && !string.IsNullOrWhiteSpace(remarksNode.InnerXml)) | ||
{ | ||
schema.Description += $" ({XmlCommentsTextHelper.Humanize(remarksNode.InnerXml)})"; | ||
} | ||
} | ||
} | ||
|
||
var exampleNode = targetXmlNode.SelectSingleNode(ExampleTag); | ||
if (exampleNode != null) | ||
{ | ||
schema.Example = new OpenApiString(XmlCommentsTextHelper.Humanize(exampleNode.InnerXml)); | ||
} | ||
} | ||
|
||
internal static XPathNavigator GetMemberXmlNode(string memberName, List<XPathDocument> documents) | ||
{ | ||
string path = $"/doc/members/member[@name='{memberName}']"; | ||
|
||
foreach (var document in documents) | ||
{ | ||
var node = document.CreateNavigator().SelectSingleNode(path); | ||
|
||
if (node != null) | ||
{ | ||
return node; | ||
} | ||
} | ||
|
||
return null; | ||
} | ||
} | ||
} |
106 changes: 106 additions & 0 deletions
106
src/Unchase.Swashbuckle.AspNetCore.Extensions/Filters/InheritDocParameterFilter.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Xml.XPath; | ||
using Microsoft.OpenApi.Models; | ||
using Swashbuckle.AspNetCore.SwaggerGen; | ||
using Unchase.Swashbuckle.AspNetCore.Extensions.Extensions; | ||
|
||
namespace Unchase.Swashbuckle.AspNetCore.Extensions.Filters | ||
{ | ||
/// <summary> | ||
/// Adds documentation to parameters that is provided by the <inhertidoc /> tag. | ||
/// </summary> | ||
/// <seealso cref="IParameterFilter" /> | ||
internal class InheritDocParameterFilter : IParameterFilter | ||
{ | ||
#region Fields | ||
|
||
private const string SummaryTag = "summary"; | ||
private const string RemarksTag = "remarks"; | ||
private const string ExampleTag = "example"; | ||
private readonly bool _includeRemarks; | ||
private readonly List<XPathDocument> _documents; | ||
private readonly Dictionary<string, string> _inheritedDocs; | ||
private readonly Type[] _excludedTypes; | ||
|
||
#endregion | ||
|
||
#region Constructors | ||
|
||
/// <summary> | ||
/// Initializes a new instance of the <see cref="InheritDocParameterFilter" /> class. | ||
/// </summary> | ||
/// <param name="inheritedDocs">Dictionary with inheritdoc in form of name-cref.</param> | ||
/// <param name="includeRemarks">Include remarks from inheritdoc XML comments.</param> | ||
/// <param name="documents">List of <see cref="XPathDocument"/>.</param> | ||
public InheritDocParameterFilter(List<XPathDocument> documents, Dictionary<string, string> inheritedDocs, bool includeRemarks = false) | ||
: this(documents, inheritedDocs, includeRemarks, Array.Empty<Type>()) | ||
{ | ||
} | ||
|
||
/// <summary> | ||
/// Initializes a new instance of the <see cref="InheritDocParameterFilter" /> class. | ||
/// </summary> | ||
/// <param name="inheritedDocs">Dictionary with inheritdoc in form of name-cref.</param> | ||
/// <param name="includeRemarks">Include remarks from inheritdoc XML comments.</param> | ||
/// <param name="documents">List of <see cref="XPathDocument"/>.</param> | ||
/// <param name="excludedTypes">Excluded types.</param> | ||
public InheritDocParameterFilter(List<XPathDocument> documents, Dictionary<string, string> inheritedDocs, bool includeRemarks = false, params Type[] excludedTypes) | ||
{ | ||
_includeRemarks = includeRemarks; | ||
_excludedTypes = excludedTypes; | ||
_documents = documents; | ||
_inheritedDocs = inheritedDocs; | ||
} | ||
|
||
#endregion | ||
|
||
#region Methods | ||
|
||
/// <summary> | ||
/// Apply filter. | ||
/// </summary> | ||
/// <param name="parameter"><see cref="OpenApiParameter"/>.</param> | ||
/// <param name="context"><see cref="ParameterFilterContext"/>.</param> | ||
public void Apply(OpenApiParameter parameter, ParameterFilterContext context) | ||
{ | ||
if (context.ApiParameterDescription.PropertyInfo() == null) | ||
{ | ||
return; | ||
} | ||
|
||
if (_excludedTypes.Any() && _excludedTypes.ToList().Contains(context.ApiParameterDescription.PropertyInfo().DeclaringType)) | ||
{ | ||
return; | ||
} | ||
|
||
// Try to apply a description for inherited types. | ||
string parameterMemberName = XmlCommentsNodeNameHelper.GetMemberNameForFieldOrProperty(context.ApiParameterDescription.PropertyInfo()); | ||
if (string.IsNullOrEmpty(parameter.Description) && _inheritedDocs.ContainsKey(parameterMemberName)) | ||
{ | ||
string cref = _inheritedDocs[parameterMemberName]; | ||
var target = context.ApiParameterDescription.PropertyInfo().GetTargetRecursive(_inheritedDocs, cref); | ||
|
||
var targetXmlNode = XmlCommentsExtensions.GetMemberXmlNode(XmlCommentsNodeNameHelper.GetMemberNameForFieldOrProperty(target), _documents); | ||
var summaryNode = targetXmlNode?.SelectSingleNode(SummaryTag); | ||
|
||
if (summaryNode != null) | ||
{ | ||
parameter.Description = XmlCommentsTextHelper.Humanize(summaryNode.InnerXml); | ||
|
||
if (_includeRemarks) | ||
{ | ||
var remarksNode = targetXmlNode.SelectSingleNode(RemarksTag); | ||
if (remarksNode != null && !string.IsNullOrWhiteSpace(remarksNode.InnerXml)) | ||
{ | ||
parameter.Description += $" ({XmlCommentsTextHelper.Humanize(remarksNode.InnerXml)})"; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
#endregion | ||
} | ||
} |
Oops, something went wrong.