| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496 |
- // Licensed to the .NET Foundation under one or more agreements.
- // The .NET Foundation licenses this file to you under the MIT license.
- using System.Globalization;
- using System.Text.Json.Nodes;
- using Microsoft.OpenApi.Models;
- namespace Microsoft.AspNetCore.OpenApi.SourceGenerators.Tests;
- // Test scenarios derived from https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/xmldoc/examples
- [UsesVerify]
- public class CompletenessTests
- {
- [Fact]
- public async Task SupportsAllXmlTagsOnSchemas()
- {
- var source = """
- using System;
- using System.Collections.Generic;
- using System.Threading.Tasks;
- using Microsoft.AspNetCore.Builder;
- using Microsoft.Extensions.DependencyInjection;
- var builder = WebApplication.CreateBuilder();
- builder.Services.AddOpenApi();
- var app = builder.Build();
- app.MapPost("/example-class", (ExampleClass example) => { });
- app.MapPost("/person", (Person person) => { });
- app.MapPost("/derived-class", (DerivedClass child) => { });
- app.MapPost("/main-class", (MainClass main) => { });
- app.MapPost("/test-interface", (ITestInterface test) => { });
- app.MapPost("/implementing-class", (ImplementingClass impl) => { });
- app.MapPost("/inherit-only-returns", (InheritOnlyReturns returns) => { });
- app.MapPost("/inherit-all-but-remarks", (InheritAllButRemarks remarks) => { });
- app.MapPost("/generic-class", (GenericClass<string> generic) => { });
- app.MapPost("/generic-parent", (GenericParent parent) => { });
- app.MapPost("/params-and-param-refs", (ParamsAndParamRefs refs) => { });
- app.Run();
- /// <summary>
- /// Every class and member should have a one sentence
- /// summary describing its purpose.
- /// </summary>
- /// <remarks>
- /// You can expand on that one sentence summary to
- /// provide more information for readers. In this case,
- /// the <c>ExampleClass</c> provides different C#
- /// elements to show how you would add documentation
- ///comments for most elements in a typical class.
- /// <para>
- /// The remarks can add multiple paragraphs, so you can
- /// write detailed information for developers that use
- /// your work. You should add everything needed for
- /// readers to be successful. This class contains
- /// examples for the following:
- /// </para>
- /// <list type="table">
- /// <item>
- /// <term>Summary</term>
- /// <description>
- /// This should provide a one sentence summary of the class or member.
- /// </description>
- /// </item>
- /// <item>
- /// <term>Remarks</term>
- /// <description>
- /// This is typically a more detailed description of the class or member
- /// </description>
- /// </item>
- /// <item>
- /// <term>para</term>
- /// <description>
- /// The para tag separates a section into multiple paragraphs
- /// </description>
- /// </item>
- /// <item>
- /// <term>list</term>
- /// <description>
- /// Provides a list of terms or elements
- /// </description>
- /// </item>
- /// <item>
- /// <term>returns, param</term>
- /// <description>
- /// Used to describe parameters and return values
- /// </description>
- /// </item>
- /// <item>
- /// <term>value</term>
- /// <description>Used to describe properties</description>
- /// </item>
- /// <item>
- /// <term>exception</term>
- /// <description>
- /// Used to describe exceptions that may be thrown
- /// </description>
- /// </item>
- /// <item>
- /// <term>c, cref, see, seealso</term>
- /// <description>
- /// These provide code style and links to other
- /// documentation elements
- /// </description>
- /// </item>
- /// <item>
- /// <term>example, code</term>
- /// <description>
- /// These are used for code examples
- /// </description>
- /// </item>
- /// </list>
- /// <para>
- /// The list above uses the "table" style. You could
- /// also use the "bullet" or "number" style. Neither
- /// would typically use the "term" element.
- /// <br/>
- /// Note: paragraphs are double spaced. Use the *br*
- /// tag for single spaced lines.
- /// </para>
- /// </remarks>
- public class ExampleClass
- {
- /// <value>
- /// The <c>Label</c> property represents a label
- /// for this instance.
- /// </value>
- /// <remarks>
- /// The <see cref="Label"/> is a <see langword="string"/>
- /// that you use for a label.
- /// <para>
- /// Note that there isn't a way to provide a "cref" to
- /// each accessor, only to the property itself.
- /// </para>
- /// </remarks>
- public string? Label
- {
- get;
- set;
- }
- /// <summary>
- /// Adds two integers and returns the result.
- /// </summary>
- /// <returns>
- /// The sum of two integers.
- /// </returns>
- /// <param name="left">
- /// The left operand of the addition.
- /// </param>
- /// <param name="right">
- /// The right operand of the addition.
- /// </param>
- /// <example>
- /// <code>
- /// int c = Math.Add(4, 5);
- /// if (c > 10)
- /// {
- /// Console.WriteLine(c);
- /// }
- /// </code>
- /// </example>
- /// <exception cref="System.OverflowException">
- /// Thrown when one parameter is
- /// <see cref="Int32.MaxValue">MaxValue</see> and the other is
- /// greater than 0.
- /// Note that here you can also use
- /// <see href="https://learn.microsoft.com/dotnet/api/system.int32.maxvalue"/>
- /// to point a web page instead.
- /// </exception>
- /// <see cref="ExampleClass"/> for a list of all
- /// the tags in these examples.
- /// <seealso cref="ExampleClass.Label"/>
- public static int Add(int left, int right)
- {
- if ((left == int.MaxValue && right > 0) || (right == int.MaxValue && left > 0))
- throw new System.OverflowException();
- return left + right;
- }
- /// <summary>
- /// This method is an example of a method that
- /// returns an awaitable item.
- /// </summary>
- public static Task<int> AddAsync(int left, int right)
- {
- return Task.FromResult(Add(left, right));
- }
- /// <summary>
- /// This method is an example of a method that
- /// returns a Task which should map to a void return type.
- /// </summary>
- public static Task DoNothingAsync()
- {
- return Task.CompletedTask;
- }
- /// <summary>
- /// This method is an example of a method that consumes
- /// an params array.
- /// </summary>
- public static int AddNumbers(params int[] numbers)
- {
- var sum = 0;
- foreach (var number in numbers)
- {
- sum += number;
- }
- return sum;
- }
- }
- /// <summary>
- /// This is an example of a positional record.
- /// </summary>
- /// <remarks>
- /// There isn't a way to add XML comments for properties
- /// created for positional records, yet. The language
- /// design team is still considering what tags should
- /// be supported, and where. Currently, you can use
- /// the "param" tag to describe the parameters to the
- /// primary constructor.
- /// </remarks>
- /// <param name="FirstName">
- /// This tag will apply to the primary constructor parameter.
- /// </param>
- /// <param name="LastName">
- /// This tag will apply to the primary constructor parameter.
- /// </param>
- public record Person(string FirstName, string LastName);
- /// <summary>
- /// A summary about this class.
- /// </summary>
- /// <remarks>
- /// These remarks would explain more about this class.
- /// In this example, these comments also explain the
- /// general information about the derived class.
- /// </remarks>
- public class MainClass
- {
- }
- ///<inheritdoc/>
- public class DerivedClass : MainClass
- {
- }
- /// <summary>
- /// This interface would describe all the methods in
- /// its contract.
- /// </summary>
- /// <remarks>
- /// While elided for brevity, each method or property
- /// in this interface would contain docs that you want
- /// to duplicate in each implementing class.
- /// </remarks>
- public interface ITestInterface
- {
- /// <summary>
- /// This method is part of the test interface.
- /// </summary>
- /// <remarks>
- /// This content would be inherited by classes
- /// that implement this interface when the
- /// implementing class uses "inheritdoc"
- /// </remarks>
- /// <returns>The value of <paramref name="arg" /> </returns>
- /// <param name="arg">The argument to the method</param>
- int Method(int arg);
- }
- ///<inheritdoc cref="ITestInterface"/>
- public class ImplementingClass : ITestInterface
- {
- // doc comments are inherited here.
- public int Method(int arg) => arg;
- }
- /// <summary>
- /// This class shows hows you can "inherit" the doc
- /// comments from one method in another method.
- /// </summary>
- /// <remarks>
- /// You can inherit all comments, or only a specific tag,
- /// represented by an xpath expression.
- /// </remarks>
- public class InheritOnlyReturns
- {
- /// <summary>
- /// In this example, this summary is only visible for this method.
- /// </summary>
- /// <returns>A boolean</returns>
- public static bool MyParentMethod(bool x) { return x; }
- /// <inheritdoc cref="MyParentMethod" path="/returns"/>
- public static bool MyChildMethod() { return false; }
- }
- /// <summary>
- /// This class shows an example of sharing comments across methods.
- /// </summary>
- public class InheritAllButRemarks
- {
- /// <summary>
- /// In this example, this summary is visible on all the methods.
- /// </summary>
- /// <remarks>
- /// The remarks can be inherited by other methods
- /// using the xpath expression.
- /// </remarks>
- /// <returns>A boolean</returns>
- public static bool MyParentMethod(bool x) { return x; }
- /// <inheritdoc cref="MyParentMethod" path="//*[not(self::remarks)]"/>
- public static bool MyChildMethod() { return false; }
- }
- /// <summary>
- /// This is a generic class.
- /// </summary>
- /// <remarks>
- /// This example shows how to specify the <see cref="GenericClass{T}"/>
- /// type as a cref attribute.
- /// In generic classes and methods, you'll often want to reference the
- /// generic type, or the type parameter.
- /// </remarks>
- public class GenericClass<T>
- {
- // Fields and members.
- }
- /// <summary>
- /// This class validates the behavior for mapping
- /// generic types to open generics for use in
- /// typeof expressions.
- /// </summary>
- public class GenericParent
- {
- /// <summary>
- /// This property is a nullable value type.
- /// </summary>
- public int? Id { get; set; }
- /// <summary>
- /// This property is a nullable reference type.
- /// </summary>
- public string? Name { get; set; }
- /// <summary>
- /// This property is a generic type containing a tuple.
- /// </summary>
- public Task<(int, string)> TaskOfTupleProp { get; set; }
- /// <summary>
- /// This property is a tuple with a generic type inside.
- /// </summary>
- public (int, Dictionary<int, string>) TupleWithGenericProp { get; set; }
- /// <summary>
- /// This property is a tuple with a nested generic type inside.
- /// </summary>
- public (int, Dictionary<int, Dictionary<string, int>>) TupleWithNestedGenericProp { get; set; }
- /// <summary>
- /// This method returns a generic type containing a tuple.
- /// </summary>
- public static Task<(int, string)> GetTaskOfTuple()
- {
- return Task.FromResult((1, "test"));
- }
- /// <summary>
- /// This method returns a tuple with a generic type inside.
- /// </summary>
- public static (int, Dictionary<int, string>) GetTupleOfTask()
- {
- return (1, new Dictionary<int, string>());
- }
- /// <summary>
- /// This method return a tuple with a generic type containing a
- /// type parameter inside.
- /// </summary>
- public static (int, Dictionary<int, T>) GetTupleOfTask1<T>()
- {
- return (1, new Dictionary<int, T>());
- }
- /// <summary>
- /// This method return a tuple with a generic type containing a
- /// type parameter inside.
- /// </summary>
- public static (T, Dictionary<int, string>) GetTupleOfTask2<T>()
- {
- return (default, new Dictionary<int, string>());
- }
- /// <summary>
- /// This method returns a nested generic with all types resolved.
- /// </summary>
- public static Dictionary<int, Dictionary<int, string>> GetNestedGeneric()
- {
- return new Dictionary<int, Dictionary<int, string>>();
- }
- /// <summary>
- /// This method returns a nested generic with a type parameter.
- /// </summary>
- public static Dictionary<int, Dictionary<int, T>> GetNestedGeneric1<T>()
- {
- return new Dictionary<int, Dictionary<int, T>>();
- }
- }
- /// <summary>
- /// This shows examples of typeparamref and typeparam tags
- /// </summary>
- public class ParamsAndParamRefs
- {
- /// <summary>
- /// The GetGenericValue method.
- /// </summary>
- /// <remarks>
- /// This sample shows how to specify the <see cref="GetGenericValue"/>
- /// method as a cref attribute.
- /// The parameter and return value are both of an arbitrary type,
- /// <typeparamref name="T"/>
- /// </remarks>
- public static T GetGenericValue<T>(T para)
- {
- return para;
- }
- }
- """;
- var generator = new XmlCommentGenerator();
- await SnapshotTestHelper.Verify(source, generator, out var compilation);
- await SnapshotTestHelper.VerifyOpenApi(compilation, document =>
- {
- var path = document.Paths["/example-class"].Operations[OperationType.Post];
- var exampleClass = path.RequestBody.Content["application/json"].Schema;
- Assert.Equal("Every class and member should have a one sentence\nsummary describing its purpose.", exampleClass.Description, ignoreLineEndingDifferences: true);
- Assert.Equal("The `Label` property represents a label\nfor this instance.", exampleClass.Properties["label"].Description, ignoreLineEndingDifferences: true);
- path = document.Paths["/person"].Operations[OperationType.Post];
- var person = path.RequestBody.Content["application/json"].Schema;
- Assert.Equal("This is an example of a positional record.", person.Description);
- Assert.Equal("This tag will apply to the primary constructor parameter.", person.Properties["firstName"].Description);
- Assert.Equal("This tag will apply to the primary constructor parameter.", person.Properties["lastName"].Description);
- path = document.Paths["/derived-class"].Operations[OperationType.Post];
- var derivedClass = path.RequestBody.Content["application/json"].Schema;
- Assert.Equal("A summary about this class.", derivedClass.Description);
- path = document.Paths["/main-class"].Operations[OperationType.Post];
- var mainClass = path.RequestBody.Content["application/json"].Schema;
- Assert.Equal("A summary about this class.", mainClass.Description);
- path = document.Paths["/implementing-class"].Operations[OperationType.Post];
- var implementingClass = path.RequestBody.Content["application/json"].Schema;
- Assert.Equal("This interface would describe all the methods in\nits contract.", implementingClass.Description, ignoreLineEndingDifferences: true);
- path = document.Paths["/inherit-only-returns"].Operations[OperationType.Post];
- var inheritOnlyReturns = path.RequestBody.Content["application/json"].Schema;
- Assert.Equal("This class shows hows you can \"inherit\" the doc\ncomments from one method in another method.", inheritOnlyReturns.Description, ignoreLineEndingDifferences: true);
- path = document.Paths["/inherit-all-but-remarks"].Operations[OperationType.Post];
- var inheritAllButRemarks = path.RequestBody.Content["application/json"].Schema;
- Assert.Equal("This class shows an example of sharing comments across methods.", inheritAllButRemarks.Description);
- path = document.Paths["/generic-class"].Operations[OperationType.Post];
- var genericClass = path.RequestBody.Content["application/json"].Schema;
- Assert.Equal("This is a generic class.", genericClass.Description);
- path = document.Paths["/generic-parent"].Operations[OperationType.Post];
- var genericParent = path.RequestBody.Content["application/json"].Schema;
- Assert.Equal("This class validates the behavior for mapping\ngeneric types to open generics for use in\ntypeof expressions.", genericParent.Description, ignoreLineEndingDifferences: true);
- Assert.Equal("This property is a nullable value type.", genericParent.Properties["id"].Description);
- Assert.Equal("This property is a nullable reference type.", genericParent.Properties["name"].Description);
- Assert.Equal("This property is a generic type containing a tuple.", genericParent.Properties["taskOfTupleProp"].Description);
- Assert.Equal("This property is a tuple with a generic type inside.", genericParent.Properties["tupleWithGenericProp"].Description);
- Assert.Equal("This property is a tuple with a nested generic type inside.", genericParent.Properties["tupleWithNestedGenericProp"].Description);
- path = document.Paths["/params-and-param-refs"].Operations[OperationType.Post];
- var paramsAndParamRefs = path.RequestBody.Content["application/json"].Schema;
- Assert.Equal("This shows examples of typeparamref and typeparam tags", paramsAndParamRefs.Description);
- });
- }
- }
|