Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide more extensive method documentation. #281

Merged
merged 7 commits into from
Jan 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ The following is an example `.refitter` file
},
"generateContracts": true, // Optional. Default=true
"generateXmlDocCodeComments": true, // Optional. Default=true
"generateStatusCodeComments": true, // Optional. Default=true
"addAutoGeneratedHeader": true, // Optional. Default=true
"addAcceptHeaders": true, // Optional. Default=true
"returnIApiResponse": false, // Optional. Default=false
Expand Down Expand Up @@ -231,6 +232,7 @@ The following is an example `.refitter` file
- `naming.interfaceName` - the name of the generated interface. The generated code will automatically prefix this with `I` so if this set to `MyApiClient` then the generated interface is called `IMyApiClient`. Default is `ApiClient`
- `generateContracts` - a boolean indicating whether contracts should be generated. A use case for this is several API clients use the same contracts. Default is `true`
- `generateXmlDocCodeComments` - a boolean indicating whether XML doc comments should be generated. Default is `true`
- `generateStatusCodeComments` - a boolean indicating whether the XML docs for `ApiException` and `IApiResponse` contain detailed descriptions for every documented status code. Default is `true`
- `addAutoGeneratedHeader` - a boolean indicating whether XML doc comments should be generated. Default is `true`
- `addAcceptHeaders` - a boolean indicating whether to add accept headers [Headers("Accept: application/json")]. Default is `true`
- `returnIApiResponse` - a boolean indicating whether to return `IApiResponse<T>` objects. Default is `false`
Expand Down
11 changes: 4 additions & 7 deletions src/Refitter.Core/RefitGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,6 @@
using System.Text;
using System.Text.RegularExpressions;

using Microsoft.OpenApi;
using Microsoft.OpenApi.Extensions;
using Microsoft.OpenApi.Readers;

namespace Refitter.Core;

/// <summary>
Expand Down Expand Up @@ -128,6 +124,7 @@ public string Generate()
{
var factory = new CSharpClientGeneratorFactory(settings, document);
var generator = factory.Create();
var docGenerator = new XmlDocumentationGenerator(settings);
var contracts = RefitInterfaceImports
.GetImportedNamespaces(settings)
.Aggregate(
Expand All @@ -136,9 +133,9 @@ public string Generate()

IRefitInterfaceGenerator interfaceGenerator = settings.MultipleInterfaces switch
{
MultipleInterfaces.ByEndpoint => new RefitMultipleInterfaceGenerator(settings, document, generator),
MultipleInterfaces.ByTag => new RefitMultipleInterfaceByTagGenerator(settings, document, generator),
_ => new RefitInterfaceGenerator(settings, document, generator),
MultipleInterfaces.ByEndpoint => new RefitMultipleInterfaceGenerator(settings, document, generator, docGenerator),
MultipleInterfaces.ByTag => new RefitMultipleInterfaceByTagGenerator(settings, document, generator, docGenerator),
_ => new RefitInterfaceGenerator(settings, document, generator, docGenerator),
};

var generatedCode = GenerateClient(interfaceGenerator);
Expand Down
37 changes: 5 additions & 32 deletions src/Refitter.Core/RefitInterfaceGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,18 @@ internal class RefitInterfaceGenerator : IRefitInterfaceGenerator
protected readonly RefitGeneratorSettings settings;
protected readonly OpenApiDocument document;
protected readonly CustomCSharpClientGenerator generator;
protected readonly XmlDocumentationGenerator docGenerator;

internal RefitInterfaceGenerator(
RefitGeneratorSettings settings,
OpenApiDocument document,
CustomCSharpClientGenerator generator)
CustomCSharpClientGenerator generator,
XmlDocumentationGenerator docGenerator)
{
this.settings = settings;
this.document = document;
this.generator = generator;
this.docGenerator = docGenerator;
generator.BaseSettings.OperationNameGenerator = new OperationNameGenerator(document, settings);
}

Expand Down Expand Up @@ -58,7 +61,7 @@ private string GenerateInterfaceBody()
var parameters = ParameterExtractor.GetParameters(operationModel, operation, settings);
var parametersString = string.Join(", ", parameters);

GenerateMethodXmlDocComments(operation, code);
this.docGenerator.AppendMethodDocumentation(operationModel, code);
GenerateObsoleteAttribute(operation, code);
GenerateForMultipartFormData(operationModel, code);
GenerateAcceptHeaders(operations, operation, code);
Expand Down Expand Up @@ -177,22 +180,6 @@ private string GetConfiguredReturnType(string returnTypeParameter)
: $"Task<{WellKnownNamesspaces.TrimImportedNamespaces(returnTypeParameter)}>";
}

