Skip to content

Commit ef24fd0

Browse files
Support <example> tags. Fixes #1170
1 parent b72a54e commit ef24fd0

11 files changed

Lines changed: 253 additions & 27 deletions

File tree

src/AspNetCore/WebApi/src/Asp.Versioning.OpenApi/Asp.Versioning.OpenApi.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
</ItemGroup>
1717

1818
<ItemGroup>
19+
<PackageReference Include="Microsoft.OpenApi" Version="2.0.0" />
1920
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.*" />
2021
</ItemGroup>
2122

src/AspNetCore/WebApi/src/Asp.Versioning.OpenApi/Transformers/XmlComments.cs

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,40 +42,48 @@ public class XmlComments
4242
/// Gets the <c>summary</c> from the specified member, if any.
4343
/// </summary>
4444
/// <param name="member">The member to get the summary from.</param>
45-
/// <returns>The corresponding summary or an empty string.</returns>
45+
/// <returns>The corresponding <c>&lt;summary&gt;</c> or an empty string.</returns>
4646
public string GetSummary( MemberInfo member )
4747
=> GetMember( member )?.Element( "summary" )?.Value.Trim() ?? string.Empty;
4848

4949
/// <summary>
5050
/// Gets the <c>description</c> from the specified member, if any.
5151
/// </summary>
5252
/// <param name="member">The member to get the description from.</param>
53-
/// <returns>The corresponding description or an empty string.</returns>
53+
/// <returns>The corresponding <c>&lt;description&gt;</c> or an empty string.</returns>
5454
public string GetDescription( MemberInfo member )
5555
=> GetMember( member )?.Element( "description" )?.Value.Trim() ?? string.Empty;
5656

5757
/// <summary>
5858
/// Gets the <c>remarks</c> from the specified member, if any.
5959
/// </summary>
6060
/// <param name="member">The member to get the remarks from.</param>
61-
/// <returns>The corresponding remarks or an empty string.</returns>
61+
/// <returns>The corresponding <c>&lt;remarks&gt;</c> or an empty string.</returns>
6262
public string GetRemarks( MemberInfo member )
6363
=> GetMember( member )?.Element( "remarks" )?.Value.Trim() ?? string.Empty;
6464

6565
/// <summary>
6666
/// Gets the <c>returns</c> from the specified member, if any.
6767
/// </summary>
6868
/// <param name="member">The member to get the returns from.</param>
69-
/// <returns>The corresponding returns or an empty string.</returns>
69+
/// <returns>The corresponding <c>&lt;returns&gt;</c> or an empty string.</returns>
7070
public string GetReturns( MemberInfo member )
7171
=> GetMember( member )?.Element( "returns" )?.Value.Trim() ?? string.Empty;
7272

73+
/// <summary>
74+
/// Gets the <c>example</c> from the specified member, if any.
75+
/// </summary>
76+
/// <param name="member">The member to get the example from.</param>
77+
/// <returns>The corresponding <c>&lt;example&gt;</c> or an empty string.</returns>
78+
public string GetExample( MemberInfo member )
79+
=> GetMember( member )?.Element( "example" )?.Value.Trim() ?? string.Empty;
80+
7381
/// <summary>
7482
/// Gets the <c>param</c> description from the specified member, if any.
7583
/// </summary>
7684
/// <param name="member">The member to get the parameter from.</param>
7785
/// <param name="name">The name of the parameter.</param>
78-
/// <returns>The corresponding returns or an empty string.</returns>
86+
/// <returns>The corresponding description or an empty string.</returns>
7987
public string GetParameterDescription( MemberInfo member, string name )
8088
{
8189
if ( GetMember( member ) is { } element )
@@ -89,6 +97,50 @@ public string GetParameterDescription( MemberInfo member, string name )
8997
return string.Empty;
9098
}
9199

100+
/// <summary>
101+
/// Gets the parameter <c>example</c> from the specified member, if any.
102+
/// </summary>
103+
/// <param name="member">The member to get the parameter from.</param>
104+
/// <param name="name">The name of the parameter.</param>
105+
/// <returns>The corresponding <c>&lt;example&gt;</c> or an empty string.</returns>
106+
public string GetParameterExample( MemberInfo member, string name )
107+
{
108+
if ( GetMember( member ) is { } element )
109+
{
110+
return element.Elements( "param" )
111+
.FirstOrDefault( x => x.Attribute( "name" )?.Value == name )?
112+
.Attribute( "example" )?
113+
.Value
114+
.Trim() ?? string.Empty;
115+
}
116+
117+
return string.Empty;
118+
}
119+
120+
/// <summary>
121+
/// Gets the <c>deprecated</c> attribute from the specified member, if any.
122+
/// </summary>
123+
/// <param name="member">The member to get the parameter from.</param>
124+
/// <param name="name">The name of the parameter.</param>
125+
/// <returns><c>true</c> if the <c>deprecated</c> attribute is present with a value of <c>"true"</c>;
126+
/// otherwise <c>false</c>.</returns>
127+
public bool IsParameterDeprecated( MemberInfo member, string name )
128+
{
129+
if ( GetMember( member ) is { } element )
130+
{
131+
var deprecated = element.Elements( "param" )
132+
.FirstOrDefault( x => x.Attribute( "name" )?.Value == name )?
133+
.Attribute( "deprecated" )?.Value;
134+
135+
if ( deprecated is { } value )
136+
{
137+
return StringComparer.OrdinalIgnoreCase.Equals( value, bool.TrueString );
138+
}
139+
}
140+
141+
return false;
142+
}
143+
92144
/// <summary>
93145
/// Gets the <c>response</c> description from the specified member, if any.
94146
/// </summary>

