From c3decc30d82c864551ea70a09a8ae7d1e1d9a3f5 Mon Sep 17 00:00:00 2001 From: Justin Yoo Date: Sun, 19 Jul 2020 12:19:18 +0900 Subject: [PATCH] Merge for #60 by resolving conflicts (#96) --- .../SampleHttpTrigger.cs | 5 +- .../Attributes/OpenApiRequestBodyAttribute.cs | 7 ++- .../OpenApiResponseBodyAttribute.cs | 4 +- .../OpenApiResponseWithBodyAttribute.cs | 35 ++++++++++++++ .../OpenApiResponseWithoutBodyAttribute.cs | 36 ++++++++++++++ .../Document.cs | 2 +- .../DocumentHelper.cs | 47 +++++++++++++++---- ...ApiResponseWithBodyAttributeExtensions.cs} | 16 +++---- ...iResponseWithoutBodyAttributeExtensions.cs | 43 +++++++++++++++++ .../Extensions/OpenApiResponsesExtensions.cs | 3 +- .../IDocumentHelper.cs | 14 +++++- ... OpenApiResponseWithBodyAttributeTests.cs} | 11 +++-- ...penApiResponseWithoutBodyAttributeTests.cs | 25 ++++++++++ 13 files changed, 216 insertions(+), 32 deletions(-) create mode 100644 src/Aliencube.AzureFunctions.Extensions.OpenApi/Attributes/OpenApiResponseWithBodyAttribute.cs create mode 100644 src/Aliencube.AzureFunctions.Extensions.OpenApi/Attributes/OpenApiResponseWithoutBodyAttribute.cs rename src/Aliencube.AzureFunctions.Extensions.OpenApi/Extensions/{OpenApiResponseBodyAttributeExtensions.cs => OpenApiResponseWithBodyAttributeExtensions.cs} (77%) create mode 100644 src/Aliencube.AzureFunctions.Extensions.OpenApi/Extensions/OpenApiResponseWithoutBodyAttributeExtensions.cs rename test/Aliencube.AzureFunctions.Extensions.OpenApi.Tests/Attributes/{OpenApiResponseBodyAttributeTests.cs => OpenApiResponseWithBodyAttributeTests.cs} (70%) create mode 100644 test/Aliencube.AzureFunctions.Extensions.OpenApi.Tests/Attributes/OpenApiResponseWithoutBodyAttributeTests.cs diff --git a/samples/Aliencube.AzureFunctions.FunctionAppV2/SampleHttpTrigger.cs b/samples/Aliencube.AzureFunctions.FunctionAppV2/SampleHttpTrigger.cs index fd5624b..de6a213 100644 --- a/samples/Aliencube.AzureFunctions.FunctionAppV2/SampleHttpTrigger.cs +++ b/samples/Aliencube.AzureFunctions.FunctionAppV2/SampleHttpTrigger.cs @@ -48,7 +48,6 @@ public SampleHttpTrigger(ISampleHttpFunction function) [OpenApiParameter(name: "category", In = ParameterLocation.Path, Required = true, Type = typeof(string), Summary = "The category parameter", Visibility = OpenApiVisibilityType.Advanced)] [OpenApiParameter(name: "name", In = ParameterLocation.Query, Required = true, Type = typeof(string), Summary = "The name query key", Visibility = OpenApiVisibilityType.Advanced)] [OpenApiParameter(name: "limit", In = ParameterLocation.Query, Required = false, Type = typeof(int), Description = "The number of samples to return")] - [OpenApiResponseBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(SampleResponseModel), Summary = "Sample response")] [OpenApiResponseBody(statusCode: HttpStatusCode.Created, contentType: "application/json", bodyType: typeof(List), Summary = "Sample response of a List")] [OpenApiResponseBody(statusCode: HttpStatusCode.Accepted, contentType: "application/json", bodyType: typeof(IList), Summary = "Sample response of a IList")] [OpenApiResponseBody(statusCode: HttpStatusCode.NonAuthoritativeInformation, contentType: "application/json", bodyType: typeof(SampleResponseModel[]), Summary = "Sample response of a Array")] @@ -59,6 +58,8 @@ public SampleHttpTrigger(ISampleHttpFunction function) [OpenApiResponseBody(statusCode: HttpStatusCode.InternalServerError, contentType: "application/json", bodyType: typeof(List), Summary = "Sample response of a List")] [OpenApiResponseBody(statusCode: HttpStatusCode.NotImplemented, contentType: "application/json", bodyType: typeof(Dictionary), Summary = "Sample response of a Dictionary")] [OpenApiResponseBody(statusCode: HttpStatusCode.BadGateway, contentType: "application/json", bodyType: typeof(SampleGenericResponseModel), Summary = "Sample response of a Generic")] + //[OpenApiResponseWithoutBody(statusCode: HttpStatusCode.NoContent)] + //[OpenApiResponseWithoutBody(statusCode: HttpStatusCode.RequestTimeout, Description = "Sample response with only status code", Summary = "Sample summary")] public async Task GetSample( [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "samples/{id:int}/categories/{category:regex(^[a-z]{{3,}}$)}")] HttpRequest req, int id, @@ -93,4 +94,4 @@ public async Task PostSample( return result; } } -} \ No newline at end of file +} diff --git a/src/Aliencube.AzureFunctions.Extensions.OpenApi/Attributes/OpenApiRequestBodyAttribute.cs b/src/Aliencube.AzureFunctions.Extensions.OpenApi/Attributes/OpenApiRequestBodyAttribute.cs index f385187..00b2e62 100644 --- a/src/Aliencube.AzureFunctions.Extensions.OpenApi/Attributes/OpenApiRequestBodyAttribute.cs +++ b/src/Aliencube.AzureFunctions.Extensions.OpenApi/Attributes/OpenApiRequestBodyAttribute.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace Aliencube.AzureFunctions.Extensions.OpenApi.Attributes { @@ -17,5 +17,10 @@ public OpenApiRequestBodyAttribute(string contentType, Type bodyType) : base(contentType, bodyType) { } + + /// + /// Gets or sets the value indicating whether the request body is required or not. + /// + public virtual bool Required { get; set; } } } diff --git a/src/Aliencube.AzureFunctions.Extensions.OpenApi/Attributes/OpenApiResponseBodyAttribute.cs b/src/Aliencube.AzureFunctions.Extensions.OpenApi/Attributes/OpenApiResponseBodyAttribute.cs index 0c23ba2..8ead785 100644 --- a/src/Aliencube.AzureFunctions.Extensions.OpenApi/Attributes/OpenApiResponseBodyAttribute.cs +++ b/src/Aliencube.AzureFunctions.Extensions.OpenApi/Attributes/OpenApiResponseBodyAttribute.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Net; namespace Aliencube.AzureFunctions.Extensions.OpenApi.Attributes @@ -6,7 +6,7 @@ namespace Aliencube.AzureFunctions.Extensions.OpenApi.Attributes /// /// This represents the attribute entity for HTTP triggers to define response body payload. /// - + [Obsolete("This class is obsolete from 2.0.0. Use OpenApiResponseWithBodyAttribute instead", error: true)] [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] public class OpenApiResponseBodyAttribute : OpenApiPayloadAttribute { diff --git a/src/Aliencube.AzureFunctions.Extensions.OpenApi/Attributes/OpenApiResponseWithBodyAttribute.cs b/src/Aliencube.AzureFunctions.Extensions.OpenApi/Attributes/OpenApiResponseWithBodyAttribute.cs new file mode 100644 index 0000000..feca764 --- /dev/null +++ b/src/Aliencube.AzureFunctions.Extensions.OpenApi/Attributes/OpenApiResponseWithBodyAttribute.cs @@ -0,0 +1,35 @@ +using System; +using System.Net; + +namespace Aliencube.AzureFunctions.Extensions.OpenApi.Attributes +{ + /// + /// This represents the attribute entity for HTTP triggers to define response body payload. + /// + + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] + public class OpenApiResponseWithBodyAttribute : OpenApiPayloadAttribute + { + /// + /// Initializes a new instance of the class. + /// + /// HTTP status code. + /// Content type. + /// Type of payload. + public OpenApiResponseWithBodyAttribute(HttpStatusCode statusCode, string contentType, Type bodyType) + : base(contentType, bodyType) + { + this.StatusCode = statusCode; + } + + /// + /// Gets the HTTP status code value. + /// + public virtual HttpStatusCode StatusCode { get; } + + /// + /// Gets or sets the summary. + /// + public virtual string Summary { get; set; } + } +} diff --git a/src/Aliencube.AzureFunctions.Extensions.OpenApi/Attributes/OpenApiResponseWithoutBodyAttribute.cs b/src/Aliencube.AzureFunctions.Extensions.OpenApi/Attributes/OpenApiResponseWithoutBodyAttribute.cs new file mode 100644 index 0000000..ea97fe3 --- /dev/null +++ b/src/Aliencube.AzureFunctions.Extensions.OpenApi/Attributes/OpenApiResponseWithoutBodyAttribute.cs @@ -0,0 +1,36 @@ +using System; +using System.Net; + +namespace Aliencube.AzureFunctions.Extensions.OpenApi.Attributes +{ + /// + /// This represents the attribute entity for HTTP triggers to define response body payload. + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] + public class OpenApiResponseWithoutBodyAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + /// HTTP status code. + public OpenApiResponseWithoutBodyAttribute(HttpStatusCode statusCode) + { + this.StatusCode = statusCode; + } + + /// + /// Gets the HTTP status code value. + /// + public virtual HttpStatusCode StatusCode { get; } + + /// + /// Gets or sets the summary. + /// + public virtual string Summary { get; set; } + + /// + /// Gets or sets the description. + /// + public virtual string Description { get; set; } + } +} diff --git a/src/Aliencube.AzureFunctions.Extensions.OpenApi/Document.cs b/src/Aliencube.AzureFunctions.Extensions.OpenApi/Document.cs index 04ae104..e160c87 100644 --- a/src/Aliencube.AzureFunctions.Extensions.OpenApi/Document.cs +++ b/src/Aliencube.AzureFunctions.Extensions.OpenApi/Document.cs @@ -123,7 +123,7 @@ public IDocument Build(Assembly assembly, NamingStrategy namingStrategy = null) operation.Parameters = this._helper.GetOpenApiParameters(method, trigger); operation.RequestBody = this._helper.GetOpenApiRequestBody(method); - operation.Responses = this._helper.GetOpenApiResponseBody(method); + operation.Responses = this._helper.GetOpenApiResponses(method); operations[verb] = operation; item.Operations = operations; diff --git a/src/Aliencube.AzureFunctions.Extensions.OpenApi/DocumentHelper.cs b/src/Aliencube.AzureFunctions.Extensions.OpenApi/DocumentHelper.cs index bed42ba..598d28c 100644 --- a/src/Aliencube.AzureFunctions.Extensions.OpenApi/DocumentHelper.cs +++ b/src/Aliencube.AzureFunctions.Extensions.OpenApi/DocumentHelper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -125,6 +125,7 @@ public List GetOpenApiParameters(MethodInfo element, HttpTrigg .Select(p => p.ToOpenApiParameter()) .ToList(); + // This needs to be provided separately. if (trigger.AuthLevel != AuthorizationLevel.Anonymous) { parameters.AddOpenApiParameter("code", @in: ParameterLocation.Query, required: false); @@ -136,23 +137,51 @@ public List GetOpenApiParameters(MethodInfo element, HttpTrigg /// public OpenApiRequestBody GetOpenApiRequestBody(MethodInfo element, NamingStrategy namingStrategy = null) { - var contents = element.GetCustomAttributes(inherit: false) - .ToDictionary(p => p.ContentType, p => p.ToOpenApiMediaType()); + var attributes = element.GetCustomAttributes(inherit: false); + if (!attributes.Any()) + { + return null; + } + + var contents = attributes.ToDictionary(p => p.ContentType, p => p.ToOpenApiMediaType()); if (contents.Any()) { - return new OpenApiRequestBody() { Content = contents }; + return new OpenApiRequestBody() + { + Content = contents, + Required = attributes.First().Required + }; } return null; } /// + [Obsolete("This method is obsolete from 2.0.0. Use GetOpenApiResponses instead", error: true)] public OpenApiResponses GetOpenApiResponseBody(MethodInfo element, NamingStrategy namingStrategy = null) { - var responses = element.GetCustomAttributes(inherit: false) - .ToDictionary(p => ((int)p.StatusCode).ToString(), p => p.ToOpenApiResponse(namingStrategy)) - .ToOpenApiResponses(); + return this.GetOpenApiResponses(element, namingStrategy); + + //var responses = element.GetCustomAttributes(inherit: false) + // .ToDictionary(p => ((int)p.StatusCode).ToString(), p => p.ToOpenApiResponse(namingStrategy)) + // .ToOpenApiResponses(); + + //return responses; + } + + /// + public OpenApiResponses GetOpenApiResponses(MethodInfo element, NamingStrategy namingStrategy = null) + { + var responsesWithBody = element.GetCustomAttributes(inherit: false) + .Select(p => new { StatusCode = p.StatusCode, Response = p.ToOpenApiResponse(namingStrategy) }); + + var responsesWithoutBody = element.GetCustomAttributes(inherit: false) + .Select(p => new { StatusCode = p.StatusCode, Response = p.ToOpenApiResponse(namingStrategy) }); + + var responses = responsesWithBody.Concat(responsesWithoutBody) + .ToDictionary(p => ((int)p.StatusCode).ToString(), p => p.Response) + .ToOpenApiResponses(); return responses; } @@ -162,7 +191,7 @@ public Dictionary GetOpenApiSchemas(List elem { var requests = elements.SelectMany(p => p.GetCustomAttributes(inherit: false)) .Select(p => p.BodyType); - var responses = elements.SelectMany(p => p.GetCustomAttributes(inherit: false)) + var responses = elements.SelectMany(p => p.GetCustomAttributes(inherit: false)) .Select(p => p.BodyType); var types = requests.Union(responses) .Select(p => p.IsOpenApiArray() || p.IsOpenApiDictionary() ? p.GetOpenApiSubType() : p) @@ -204,4 +233,4 @@ private string FilterRoute(string route) return string.Join("/", segments); } } -} \ No newline at end of file +} diff --git a/src/Aliencube.AzureFunctions.Extensions.OpenApi/Extensions/OpenApiResponseBodyAttributeExtensions.cs b/src/Aliencube.AzureFunctions.Extensions.OpenApi/Extensions/OpenApiResponseWithBodyAttributeExtensions.cs similarity index 77% rename from src/Aliencube.AzureFunctions.Extensions.OpenApi/Extensions/OpenApiResponseBodyAttributeExtensions.cs rename to src/Aliencube.AzureFunctions.Extensions.OpenApi/Extensions/OpenApiResponseWithBodyAttributeExtensions.cs index 330d151..ebb6fa4 100644 --- a/src/Aliencube.AzureFunctions.Extensions.OpenApi/Extensions/OpenApiResponseBodyAttributeExtensions.cs +++ b/src/Aliencube.AzureFunctions.Extensions.OpenApi/Extensions/OpenApiResponseWithBodyAttributeExtensions.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Aliencube.AzureFunctions.Extensions.OpenApi.Attributes; @@ -10,24 +10,24 @@ namespace Aliencube.AzureFunctions.Extensions.OpenApi.Extensions { /// - /// This represents the extension entity for . + /// This represents the extension entity for . /// - public static class OpenApiResponseBodyAttributeExtensions + public static class OpenApiResponseWithBodyAttributeExtensions { /// - /// Converts to . + /// Converts to . /// - /// instance. + /// instance. /// instance to create the JSON schema from .NET Types. /// instance. - public static OpenApiResponse ToOpenApiResponse(this OpenApiResponseBodyAttribute attribute, NamingStrategy namingStrategy = null) + public static OpenApiResponse ToOpenApiResponse(this OpenApiResponseWithBodyAttribute attribute, NamingStrategy namingStrategy = null) { attribute.ThrowIfNullOrDefault(); var description = string.IsNullOrWhiteSpace(attribute.Description) ? $"Payload of {attribute.BodyType.GetOpenApiDescription()}" : attribute.Description; - var mediaType = attribute.ToOpenApiMediaType(namingStrategy); + var mediaType = attribute.ToOpenApiMediaType(namingStrategy); var content = new Dictionary() { { attribute.ContentType, mediaType } @@ -48,4 +48,4 @@ public static OpenApiResponse ToOpenApiResponse(this OpenApiResponseBodyAttribut return response; } } -} \ No newline at end of file +} diff --git a/src/Aliencube.AzureFunctions.Extensions.OpenApi/Extensions/OpenApiResponseWithoutBodyAttributeExtensions.cs b/src/Aliencube.AzureFunctions.Extensions.OpenApi/Extensions/OpenApiResponseWithoutBodyAttributeExtensions.cs new file mode 100644 index 0000000..60bdf19 --- /dev/null +++ b/src/Aliencube.AzureFunctions.Extensions.OpenApi/Extensions/OpenApiResponseWithoutBodyAttributeExtensions.cs @@ -0,0 +1,43 @@ +using Aliencube.AzureFunctions.Extensions.OpenApi.Attributes; + +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; + +using Newtonsoft.Json.Serialization; + +namespace Aliencube.AzureFunctions.Extensions.OpenApi.Extensions +{ + /// + /// This represents the extension entity for . + /// + public static class OpenApiResponseWithoutBodyAttributeExtensions + { + /// + /// Converts to . + /// + /// instance. + /// instance to create the JSON schema from .NET Types. + /// instance. + public static OpenApiResponse ToOpenApiResponse(this OpenApiResponseWithoutBodyAttribute attribute, NamingStrategy namingStrategy = null) + { + attribute.ThrowIfNullOrDefault(); + + var description = string.IsNullOrWhiteSpace(attribute.Description) + ? "No description" + : attribute.Description; + var response = new OpenApiResponse() + { + Description = description, + }; + + if (!string.IsNullOrWhiteSpace(attribute.Summary)) + { + var summary = new OpenApiString(attribute.Summary); + + response.Extensions.Add("x-ms-summary", summary); + } + + return response; + } + } +} diff --git a/src/Aliencube.AzureFunctions.Extensions.OpenApi/Extensions/OpenApiResponsesExtensions.cs b/src/Aliencube.AzureFunctions.Extensions.OpenApi/Extensions/OpenApiResponsesExtensions.cs index a4b313b..302ec8f 100644 --- a/src/Aliencube.AzureFunctions.Extensions.OpenApi/Extensions/OpenApiResponsesExtensions.cs +++ b/src/Aliencube.AzureFunctions.Extensions.OpenApi/Extensions/OpenApiResponsesExtensions.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using Microsoft.OpenApi.Models; @@ -28,4 +27,4 @@ public static OpenApiResponses ToOpenApiResponses(this Dictionary instance. /// instance to create the JSON schema from .NET Types. /// instance. + [Obsolete("This method is obsolete from 2.0.0. Use GetOpenApiResponses instead", error: true)] OpenApiResponses GetOpenApiResponseBody(MethodInfo element, NamingStrategy namingStrategy = null); + /// + /// Gets the instance. + /// + /// instance. + /// instance to create the JSON schema from .NET Types. + /// instance. + OpenApiResponses GetOpenApiResponses(MethodInfo element, NamingStrategy namingStrategy = null); + /// /// Gets the collection of instances. /// @@ -104,4 +114,4 @@ public interface IDocumentHelper /// Collection of instance. Dictionary GetOpenApiSecuritySchemes(); } -} \ No newline at end of file +} diff --git a/test/Aliencube.AzureFunctions.Extensions.OpenApi.Tests/Attributes/OpenApiResponseBodyAttributeTests.cs b/test/Aliencube.AzureFunctions.Extensions.OpenApi.Tests/Attributes/OpenApiResponseWithBodyAttributeTests.cs similarity index 70% rename from test/Aliencube.AzureFunctions.Extensions.OpenApi.Tests/Attributes/OpenApiResponseBodyAttributeTests.cs rename to test/Aliencube.AzureFunctions.Extensions.OpenApi.Tests/Attributes/OpenApiResponseWithBodyAttributeTests.cs index 25e478b..da4ee6e 100644 --- a/test/Aliencube.AzureFunctions.Extensions.OpenApi.Tests/Attributes/OpenApiResponseBodyAttributeTests.cs +++ b/test/Aliencube.AzureFunctions.Extensions.OpenApi.Tests/Attributes/OpenApiResponseWithBodyAttributeTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Net; using Aliencube.AzureFunctions.Extensions.OpenApi.Attributes; @@ -10,17 +10,17 @@ namespace Aliencube.AzureFunctions.Extensions.OpenApi.Tests.Attributes { [TestClass] - public class OpenApiResponseBodyAttributeTests + public class OpenApiResponseWithBodyAttributeTests { [TestMethod] public void Given_Null_Constructor_Should_Throw_Exception() { var statusCode = HttpStatusCode.OK; - Action action = () => new OpenApiResponseBodyAttribute(statusCode, null, null); + Action action = () => new OpenApiResponseWithBodyAttribute(statusCode, null, null); action.Should().Throw(); - action = () => new OpenApiResponseBodyAttribute(statusCode, "hello world", null); + action = () => new OpenApiResponseWithBodyAttribute(statusCode, "hello world", null); action.Should().Throw(); } @@ -30,11 +30,12 @@ public void Given_Value_Property_Should_Return_Value() var statusCode = HttpStatusCode.OK; var contentType = "Hello World"; var bodyType = typeof(object); - var attribute = new OpenApiResponseBodyAttribute(statusCode, contentType, bodyType); + var attribute = new OpenApiResponseWithBodyAttribute(statusCode, contentType, bodyType); attribute.StatusCode.Should().Be(statusCode); attribute.ContentType.Should().BeEquivalentTo(contentType); attribute.BodyType.Should().Be(bodyType); + attribute.Summary.Should().BeNullOrWhiteSpace(); attribute.Description.Should().BeNullOrWhiteSpace(); } } diff --git a/test/Aliencube.AzureFunctions.Extensions.OpenApi.Tests/Attributes/OpenApiResponseWithoutBodyAttributeTests.cs b/test/Aliencube.AzureFunctions.Extensions.OpenApi.Tests/Attributes/OpenApiResponseWithoutBodyAttributeTests.cs new file mode 100644 index 0000000..ebfde30 --- /dev/null +++ b/test/Aliencube.AzureFunctions.Extensions.OpenApi.Tests/Attributes/OpenApiResponseWithoutBodyAttributeTests.cs @@ -0,0 +1,25 @@ +using System.Net; + +using Aliencube.AzureFunctions.Extensions.OpenApi.Attributes; + +using FluentAssertions; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Aliencube.AzureFunctions.Extensions.OpenApi.Tests.Attributes +{ + [TestClass] + public class OpenApiResponseWithoutBodyAttributeTests + { + [TestMethod] + public void Given_Value_Property_Should_Return_Value() + { + var statusCode = HttpStatusCode.OK; + var attribute = new OpenApiResponseWithoutBodyAttribute(statusCode); + + attribute.StatusCode.Should().Be(statusCode); + attribute.Summary.Should().BeNullOrWhiteSpace(); + attribute.Description.Should().BeNullOrWhiteSpace(); + } + } +}