protected void GenerateMethodXmlDocComments(OpenApiOperation operation, StringBuilder code)
{
if (!settings.GenerateXmlDocCodeComments)
return;

if (!string.IsNullOrWhiteSpace(operation.Description))
{
code.AppendLine($"{Separator}{Separator}/// <summary>");

foreach (var line in operation.Description.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None))
code.AppendLine($"{Separator}{Separator}/// {line.Trim()}");

code.AppendLine($"{Separator}{Separator}/// </summary>");
}
}

protected void GenerateObsoleteAttribute(OpenApiOperation operation, StringBuilder code)
{
if (operation.IsDeprecated)
Expand All @@ -215,20 +202,6 @@ private string GenerateInterfaceDeclaration(out string interfaceName)
""";
}

protected void GenerateInterfaceXmlDocComments(OpenApiOperation operation, StringBuilder code)
{
if (!settings.GenerateXmlDocCodeComments ||
string.IsNullOrWhiteSpace(operation.Summary))
return;

code.AppendLine(
$"""
{Separator}/// <summary>
{Separator}/// {operation.Summary}
{Separator}/// </summary>
""");
}

protected string GetGeneratedCodeAttribute() =>
$"""
[System.CodeDom.Compiler.GeneratedCode("Refitter", "{GetType().Assembly.GetName().Version}")]
Expand Down
9 changes: 5 additions & 4 deletions src/Refitter.Core/RefitMultipleInterfaceByTagGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ internal class RefitMultipleInterfaceByTagGenerator : RefitInterfaceGenerator
internal RefitMultipleInterfaceByTagGenerator(
RefitGeneratorSettings settings,
OpenApiDocument document,
CustomCSharpClientGenerator generator)
: base(settings, document, generator)
CustomCSharpClientGenerator generator,
XmlDocumentationGenerator docGenerator)
: base(settings, document, generator, docGenerator)
{
}

Expand Down Expand Up @@ -49,7 +50,7 @@ public override RefitGeneratedCode GenerateCode()
if (!interfacesByGroup.TryGetValue(kv.Key, out var sb))
{
interfacesByGroup[kv.Key] = sb = new StringBuilder();
GenerateInterfaceXmlDocComments(operation, sb);
this.docGenerator.AppendInterfaceDocumentation(operation, sb);

interfaceName = GetInterfaceName(kv.Key);
interfaceNames.Add(interfaceName);
Expand All @@ -63,7 +64,7 @@ public override RefitGeneratedCode GenerateCode()
var parameters = ParameterExtractor.GetParameters(operationModel, operation, settings);
var parametersString = string.Join(", ", parameters);

GenerateMethodXmlDocComments(operation, sb);
this.docGenerator.AppendMethodDocumentation(operationModel, sb);
GenerateObsoleteAttribute(operation, sb);
GenerateForMultipartFormData(operationModel, sb);
GenerateAcceptHeaders(operations, operation, sb);
Expand Down
9 changes: 5 additions & 4 deletions src/Refitter.Core/RefitMultipleInterfaceGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ internal class RefitMultipleInterfaceGenerator : RefitInterfaceGenerator
internal RefitMultipleInterfaceGenerator(
RefitGeneratorSettings settings,
OpenApiDocument document,
CustomCSharpClientGenerator generator)
: base(settings, document, generator)
CustomCSharpClientGenerator generator,
XmlDocumentationGenerator docGenerator)
: base(settings, document, generator, docGenerator)
{
}

Expand All @@ -35,7 +36,7 @@ public override RefitGeneratedCode GenerateCode()
var returnType = GetTypeName(operation);
var verb = operations.Key.CapitalizeFirstCharacter();