src/AspNetCore/WebApi/src/Asp.Versioning.OpenApi/Transformers/XmlCommentsTransformer.cs

Lines changed: 82 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ namespace Asp.Versioning.OpenApi.Transformers;
88
using Microsoft.OpenApi;
99
using System;
1010
using System.Reflection;
11+
using System.Text.Json;
12+
using System.Text.Json.Nodes;
1113
using System.Threading;
1214
using static System.Reflection.BindingFlags;
1315

@@ -45,24 +47,46 @@ public virtual Task TransformAsync(
4547
ArgumentNullException.ThrowIfNull( schema );
4648
ArgumentNullException.ThrowIfNull( context );
4749

48-
if ( schema.Properties is not { } properties
49-
|| context.JsonTypeInfo?.Type is not Type type )
50+
if ( context.JsonTypeInfo?.Type is not Type type )
5051
{
5152
return Task.CompletedTask;
5253
}
5354

54-
if ( string.IsNullOrEmpty( schema.Description ) )
55+
var description = schema.Description;
56+
57+
if ( string.IsNullOrEmpty( description )
58+
&& !string.IsNullOrEmpty( description = Documentation.GetSummary( type ) ) )
5559
{
56-
schema.Description = Documentation.GetSummary( type );
60+
schema.Description = description;
61+
}
62+
63+
if ( schema.Example is null && ToJson( Documentation.GetExample( type ) ) is { } example )
64+
{
65+
schema.Example = example;
66+
}
67+
68+
if ( schema.Properties is not { } properties )
69+
{
70+
return Task.CompletedTask;
5771
}
5872

5973
foreach ( var (name, prop) in properties )
6074
{
6175
if ( prop is not null
62-
&& string.IsNullOrEmpty( prop.Description )
63-
&& type.GetProperty( name, IgnoreCase | Instance | Public ) is { } property )
76+
&& type.GetProperty( name, IgnoreCase | Instance | Public ) is { } property )
6477
{
65-
prop.Description = Documentation.GetSummary( property );
78+
if ( string.IsNullOrEmpty( prop.Description )
79+
&& !string.IsNullOrEmpty( description = Documentation.GetSummary( property ) ) )
80+
{
81+
prop.Description = description;
82+
}
83+
84+
if ( prop.Example is null
85+
&& prop.Examples is not null
86+
&& ( example = ToJson( Documentation.GetExample( property ) ) ) is not null )
87+
{
88+
prop.Examples.Add( example );
89+
}
6690
}
6791
}
6892

@@ -88,16 +112,19 @@ public virtual Task TransformAsync(
88112
operation.Summary = Documentation.GetSummary( method );
89113
}
90114

91-
if ( string.IsNullOrEmpty( operation.Description ) )
115+
var description = operation.Description;
116+
117+
if ( string.IsNullOrEmpty( description )
118+
&& !string.IsNullOrEmpty( description = Documentation.GetDescription( method ) ) )
92119
{
93-
operation.Description = Documentation.GetDescription( method );
120+
operation.Description = description;
94121
}
95122

