Skip to content

Commit afd6808

Browse files
committed
Allow inherited lists as reference types
1 parent 726d2f1 commit afd6808

File tree

14 files changed

+229
-20
lines changed

14 files changed

+229
-20
lines changed

src/Microsoft.Azure.WebJobs.Extensions.OpenApi.Core/DocumentHelper.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,8 @@ public Dictionary<string, OpenApiSchema> GetOpenApiSchemas(List<MethodInfo> elem
135135
var responses = elements.SelectMany(p => p.GetCustomAttributes<OpenApiResponseWithBodyAttribute>(inherit: false))
136136
.Select(p => p.BodyType);
137137
var types = requests.Union(responses)
138-
.Select(p => p.IsOpenApiArray() || p.IsOpenApiDictionary() ? p.GetOpenApiSubType() : p)
138+
.SelectMany(p => p.IsReferencedOpenApiArray() ? new[] { p, p.GetOpenApiSubType() } :
139+
p.IsOpenApiArray() || p.IsOpenApiDictionary() ? new [] { p.GetOpenApiSubType() } : new[] { p })
139140
.Distinct()
140141
.Where(p => !p.IsSimpleType())
141142
.Where(p => p.IsReferentialType())

src/Microsoft.Azure.WebJobs.Extensions.OpenApi.Core/Extensions/OpenApiParameterAttributeExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public static OpenApiParameter ToOpenApiParameter(this OpenApiParameterAttribute
5353
Schema = schema
5454
};
5555

56-
if (type.IsOpenApiArray())
56+
if (type.IsOpenApiArray() || type.IsReferencedOpenApiArray())
5757
{
5858
if (attribute.In == ParameterLocation.Path)
5959
{

src/Microsoft.Azure.WebJobs.Extensions.OpenApi.Core/Extensions/OpenApiPayloadAttributeExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public static OpenApiMediaType ToOpenApiMediaType<T>(this T attribute, NamingStr
5555
}
5656

5757
// For array and dictionary object, the reference has already been added by the visitor.
58-
if (type.IsReferentialType() && !type.IsOpenApiNullable() && !type.IsOpenApiArray() && !type.IsOpenApiDictionary())
58+
if (type.IsReferentialType() && !type.IsOpenApiNullable() && !type.IsOpenApiArray() && !type.IsReferencedOpenApiArray() && !type.IsOpenApiDictionary())
5959
{
6060
var reference = new OpenApiReference()
6161
{

src/Microsoft.Azure.WebJobs.Extensions.OpenApi.Core/Extensions/TypeExtensions.cs

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,21 @@ public static bool IsOpenApiArray(this Type type)
322322
}
323323

324324
return type.IsArrayType();
325+
}
326+
327+
/// <summary>
328+
/// Checks whether the given type is an array type that should be referenced or not, from the OpenAPI perspective.
329+
/// </summary>
330+
/// <param name="type"><see cref="Type"/> instance.</param>
331+
/// <returns>Returns <c>True</c>, if the type is identified as array; otherwise returns <c>False</c>.</returns>
332+
public static bool IsReferencedOpenApiArray(this Type type)
333+
{
334+
if (type.IsNullOrDefault())
335+
{
336+
return false;
337+
}
338+
339+
return type.IsInheritedArrayType();
325340
}
326341

327342
/// <summary>
@@ -460,9 +475,9 @@ public static Type GetUnderlyingType(this Type type)
460475
underlyingType = nullableUnderlyingType;
461476
}
462477

463-
if (type.IsOpenApiArray())
478+
if (type.IsOpenApiArray() || type.IsReferencedOpenApiArray())
464479
{
465-
underlyingType = type.GetElementType() ?? type.GetGenericArguments()[0];
480+
underlyingType = type.GetElementType() ?? type.GetArrayTypeGenericArgument();
466481
}
467482

468483
if (type.IsOpenApiDictionary())
@@ -559,9 +574,9 @@ public static Type GetOpenApiSubType(this Type type)
559574
return type.GetElementType();
560575
}
561576

562-
if (type.IsArrayType())
577+
if (type.IsArrayType() || type.IsInheritedArrayType())
563578
{
564-
return type.GetGenericArguments()[0];
579+
return type.GetArrayTypeGenericArgument();
565580
}
566581

567582
return null;
@@ -594,9 +609,9 @@ public static string GetOpenApiSubTypeName(this Type type, NamingStrategy naming
594609
return namingStrategy.GetPropertyName(name, hasSpecifiedName: false);
595610
}
596611

597-
if (type.IsArrayType())
612+
if (type.IsArrayType() || type.IsInheritedArrayType())
598613
{
599-
var name = type.GetGenericArguments()[0].Name;
614+
var name = type.GetArrayTypeGenericArgument().Name;
600615

601616
return namingStrategy.GetPropertyName(name, hasSpecifiedName: false);
602617
}
@@ -738,12 +753,40 @@ public static string GetTypeName(this Type type ,bool useFullName = false){
738753
var name = useFullName ? type.FullName : type.Name;
739754

740755
return name;
741-
}
756+
}
757+
758+
private static Type GetArrayTypeGenericArgument(this Type type)
759+
{
760+
if (type.Name.Equals("IEnumerable`1", StringComparison.InvariantCultureIgnoreCase) == true)
761+
{
762+
return type.GetGenericArguments()[0];
763+
}
742764

765+
return type.GetInterfaces()
766+
.Where(p => p.IsInterface)
767+
.Where(p => p.Name.Equals("IEnumerable`1", StringComparison.InvariantCultureIgnoreCase) == true)
768+
.FirstOrDefault()?
769+
.GetGenericArguments()[0];
770+
}
743771