GenerateInterfaceXmlDocComments(operation, code);
this.docGenerator.AppendInterfaceDocumentation(operation, code);

var interfaceName = GetInterfaceName(kv, verb, operation);
interfaceNames.Add(interfaceName);
Expand All @@ -48,7 +49,7 @@ public override RefitGeneratedCode GenerateCode()
var parameters = ParameterExtractor.GetParameters(operationModel, operation, settings);
var parametersString = string.Join(", ", parameters);

GenerateMethodXmlDocComments(operation, code);
this.docGenerator.AppendMethodDocumentation(operationModel, code);
GenerateObsoleteAttribute(operation, code);
GenerateForMultipartFormData(operationModel, code);
GenerateAcceptHeaders(operations, operation, code);
Expand Down
6 changes: 6 additions & 0 deletions src/Refitter.Core/Settings/RefitGeneratorSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ public class RefitGeneratorSettings
/// </summary>
public bool GenerateXmlDocCodeComments { get; set; } = true;

/// <summary>
/// Gets or sets a value indicating whether <c>ApiException</c> and <c>IApiResponse</c> should be documented with
/// the relevant status codes specified in the OpenAPI document.
/// </summary>
public bool GenerateStatusCodeComments { get; set; } = true;

/// <summary>
/// Gets or sets a value indicating whether to add auto-generated header.
/// </summary>
Expand Down
194 changes: 194 additions & 0 deletions src/Refitter.Core/XmlDocumentationGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
using System.Text;

using NSwag;
using NSwag.CodeGeneration.CSharp.Models;

namespace Refitter.Core;

/// <summary>
/// Generator class for creating XML documentation.
/// </summary>
public class XmlDocumentationGenerator
{
/// <summary>
/// The global code generation settings.
/// </summary>
private readonly RefitGeneratorSettings _settings;

/// <summary>
/// The whitespace to use for a single level of indentation.
/// </summary>
private const string Separator = " ";

/// <summary>
/// Instantiates a new instance of the <see cref="XmlDocumentationGenerator"/> class.
/// </summary>
/// <param name="settings">The code generation settings to use.</param>
internal XmlDocumentationGenerator(RefitGeneratorSettings settings)
{
this._settings = settings;
}

/// <summary>
/// Appends XML docs for the given interface definition to the given code builder.
/// </summary>
/// <param name="group">The OpenAPI definition of the interface.</param>
/// <param name="code">The builder to append the documentation to.</param>
public void AppendInterfaceDocumentation(OpenApiOperation group, StringBuilder code)
{
if (!_settings.GenerateXmlDocCodeComments || string.IsNullOrWhiteSpace(group.Summary))
return;

this.AppendXmlCommentBlock("summary", group.Summary, code, indent: Separator);
}

/// <summary>
/// Appends XML docs for the given method to the given code builder.
/// </summary>
/// <param name="method">The NSwag model of the method's OpenAPI definition.</param>
/// <param name="code">The builder to append the documentation to.</param>
public void AppendMethodDocumentation(CSharpOperationModel method, StringBuilder code)
{
if (!_settings.GenerateXmlDocCodeComments)
return;

if (!string.IsNullOrWhiteSpace(method.Summary))
this.AppendXmlCommentBlock("summary", method.Summary, code);

if (!string.IsNullOrWhiteSpace(method.Description))
{
this.AppendXmlCommentBlock("remarks", method.Description, code);
}

foreach (var parameter in method.Parameters)
{
if (parameter == null || string.IsNullOrWhiteSpace(parameter.Description))
continue;

this.AppendXmlCommentBlock("param", parameter.Description, code, new Dictionary<string, string>
{ ["name"] = parameter.VariableName });
}

if (_settings.ReturnIApiResponse)
{
this.AppendXmlCommentBlock("returns", this.BuildApiResponseDescription(method.Responses), code);
}
else
{
if (method.HasResult)
{
// Document the result with a fallback description.
var description = method.ResultDescription;
if (string.IsNullOrWhiteSpace(description))
description = "A <see cref=\"Task\"/> representing the result of the request.";
this.AppendXmlCommentBlock("returns", description, code);
}
else
{
// Document the returned task even when there is no result.
this.AppendXmlCommentBlock(
"returns",
"A <see cref=\"Task\"/> that completes when the request is finished.",
code);
}

this.AppendXmlCommentBlock(
"throws",
this.BuildErrorDescription(method.Responses),
code,
new Dictionary<string, string> { ["cref"] = "ApiException" });
}
}

/// <summary>
/// Append a single XML element to the given code builder.
/// If the content includes line breaks, it is placed on a new line and the existing breaks are preserved.
/// Otherwise, the element and its content are placed on the same line.
/// </summary>
/// <param name="tagName">The name of the tag to write.</param>
/// <param name="content">The content to place within the tag.</param>
/// <param name="code">The builder to append the tag to.</param>
/// <param name="attributes">An optional dictionary of attributes to add to the tag.</param>
/// <param name="indent">The whitespace to add before new lines.</param>
private void AppendXmlCommentBlock(
string tagName,
string content,
StringBuilder code,
Dictionary<string, string>? attributes = null,
string indent = $"{Separator}{Separator}")
{
code.Append($"{indent}/// <{tagName}");
if (attributes != null)
foreach (var attribute in attributes)
code.Append($" {attribute.Key}=\"{attribute.Value}\"");

code.Append(">");

var lines = content.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None);
if (lines.Length > 1)
{
// When working with multiple lines, place the content on a separate line with normalized linebreaks.
code.AppendLine();
foreach (var line in content.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None))
code.AppendLine($"{indent}/// {line.Trim()}");

code.AppendLine($"{indent}/// </{tagName}>");
}
else
{
// When the content only has a single line, place it on the same line as the tag.
code.AppendLine($"{content}</{tagName}>");
}
}