96123
if ( operation.Responses is { } responses )
97124
{
98125
foreach ( var (statusCode, response) in responses )
99126
{
100-
var description = Documentation.GetResponseDescription( method, statusCode );
127+
description = Documentation.GetResponseDescription( method, statusCode );
101128

102129
if ( !string.IsNullOrEmpty( description ) )
103130
{
@@ -118,18 +145,40 @@ public virtual Task TransformAsync(
118145
{
119146
var parameter = parameters[i];
120147

121-
if ( !string.IsNullOrEmpty( parameter.Name ) && string.IsNullOrEmpty( parameter.Description ) )
148+
if ( string.IsNullOrEmpty( parameter.Name ) )
122149
{
123-
for ( var j = 0; j < args.Count; j++ )
150+
continue;
151+
}
152+
153+
for ( var j = 0; j < args.Count; j++ )
154+
{
155+
var arg = args[j];
156+
157+
if ( arg.Name != parameter.Name )
124158
{
125-
var arg = args[i];
159+
continue;
160+
}
161+
162+
var name = arg.ParameterDescriptor.Name;
163+
164+
if ( string.IsNullOrEmpty( parameter.Description )
165+
&& !string.IsNullOrEmpty( description = Documentation.GetParameterDescription( method, name ) ) )
166+
{
167+
parameter.Description = description;
168+
}
126169

127-
if ( arg.Name == parameter.Name )
170+
if ( parameter is OpenApiParameter param )
171+
{
172+
if ( param.Example is null
173+
&& ToJson( Documentation.GetParameterExample( method, name ) ) is { } example )
128174
{
129-
var name = arg.ParameterDescriptor.Name;
130-
parameter.Description = Documentation.GetParameterDescription( method, name );
175+
param.Example = example;
131176
}
177+
178+
param.Deprecated |= Documentation.IsParameterDeprecated( method, name );
132179
}
180+
181+
break;
133182
}
134183
}
135184

@@ -159,4 +208,21 @@ private static bool TryResolveMethod( ActionDescriptor action, [MaybeNullWhen( f
159208
method = default;
160209
return false;
161210
}
211+
212+
private static JsonNode? ToJson( string? example )
213+
{
214+
if ( string.IsNullOrEmpty( example ) )
215+
{
216+
return default;
217+
}
218+
219+
try
220+
{
221+
return JsonNode.Parse( example );
222+
}
223+
catch ( JsonException )
224+
{
225+
return JsonNode.Parse( $"\"{example}\"" );
226+
}
227+
}
162228
}

src/AspNetCore/WebApi/test/Asp.Versioning.OpenApi.Tests/Content/v1-minimal.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828
"pattern": "^-?(?:0|[1-9]\\d*)$",
2929
"type": "integer",
3030
"format": "int32"
31-
}
31+
},
32+
"example": 42
3233
},
3334
{
3435
"name": "api-version",

src/AspNetCore/WebApi/test/Asp.Versioning.OpenApi.Tests/Content/v1-mixed.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828
"pattern": "^-?(?:0|[1-9]\\d*)$",
2929
"type": "integer",
3030
"format": "int32"
31-
}
31+
},
32+
"example": 42
3233
},
3334
{
3435
"name": "api-version",
@@ -82,7 +83,8 @@
8283
"string"
8384
],
8485
"format": "int32"
85-
}
86+
},
87+
"example": 42
8688
},
8789
{
8890
"name": "api-version",

src/AspNetCore/WebApi/test/Asp.Versioning.OpenApi.Tests/Content/v1.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@
3030
"string"
3131
],
3232
"format": "int32"
33-
}
33+
},
34+
"example": 42
3435
},
3536
{
3637
"name": "api-version",

src/AspNetCore/WebApi/test/Asp.Versioning.OpenApi.Tests/Simulators/MinimalApi.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ public static class MinimalApi
1010
/// Test
1111
/// </summary>
1212
/// <description>A test API.</description>
13-
/// <param name="id">A test parameter.</param>
13+
/// <param name="id" example="42">A test parameter.</param>
1414
/// <returns>The original identifier.</returns>
1515
/// <response code="200">Pass</response>
1616
/// <response code="400">Fail</response>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
3+
#pragma warning disable SA1629
4+
5+
namespace Asp.Versioning.OpenApi.Simulators;
6+
7+
/// <summary>
8+
/// Represents a model.
9+
/// </summary>
10+
public class Model
11+
{
12+
/// <summary>
13+
/// Gets or sets the user associated with the model.
14+
/// </summary>
15+
/// <example>
16+
/// {
17+
/// "userName": "John Doe",
18+
/// "email": "john.doe@example.com"
19+
/// }
20+
/// </example>
21+
public User User { get; set; }
22+
}

src/AspNetCore/WebApi/test/Asp.Versioning.OpenApi.Tests/Simulators/TestController.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public class TestController : ControllerBase
1616
/// Test
1717
/// </summary>
1818
/// <description>A test API.</description>
19-
/// <param name="id">A test parameter.</param>
19+
/// <param name="id" example="42">A test parameter.</param>
2020
/// <returns>The original identifier.</returns>
2121
/// <response code="200">Pass</response>
2222
/// <response code="400">Fail</response>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
3+
#pragma warning disable SA1629
4+
5+
namespace Asp.Versioning.OpenApi.Simulators;
6+
7+
/// <summary>
8+
/// Represents a user.
9+
/// </summary>
10+
public class User
11+
{
12+
/// <summary>
13+
/// Gets or sets the username associated with the account.
14+
/// </summary>
15+
/// <example>John Doe</example>
16+
public string UserName { get; set; }
17+
18+
/// <summary>
19+
/// Gets or sets the email address associated with the user.
20+
/// </summary>
21+
/// <example>user@example.com</example>
22+
public string Email { get; set; }
23+
}

0 commit comments

Comments
 (0)