744772
private static bool IsArrayType(this Type type)
745773
{
746774
var isArrayType = type.Name.Equals("String", StringComparison.InvariantCultureIgnoreCase) == false &&
775+
type.Namespace?.StartsWith("System") == true &&
776+
type.GetInterfaces()
777+
.Where(p => p.IsInterface)
778+
.Where(p => p.Name.Equals("IEnumerable", StringComparison.InvariantCultureIgnoreCase) == true)
779+
.Any() &&
780+
type.IsJObjectType() == false &&
781+
type.IsDictionaryType() == false;
782+
783+
return isArrayType;
784+
}
785+
786+
private static bool IsInheritedArrayType(this Type type)
787+
{
788+
var isArrayType = type.Name.Equals("String", StringComparison.InvariantCultureIgnoreCase) == false &&
789+
type.Namespace?.StartsWith("System") != true &&
747790
type.GetInterfaces()
748791
.Where(p => p.IsInterface)
749792
.Where(p => p.Name.Equals("IEnumerable", StringComparison.InvariantCultureIgnoreCase) == true)

src/Microsoft.Azure.WebJobs.Extensions.OpenApi.Core/Visitors/ListObjectTypeVisitor.cs

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public ListObjectTypeVisitor(VisitorCollection visitorCollection)
2626
/// <inheritdoc />
2727
public override bool IsVisitable(Type type)
2828
{
29-
var isVisitable = this.IsVisitable(type, TypeCode.Object) && type.IsOpenApiArray();
29+
var isVisitable = this.IsVisitable(type, TypeCode.Object) && (type.IsOpenApiArray() || type.IsReferencedOpenApiArray());
3030

3131
return isVisitable;
3232
}
@@ -98,6 +98,23 @@ public override void Visit(IAcceptor acceptor, KeyValuePair<string, Type> type,
9898
)
9999
.ToDictionary(p => p.Key, p => p.Value);
100100

101+
if (type.Value.IsReferencedOpenApiArray())
102+
{
103+
var reference = new OpenApiReference()
104+
{
105+
Type = ReferenceType.Schema,
106+
Id = type.Value.GetOpenApiReferenceId(isDictionary: false, isList: false, namingStrategy, useFullName)
107+
};
108+
109+
instance.Schemas[name].Title = type.Value.IsGenericType
110+
? namingStrategy.GetPropertyName(type.Value.GetTypeName(useFullName).Split('`').First(), hasSpecifiedName: false) + "_" +
111+
string.Join("_",
112+
type.Value.GenericTypeArguments.Select(a => namingStrategy.GetPropertyName(a.GetTypeName(useFullName), false)))
113+
: namingStrategy.GetPropertyName(type.Value.GetTypeName(useFullName), hasSpecifiedName: false);
114+
instance.Schemas[name].Type = "array";
115+
instance.Schemas[name].Reference = reference;
116+
}
117+
101118
if (!schemasToBeAdded.Any())
102119
{
103120
return;
@@ -125,6 +142,18 @@ public override bool IsParameterVisitable(Type type)
125142
/// <inheritdoc />
126143
public override OpenApiSchema ParameterVisit(Type type, NamingStrategy namingStrategy)
127144
{
145+
if (type.IsReferencedOpenApiArray())
146+
{
147+
return new OpenApiSchema()
148+
{
149+
Reference = new OpenApiReference()
150+
{
151+
Type = ReferenceType.Schema,
152+
Id = type.GetOpenApiReferenceId(isDictionary: false, isList: false, namingStrategy)
153+
}
154+
};
155+
}
156+
128157
var schema = this.ParameterVisit(dataType: "array", dataFormat: null);
129158

130159
var underlyingType = type.GetUnderlyingType();
@@ -146,8 +175,19 @@ public override bool IsPayloadVisitable(Type type)
146175
/// <inheritdoc />
147176
public override OpenApiSchema PayloadVisit(Type type, NamingStrategy namingStrategy, bool useFullName = false)
148177
{
149-
var schema = this.PayloadVisit(dataType: "array", dataFormat: null);
178+
if (type.IsReferencedOpenApiArray())
179+
{
180+
return new OpenApiSchema()
181+
{
182+
Reference = new OpenApiReference()
183+
{
184+
Type = ReferenceType.Schema,
185+
Id = type.GetOpenApiReferenceId(isDictionary: false, isList: false, namingStrategy, useFullName)
186+
}
187+
};
188+
}
150189

190+
var schema = this.PayloadVisit(dataType: "array", dataFormat: null);
151191
// Gets the schema for the underlying type.
152192
var underlyingType = type.GetUnderlyingType();
153193
var items = this.VisitorCollection.PayloadVisit(underlyingType, namingStrategy, useFullName);

src/Microsoft.Azure.WebJobs.Extensions.OpenApi.Core/Visitors/ObjectTypeVisitor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ private void ProcessProperties(IOpenApiSchemaAcceptor instance, string schemaNam
226226
// Adds schemas to the root.
227227
var schemasToBeAdded = subAcceptor.Schemas
228228
.Where(p => !instance.Schemas.Keys.Contains(p.Key))
229-
.Where(p => p.Value.IsOpenApiSchemaObject())
229+
.Where(p => p.Value.IsOpenApiSchemaObject() || (p.Value.IsOpenApiSchemaArray() && !p.Value.Title.IsNullOrWhiteSpace()))
230230
.GroupBy(p => p.Value.Title)
231231
.Select(p => p.First())
232232
.ToDictionary(p => p.Value.Title, p => p.Value);

test-integration/Microsoft.Azure.WebJobs.Extensions.OpenApi.Document.Tests/Get_ApplicationJson_ArrayObject_Tests.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,18 @@ public void Given_OpenApiDocument_Then_It_Should_Return_ComponentSchemaProperty(
8686
value.Value<string>("type").Should().Be(propertyType);
8787
}
8888

89+
[DataTestMethod]
90+
[DataRow("microsoft.Azure.WebJobs.Extensions.OpenApi.TestApp.Models.ArrayObjectModel", "object", "listStringObjectValue", "microsoft.Azure.WebJobs.Extensions.OpenApi.TestApp.Models.ListStringObjectModel")]
91+
public void Given_OpenApiDocument_Then_It_Should_Return_ComponentSchemaPropertyWithReference(string @ref, string refType, string propertyName, string reference)
92+
{
93+
var properties = this._doc["components"]["schemas"][@ref]["properties"];
94+
95+
var value = properties[propertyName];
96+
97+
value.Should().NotBeNull();
98+
value.Value<string>("$ref").Should().Be($"#/components/schemas/{reference}");
99+
}
100+
89101
[DataTestMethod]
90102
[DataRow("microsoft.Azure.WebJobs.Extensions.OpenApi.TestApp.Models.ArrayObjectModel", "object", "objectValue", "array", "object")]
91103
[DataRow("microsoft.Azure.WebJobs.Extensions.OpenApi.TestApp.Models.ArrayObjectModel", "object", "booleanValue", "array", "boolean")]

test-integration/Microsoft.Azure.WebJobs.Extensions.OpenApi.Document.Tests/Get_ApplicationJson_Array_Tests.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public async Task Init()
3232
[DataRow("/get-applicationjson-int-array")]
3333
[DataRow("/get-applicationjson-bool-array")]
3434
[DataRow("/get-applicationjson-int-list")]
35+
[DataRow("/get-applicationjson-named-list")]
3536
public void Given_OpenApiDocument_Then_It_Should_Return_Path(string path)
3637
{
3738
var paths = this._doc["paths"];
@@ -44,6 +45,7 @@ public void Given_OpenApiDocument_Then_It_Should_Return_Path(string path)
4445
[DataRow("/get-applicationjson-int-array", "get")]
4546
[DataRow("/get-applicationjson-bool-array", "get")]
4647
[DataRow("/get-applicationjson-int-list", "get")]
48+
[DataRow("/get-applicationjson-named-list", "get")]
4749
public void Given_OpenApiDocument_Then_It_Should_Return_OperationType(string path, string operationType)
4850
{
4951
var pathItem = this._doc["paths"][path];
@@ -56,6 +58,7 @@ public void Given_OpenApiDocument_Then_It_Should_Return_OperationType(string pat
5658
[DataRow("/get-applicationjson-int-array", "get", "200")]
5759
[DataRow("/get-applicationjson-bool-array", "get", "200")]
5860
[DataRow("/get-applicationjson-int-list", "get", "200")]
61+
[DataRow("/get-applicationjson-named-list", "get", "200")]
5962
public void Given_OpenApiDocument_Then_It_Should_Return_OperationResponse(string path, string operationType, string responseCode)
6063
{
6164
var responses = this._doc["paths"][path][operationType]["responses"];
@@ -68,6 +71,7 @@ public void Given_OpenApiDocument_Then_It_Should_Return_OperationResponse(string
6871
[DataRow("/get-applicationjson-int-array", "get", "200", "application/json")]
6972
[DataRow("/get-applicationjson-bool-array", "get", "200", "application/json")]
7073
[DataRow("/get-applicationjson-int-list", "get", "200", "application/json")]
74+
[DataRow("/get-applicationjson-named-list", "get", "200", "application/json")]
7175
public void Given_OpenApiDocument_Then_It_Should_Return_OperationResponseContentType(string path, string operationType, string responseCode, string contentType)
7276
{
7377
var content = this._doc["paths"][path][operationType]["responses"][responseCode]["content"];
@@ -89,6 +93,29 @@ public void Given_OpenApiDocument_Then_It_Should_Return_OperationResponseContent
8993
schema.Value<string>("type").Should().Be(dataType);
9094
}
9195

96+
[DataTestMethod]
97+
[DataRow("/get-applicationjson-named-list", "get", "200", "application/json", "microsoft.Azure.WebJobs.Extensions.OpenApi.TestApp.Models.ListStringObjectModel")]
98+
public void Given_OpenApiDocument_Then_It_Should_Return_OperationResponseContentTypeSchemaWithReference(string path, string operationType, string responseCode, string contentType, string reference)
99+
{
100+
var content = this._doc["paths"][path][operationType]["responses"][responseCode]["content"];
101+
102+
var @ref = content[contentType]["schema"]["$ref"];
103+
104+
@ref.Value<string>().Should().Be($"#/components/schemas/{reference}");
105+
}
106+
107+
[DataTestMethod]
108+
[DataRow("microsoft.Azure.WebJobs.Extensions.OpenApi.TestApp.Models.ListStringObjectModel", "array")]
109+
public void Given_OpenApiDocument_Then_It_Should_Return_ComponentSchemaProperty(string reference, string referenceType)
110+
{
111+
var properties = this._doc["components"]["schemas"][reference];
112+
113+
var type = properties["type"];
114+
115+
type.Should().NotBeNull();
116+
type.Value<string>().Should().Be(referenceType);
117+
}
118+
92119
[DataTestMethod]
93120
[DataRow("/get-applicationjson-int-array", "get", "200", "application/json", "array", "integer", "int32")]
94121
[DataRow("/get-applicationjson-int-list", "get", "200", "application/json", "array", "integer", "int32")]
@@ -113,5 +140,13 @@ public void Given_OpenApiDocument_Then_It_Should_Return_OperationResponseContent
113140

114141
items.Value<string>("type").Should().Be(itemType);
115142
}
143+
144+
[DataTestMethod]
145+
[DataRow("microsoft.Azure.WebJobs.Extensions.OpenApi.TestApp.Models.ListStringObjectModel", "microsoft.Azure.WebJobs.Extensions.OpenApi.TestApp.Models.StringObjectModel")]
146+
public void Given_OpenApiDocument_Then_It_Should_Return_ComponentSchemaPropertyItems(string reference, string itemRef)
147+
{
148+
var items = this._doc["components"]["schemas"][reference]["items"];
149+
items.Value<string>("$ref").Should().Be($"#/components/schemas/{itemRef}");
150+
}
116151
}
117152
}

test-integration/Microsoft.Azure.WebJobs.Extensions.OpenApi.TestApp/Get_ApplicationJson_Array_HttpTrigger.cs

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
using System.Collections;
12
using System.Collections.Generic;
3+
using System.Linq;
24
using System.Net;
35
using System.Threading.Tasks;
46

@@ -14,15 +16,15 @@ namespace Microsoft.Azure.WebJobs.Extensions.OpenApi.TestApp
1416
{
1517
public static class Get_ApplicationJson_Array_HttpTrigger
1618
{
17-
19+
1820
[FunctionName(nameof(Get_ApplicationJson_Array_HttpTrigger.Get_ApplicationJson_StringArray))]
1921
[OpenApiOperation(operationId: nameof(Get_ApplicationJson_Array_HttpTrigger.Get_ApplicationJson_StringArray), tags: new[] { "array" })]
2022
[OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(string[]), Description = "The OK response")]
2123
public static async Task<IActionResult> Get_ApplicationJson_StringArray(
2224
[HttpTrigger(AuthorizationLevel.Anonymous, "GET", Route = "get-applicationjson-string-array")] HttpRequest req,
2325
ILogger log)
2426
{
25-
var result = new OkResult();
27+
var result = new OkResult();
2628

2729
return await Task.FromResult(result).ConfigureAwait(false);
2830
}
@@ -34,7 +36,7 @@ public static async Task<IActionResult> Get_ApplicationJson_IntArray(
3436
[HttpTrigger(AuthorizationLevel.Anonymous, "GET", Route = "get-applicationjson-int-array")] HttpRequest req,
3537
ILogger log)
3638
{
37-
var result = new OkResult();
39+
var result = new OkResult();
3840

3941
return await Task.FromResult(result).ConfigureAwait(false);
4042
}
@@ -46,7 +48,7 @@ public static async Task<IActionResult> Get_ApplicationJson_BoolArray(
4648
[HttpTrigger(AuthorizationLevel.Anonymous, "GET", Route = "get-applicationjson-bool-array")] HttpRequest req,
4749
ILogger log)
4850
{
49-
var result = new OkResult();
51+
var result = new OkResult();
5052

5153
return await Task.FromResult(result).ConfigureAwait(false);
5254
}
@@ -58,9 +60,21 @@ public static async Task<IActionResult> Get_ApplicationJson_IntList(
5860
[HttpTrigger(AuthorizationLevel.Anonymous, "GET", Route = "get-applicationjson-int-list")] HttpRequest req,
5961
ILogger log)
6062
{
61-
var result = new OkResult();
63+
var result = new OkResult();
64+
65+
return await Task.FromResult(result).ConfigureAwait(false);
66+
}
67+
68+
[FunctionName(nameof(Get_ApplicationJson_Array_HttpTrigger.Get_ApplicationJson_NamedList))]
69+
[OpenApiOperation(operationId: nameof(Get_ApplicationJson_Array_HttpTrigger.Get_ApplicationJson_NamedList), tags: new[] { "array" })]
70+
[OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(ListStringObjectModel), Description = "The OK response")]
71+
public static async Task<IActionResult> Get_ApplicationJson_NamedList(
72+
[HttpTrigger(AuthorizationLevel.Anonymous, "GET", Route = "get-applicationjson-named-list")] HttpRequest req,
73+
ILogger log)
74+
{
75+
var result = new OkResult();
6276

6377
return await Task.FromResult(result).ConfigureAwait(false);
6478
}
6579
}
66-
}
80+
}

test-integration/Microsoft.Azure.WebJobs.Extensions.OpenApi.TestApp/Models/ArrayObjectModel.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public class ArrayObjectModel
1212
public IReadOnlyCollection<float> FloatValue { get; set; }
1313
public HashSet<decimal> DecimalValue { get; set; }
1414
public ISet<StringObjectModel> StringObjectValue { get; set; }
15-
1615
public List<object[]> ObjectArrayValue { get; set; }
16+
public ListStringObjectModel ListStringObjectValue { get; set; }
1717
}
1818
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using System.Collections;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
5+
namespace Microsoft.Azure.WebJobs.Extensions.OpenApi.TestApp.Models;
6+
7+
public class ListStringObjectModel : IEnumerable<StringObjectModel>
8+
{
9+
public IEnumerator<StringObjectModel> GetEnumerator() => Enumerable.Empty<StringObjectModel>().GetEnumerator();
10+
11+
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
12+
}

0 commit comments

Comments
 (0)