/// <summary>
/// Generates a human readable error description for the given endpoint responses. This includes available
/// documentation for response codes below 200 or above 299 if the
/// <see cref="RefitGeneratorSettings.GenerateStatusCodeComments"/> setting is enabled.
/// </summary>
/// <param name="responses">The responses to document.</param>
/// <returns>A string detailing the error codes and their description.</returns>
private string BuildErrorDescription(IEnumerable<CSharpResponseModel> responses)
{
return this.BuildResponseDescription(
"Thrown when the request returns a non-success status code",
responses.Where(response => !HttpUtilities.IsSuccessStatusCode(response.StatusCode)));
}

/// <summary>
/// Generates a human readable result description for the given endpoint responses. This includes all documented
/// response codes if the <see cref="RefitGeneratorSettings.GenerateStatusCodeComments"/> setting is enabled.
/// </summary>
/// <param name="responses">The responses to document.</param>
/// <returns>A string detailing the response codes and their description.</returns>
private string BuildApiResponseDescription(IEnumerable<CSharpResponseModel> responses)
{
return this.BuildResponseDescription(
"A <see cref=\"Task\"/> representing the <see cref=\"IApiResponse\"/> instance containing the result",
responses);
}

/// <summary>
/// Generates a description for the given responses.
/// </summary>
/// <param name="text">The text to prepend to the responses.</param>
/// <param name="responses">The responses to document.</param>
/// <returns>A string containing the given text and response descriptions.</returns>
private string BuildResponseDescription(string text, IEnumerable<CSharpResponseModel> responses)
{
var description = new StringBuilder(text);
var responseList = responses.ToList();
if (!this._settings.GenerateStatusCodeComments || !responseList.Any())
return description.Append(".").ToString();

description.Append(":");
foreach (var response in responseList)
{
description.AppendLine().Append(response.StatusCode);
if (!string.IsNullOrWhiteSpace(response.ExceptionDescription))
description.Append(": ").Append(response.ExceptionDescription);
}

return description.ToString();
}
}
1 change: 1 addition & 0 deletions src/Refitter.SourceGenerator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ The following is an example `.refitter` file
},
"generateContracts": true, // Optional. Default=true
"generateXmlDocCodeComments": true, // Optional. Default=true
"generateStatusCodeComments": true, // Optional. Default=true
"addAutoGeneratedHeader": true, // Optional. Default=true
"addAcceptHeaders": true, // Optional. Default=true
"returnIApiResponse": false, // Optional. Default=false
Expand Down
Loading