Skip to content

Allow inherited lists as reference types #642

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

Open
wants to merge 1 commit into
base: v2
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,8 @@ public Dictionary<string, OpenApiSchema> GetOpenApiSchemas(List<MethodInfo> elem
var responses = elements.SelectMany(p => p.GetCustomAttributes<OpenApiResponseWithBodyAttribute>(inherit: false))
.Select(p => p.BodyType);
var types = requests.Union(responses)
.Select(p => p.IsOpenApiArray() || p.IsOpenApiDictionary() ? p.GetOpenApiSubType() : p)
.SelectMany(p => p.IsReferencedOpenApiArray() ? new[] { p, p.GetOpenApiSubType() } :
p.IsOpenApiArray() || p.IsOpenApiDictionary() ? new [] { p.GetOpenApiSubType() } : new[] { p })
.Distinct()
.Where(p => !p.IsSimpleType())
.Where(p => p.IsReferentialType())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public static OpenApiParameter ToOpenApiParameter(this OpenApiParameterAttribute
Schema = schema
};

if (type.IsOpenApiArray())
if (type.IsOpenApiArray() || type.IsReferencedOpenApiArray())
{
if (attribute.In == ParameterLocation.Path)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public static OpenApiMediaType ToOpenApiMediaType<T>(this T attribute, NamingStr
}

// For array and dictionary object, the reference has already been added by the visitor.
if (type.IsReferentialType() && !type.IsOpenApiNullable() && !type.IsOpenApiArray() && !type.IsOpenApiDictionary())
if (type.IsReferentialType() && !type.IsOpenApiNullable() && !type.IsOpenApiArray() && !type.IsReferencedOpenApiArray() && !type.IsOpenApiDictionary())
{
var reference = new OpenApiReference()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,21 @@ public static bool IsOpenApiArray(this Type type)
}

return type.IsArrayType();
}

/// <summary>
/// Checks whether the given type is an array type that should be referenced or not, from the OpenAPI perspective.
/// </summary>
/// <param name="type"><see cref="Type"/> instance.</param>
/// <returns>Returns <c>True</c>, if the type is identified as array; otherwise returns <c>False</c>.</returns>
public static bool IsReferencedOpenApiArray(this Type type)
{
if (type.IsNullOrDefault())
{
return false;
}

return type.IsInheritedArrayType();
}

/// <summary>
Expand Down Expand Up @@ -460,9 +475,9 @@ public static Type GetUnderlyingType(this Type type)
underlyingType = nullableUnderlyingType;
}

if (type.IsOpenApiArray())
if (type.IsOpenApiArray() || type.IsReferencedOpenApiArray())
{
underlyingType = type.GetElementType() ?? type.GetGenericArguments()[0];
underlyingType = type.GetElementType() ?? type.GetArrayTypeGenericArgument();
}

if (type.IsOpenApiDictionary())
Expand Down Expand Up @@ -559,9 +574,9 @@ public static Type GetOpenApiSubType(this Type type)
return type.GetElementType();
}

if (type.IsArrayType())
if (type.IsArrayType() || type.IsInheritedArrayType())
{
return type.GetGenericArguments()[0];
return type.GetArrayTypeGenericArgument();
}

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

if (type.IsArrayType())
if (type.IsArrayType() || type.IsInheritedArrayType())
{
var name = type.GetGenericArguments()[0].Name;
var name = type.GetArrayTypeGenericArgument().Name;

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

return name;
}
}

private static Type GetArrayTypeGenericArgument(this Type type)
{
if (type.Name.Equals("IEnumerable`1", StringComparison.InvariantCultureIgnoreCase) == true)
{
return type.GetGenericArguments()[0];
}

return type.GetInterfaces()
.Where(p => p.IsInterface)
.Where(p => p.Name.Equals("IEnumerable`1", StringComparison.InvariantCultureIgnoreCase) == true)
.FirstOrDefault()?
.GetGenericArguments()[0];
}

private static bool IsArrayType(this Type type)
{
var isArrayType = type.Name.Equals("String", StringComparison.InvariantCultureIgnoreCase) == false &&
type.Namespace?.StartsWith("System") == true &&
type.GetInterfaces()
.Where(p => p.IsInterface)
.Where(p => p.Name.Equals("IEnumerable", StringComparison.InvariantCultureIgnoreCase) == true)
.Any() &&
type.IsJObjectType() == false &&
type.IsDictionaryType() == false;

return isArrayType;
}

private static bool IsInheritedArrayType(this Type type)
{
var isArrayType = type.Name.Equals("String", StringComparison.InvariantCultureIgnoreCase) == false &&
type.Namespace?.StartsWith("System") != true &&
type.GetInterfaces()
.Where(p => p.IsInterface)
.Where(p => p.Name.Equals("IEnumerable", StringComparison.InvariantCultureIgnoreCase) == true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public ListObjectTypeVisitor(VisitorCollection visitorCollection)
/// <inheritdoc />
public override bool IsVisitable(Type type)
{
var isVisitable = this.IsVisitable(type, TypeCode.Object) && type.IsOpenApiArray();
var isVisitable = this.IsVisitable(type, TypeCode.Object) && (type.IsOpenApiArray() || type.IsReferencedOpenApiArray());

return isVisitable;
}
Expand Down Expand Up @@ -98,6 +98,23 @@ public override void Visit(IAcceptor acceptor, KeyValuePair<string, Type> type,
)
.ToDictionary(p => p.Key, p => p.Value);

if (type.Value.IsReferencedOpenApiArray())
{
var reference = new OpenApiReference()
{
Type = ReferenceType.Schema,
Id = type.Value.GetOpenApiReferenceId(isDictionary: false, isList: false, namingStrategy, useFullName)
};

instance.Schemas[name].Title = type.Value.IsGenericType
? namingStrategy.GetPropertyName(type.Value.GetTypeName(useFullName).Split('`').First(), hasSpecifiedName: false) + "_" +
string.Join("_",
type.Value.GenericTypeArguments.Select(a => namingStrategy.GetPropertyName(a.GetTypeName(useFullName), false)))
: namingStrategy.GetPropertyName(type.Value.GetTypeName(useFullName), hasSpecifiedName: false);
instance.Schemas[name].Type = "array";
instance.Schemas[name].Reference = reference;
}

if (!schemasToBeAdded.Any())
{
return;
Expand Down Expand Up @@ -125,6 +142,18 @@ public override bool IsParameterVisitable(Type type)
/// <inheritdoc />
public override OpenApiSchema ParameterVisit(Type type, NamingStrategy namingStrategy)
{
if (type.IsReferencedOpenApiArray())
{
return new OpenApiSchema()
{
Reference = new OpenApiReference()
{
Type = ReferenceType.Schema,
Id = type.GetOpenApiReferenceId(isDictionary: false, isList: false, namingStrategy)
}
};
}

var schema = this.ParameterVisit(dataType: "array", dataFormat: null);

var underlyingType = type.GetUnderlyingType();
Expand All @@ -146,8 +175,19 @@ public override bool IsPayloadVisitable(Type type)
/// <inheritdoc />
public override OpenApiSchema PayloadVisit(Type type, NamingStrategy namingStrategy, bool useFullName = false)
{
var schema = this.PayloadVisit(dataType: "array", dataFormat: null);
if (type.IsReferencedOpenApiArray())
{
return new OpenApiSchema()
{
Reference = new OpenApiReference()
{
Type = ReferenceType.Schema,
Id = type.GetOpenApiReferenceId(isDictionary: false, isList: false, namingStrategy, useFullName)
}
};
}

var schema = this.PayloadVisit(dataType: "array", dataFormat: null);
// Gets the schema for the underlying type.
var underlyingType = type.GetUnderlyingType();
var items = this.VisitorCollection.PayloadVisit(underlyingType, namingStrategy, useFullName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ private void ProcessProperties(IOpenApiSchemaAcceptor instance, string schemaNam
// Adds schemas to the root.
var schemasToBeAdded = subAcceptor.Schemas
.Where(p => !instance.Schemas.Keys.Contains(p.Key))
.Where(p => p.Value.IsOpenApiSchemaObject())
.Where(p => p.Value.IsOpenApiSchemaObject() || (p.Value.IsOpenApiSchemaArray() && !p.Value.Title.IsNullOrWhiteSpace()))
.GroupBy(p => p.Value.Title)
.Select(p => p.First())
.ToDictionary(p => p.Value.Title, p => p.Value);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,18 @@ public void Given_OpenApiDocument_Then_It_Should_Return_ComponentSchemaProperty(
value.Value<string>("type").Should().Be(propertyType);
}

[DataTestMethod]
[DataRow("microsoft.Azure.WebJobs.Extensions.OpenApi.TestApp.Models.ArrayObjectModel", "object", "listStringObjectValue", "microsoft.Azure.WebJobs.Extensions.OpenApi.TestApp.Models.ListStringObjectModel")]
public void Given_OpenApiDocument_Then_It_Should_Return_ComponentSchemaPropertyWithReference(string @ref, string refType, string propertyName, string reference)
{
var properties = this._doc["components"]["schemas"][@ref]["properties"];

var value = properties[propertyName];

value.Should().NotBeNull();
value.Value<string>("$ref").Should().Be($"#/components/schemas/{reference}");
}

[DataTestMethod]
[DataRow("microsoft.Azure.WebJobs.Extensions.OpenApi.TestApp.Models.ArrayObjectModel", "object", "objectValue", "array", "object")]
[DataRow("microsoft.Azure.WebJobs.Extensions.OpenApi.TestApp.Models.ArrayObjectModel", "object", "booleanValue", "array", "boolean")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public async Task Init()
[DataRow("/get-applicationjson-int-array")]
[DataRow("/get-applicationjson-bool-array")]
[DataRow("/get-applicationjson-int-list")]
[DataRow("/get-applicationjson-named-list")]
public void Given_OpenApiDocument_Then_It_Should_Return_Path(string path)
{
var paths = this._doc["paths"];
Expand All @@ -44,6 +45,7 @@ public void Given_OpenApiDocument_Then_It_Should_Return_Path(string path)
[DataRow("/get-applicationjson-int-array", "get")]
[DataRow("/get-applicationjson-bool-array", "get")]
[DataRow("/get-applicationjson-int-list", "get")]
[DataRow("/get-applicationjson-named-list", "get")]
public void Given_OpenApiDocument_Then_It_Should_Return_OperationType(string path, string operationType)
{
var pathItem = this._doc["paths"][path];
Expand All @@ -56,6 +58,7 @@ public void Given_OpenApiDocument_Then_It_Should_Return_OperationType(string pat
[DataRow("/get-applicationjson-int-array", "get", "200")]
[DataRow("/get-applicationjson-bool-array", "get", "200")]
[DataRow("/get-applicationjson-int-list", "get", "200")]
[DataRow("/get-applicationjson-named-list", "get", "200")]
public void Given_OpenApiDocument_Then_It_Should_Return_OperationResponse(string path, string operationType, string responseCode)
{
var responses = this._doc["paths"][path][operationType]["responses"];
Expand All @@ -68,6 +71,7 @@ public void Given_OpenApiDocument_Then_It_Should_Return_OperationResponse(string
[DataRow("/get-applicationjson-int-array", "get", "200", "application/json")]
[DataRow("/get-applicationjson-bool-array", "get", "200", "application/json")]
[DataRow("/get-applicationjson-int-list", "get", "200", "application/json")]
[DataRow("/get-applicationjson-named-list", "get", "200", "application/json")]
public void Given_OpenApiDocument_Then_It_Should_Return_OperationResponseContentType(string path, string operationType, string responseCode, string contentType)
{
var content = this._doc["paths"][path][operationType]["responses"][responseCode]["content"];
Expand All @@ -89,6 +93,29 @@ public void Given_OpenApiDocument_Then_It_Should_Return_OperationResponseContent
schema.Value<string>("type").Should().Be(dataType);
}

[DataTestMethod]
[DataRow("/get-applicationjson-named-list", "get", "200", "application/json", "microsoft.Azure.WebJobs.Extensions.OpenApi.TestApp.Models.ListStringObjectModel")]
public void Given_OpenApiDocument_Then_It_Should_Return_OperationResponseContentTypeSchemaWithReference(string path, string operationType, string responseCode, string contentType, string reference)
{
var content = this._doc["paths"][path][operationType]["responses"][responseCode]["content"];

var @ref = content[contentType]["schema"]["$ref"];

@ref.Value<string>().Should().Be($"#/components/schemas/{reference}");
}

[DataTestMethod]
[DataRow("microsoft.Azure.WebJobs.Extensions.OpenApi.TestApp.Models.ListStringObjectModel", "array")]
public void Given_OpenApiDocument_Then_It_Should_Return_ComponentSchemaProperty(string reference, string referenceType)
{
var properties = this._doc["components"]["schemas"][reference];

var type = properties["type"];

type.Should().NotBeNull();
type.Value<string>().Should().Be(referenceType);
}

[DataTestMethod]
[DataRow("/get-applicationjson-int-array", "get", "200", "application/json", "array", "integer", "int32")]
[DataRow("/get-applicationjson-int-list", "get", "200", "application/json", "array", "integer", "int32")]
Expand All @@ -113,5 +140,13 @@ public void Given_OpenApiDocument_Then_It_Should_Return_OperationResponseContent

items.Value<string>("type").Should().Be(itemType);
}

[DataTestMethod]
[DataRow("microsoft.Azure.WebJobs.Extensions.OpenApi.TestApp.Models.ListStringObjectModel", "microsoft.Azure.WebJobs.Extensions.OpenApi.TestApp.Models.StringObjectModel")]
public void Given_OpenApiDocument_Then_It_Should_Return_ComponentSchemaPropertyItems(string reference, string itemRef)
{
var items = this._doc["components"]["schemas"][reference]["items"];
items.Value<string>("$ref").Should().Be($"#/components/schemas/{itemRef}");
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;

Expand All @@ -14,15 +16,15 @@ namespace Microsoft.Azure.WebJobs.Extensions.OpenApi.TestApp
{
public static class Get_ApplicationJson_Array_HttpTrigger
{

[FunctionName(nameof(Get_ApplicationJson_Array_HttpTrigger.Get_ApplicationJson_StringArray))]
[OpenApiOperation(operationId: nameof(Get_ApplicationJson_Array_HttpTrigger.Get_ApplicationJson_StringArray), tags: new[] { "array" })]
[OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(string[]), Description = "The OK response")]
public static async Task<IActionResult> Get_ApplicationJson_StringArray(
[HttpTrigger(AuthorizationLevel.Anonymous, "GET", Route = "get-applicationjson-string-array")] HttpRequest req,
ILogger log)
{
var result = new OkResult();
var result = new OkResult();

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

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

return await Task.FromResult(result).ConfigureAwait(false);
}
Expand All @@ -58,9 +60,21 @@ public static async Task<IActionResult> Get_ApplicationJson_IntList(
[HttpTrigger(AuthorizationLevel.Anonymous, "GET", Route = "get-applicationjson-int-list")] HttpRequest req,
ILogger log)
{
var result = new OkResult();
var result = new OkResult();

return await Task.FromResult(result).ConfigureAwait(false);
}

[FunctionName(nameof(Get_ApplicationJson_Array_HttpTrigger.Get_ApplicationJson_NamedList))]
[OpenApiOperation(operationId: nameof(Get_ApplicationJson_Array_HttpTrigger.Get_ApplicationJson_NamedList), tags: new[] { "array" })]
[OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(ListStringObjectModel), Description = "The OK response")]
public static async Task<IActionResult> Get_ApplicationJson_NamedList(
[HttpTrigger(AuthorizationLevel.Anonymous, "GET", Route = "get-applicationjson-named-list")] HttpRequest req,
ILogger log)
{
var result = new OkResult();

return await Task.FromResult(result).ConfigureAwait(false);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public class ArrayObjectModel
public IReadOnlyCollection<float> FloatValue { get; set; }
public HashSet<decimal> DecimalValue { get; set; }
public ISet<StringObjectModel> StringObjectValue { get; set; }

public List<object[]> ObjectArrayValue { get; set; }
public ListStringObjectModel ListStringObjectValue { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;

namespace Microsoft.Azure.WebJobs.Extensions.OpenApi.TestApp.Models;

public class ListStringObjectModel : IEnumerable<StringObjectModel>
{
public IEnumerator<StringObjectModel> GetEnumerator() => Enumerable.Empty<StringObjectModel>().GetEnumerator();

IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
}
Loading