diff --git a/src/Microsoft.OpenApi.OData.Reader/Microsoft.OpenAPI.OData.Reader.csproj b/src/Microsoft.OpenApi.OData.Reader/Microsoft.OpenAPI.OData.Reader.csproj index 546d3e95..5b6e71c9 100644 --- a/src/Microsoft.OpenApi.OData.Reader/Microsoft.OpenAPI.OData.Reader.csproj +++ b/src/Microsoft.OpenApi.OData.Reader/Microsoft.OpenAPI.OData.Reader.csproj @@ -15,7 +15,7 @@ netstandard2.0 Microsoft.OpenApi.OData true - 1.6.0-preview.9 + 1.6.0-preview.10 This package contains the codes you need to convert OData CSDL to Open API Document of Model. © Microsoft Corporation. All rights reserved. Microsoft OpenApi OData EDM @@ -31,6 +31,7 @@ - Use alternate keys in the generation of operation ids of operations and navigation property alternate paths #488 - Fixes operation ids of paths with type cast segments #492 - Uses convert setting to toggle between generating query options schemas of type string array or enums #197 +- Adds ability to change the response or request body content media type based on custom annotation properties #405 Microsoft.OpenApi.OData.Reader ..\..\tool\Microsoft.OpenApi.OData.snk diff --git a/src/Microsoft.OpenApi.OData.Reader/Operation/EntitySetPostOperationHandler.cs b/src/Microsoft.OpenApi.OData.Reader/Operation/EntitySetPostOperationHandler.cs index 0bc36bdd..d41ad59e 100644 --- a/src/Microsoft.OpenApi.OData.Reader/Operation/EntitySetPostOperationHandler.cs +++ b/src/Microsoft.OpenApi.OData.Reader/Operation/EntitySetPostOperationHandler.cs @@ -136,7 +136,7 @@ private IDictionary GetContentDescription() } else { - // Default content type + // Default stream content type content.Add(Constants.ApplicationOctetStreamMediaType, new OpenApiMediaType { Schema = new OpenApiSchema @@ -147,11 +147,29 @@ private IDictionary GetContentDescription() }); } } - - content.Add(Constants.ApplicationJsonMediaType, new OpenApiMediaType - { - Schema = schema - }); + else + { + // Add the annotated request content media types + IEnumerable mediaTypes = _insertRestrictions?.RequestContentTypes; + if (mediaTypes != null) + { + foreach (string mediaType in mediaTypes) + { + content.Add(mediaType, new OpenApiMediaType + { + Schema = schema + }); + } + } + else + { + // Default content type + content.Add(Constants.ApplicationJsonMediaType, new OpenApiMediaType + { + Schema = schema + }); + } + } return content; } diff --git a/src/Microsoft.OpenApi.OData.Reader/Operation/EntityUpdateOperationHandler.cs b/src/Microsoft.OpenApi.OData.Reader/Operation/EntityUpdateOperationHandler.cs index fc14c9c6..caf38eb3 100644 --- a/src/Microsoft.OpenApi.OData.Reader/Operation/EntityUpdateOperationHandler.cs +++ b/src/Microsoft.OpenApi.OData.Reader/Operation/EntityUpdateOperationHandler.cs @@ -64,20 +64,41 @@ protected override void SetRequestBody(OpenApiOperation operation) { Required = true, Description = "New property values", - Content = new Dictionary - { - { - Constants.ApplicationJsonMediaType, new OpenApiMediaType - { - Schema = GetOpenApiSchema() - } - } - } + Content = GetContent() }; base.SetRequestBody(operation); } + protected IDictionary GetContent() + { + OpenApiSchema schema = GetOpenApiSchema(); + var content = new Dictionary(); + IEnumerable mediaTypes = _updateRestrictions?.RequestContentTypes; + + // Add the annotated request content media types + if (mediaTypes != null) + { + foreach (string mediaType in mediaTypes) + { + content.Add(mediaType, new OpenApiMediaType + { + Schema = schema + }); + } + } + else + { + // Default content type + content.Add(Constants.ApplicationJsonMediaType, new OpenApiMediaType + { + Schema = schema + }); + }; + + return content; + } + /// protected override void SetResponses(OpenApiOperation operation) { diff --git a/src/Microsoft.OpenApi.OData.Reader/Operation/NavigationPropertyOperationHandler.cs b/src/Microsoft.OpenApi.OData.Reader/Operation/NavigationPropertyOperationHandler.cs index 8c2d842e..2bbb3131 100644 --- a/src/Microsoft.OpenApi.OData.Reader/Operation/NavigationPropertyOperationHandler.cs +++ b/src/Microsoft.OpenApi.OData.Reader/Operation/NavigationPropertyOperationHandler.cs @@ -3,6 +3,7 @@ // Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. // ------------------------------------------------------------ +using System.Collections.Generic; using System.Linq; using Microsoft.OData.Edm; using Microsoft.OpenApi.Any; @@ -137,5 +138,45 @@ protected IRecord GetRestrictionAnnotation(string annotationTerm) _ => null, }; } + + protected IDictionary GetContent(OpenApiSchema schema = null, IEnumerable mediaTypes = null) + { + schema ??= GetOpenApiSchema(); + var content = new Dictionary(); + + if (mediaTypes != null) + { + foreach (string mediaType in mediaTypes) + { + content.Add(mediaType, new OpenApiMediaType + { + Schema = schema + }); + } + } + else + { + // Default content type + content.Add(Constants.ApplicationJsonMediaType, new OpenApiMediaType + { + Schema = schema + }); + }; + + return content; + } + + protected OpenApiSchema GetOpenApiSchema() + { + return new OpenApiSchema + { + UnresolvedReference = true, + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = NavigationProperty.ToEntityType().FullName() + } + }; + } } } diff --git a/src/Microsoft.OpenApi.OData.Reader/Operation/NavigationPropertyPostOperationHandler.cs b/src/Microsoft.OpenApi.OData.Reader/Operation/NavigationPropertyPostOperationHandler.cs index b60f6a2d..cebb88c9 100644 --- a/src/Microsoft.OpenApi.OData.Reader/Operation/NavigationPropertyPostOperationHandler.cs +++ b/src/Microsoft.OpenApi.OData.Reader/Operation/NavigationPropertyPostOperationHandler.cs @@ -3,7 +3,6 @@ // Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. // ------------------------------------------------------------ -using System.Collections.Generic; using System.Linq; using Microsoft.OData.Edm; using Microsoft.OpenApi.Models; @@ -53,40 +52,19 @@ protected override void SetBasicInfo(OpenApiOperation operation) /// protected override void SetRequestBody(OpenApiOperation operation) - { - OpenApiSchema schema = null; - - if (Context.Settings.EnableDerivedTypesReferencesForRequestBody) - { - schema = EdmModelHelper.GetDerivedTypesReferenceSchema(NavigationProperty.ToEntityType(), Context.Model); - } - - if (schema == null) - { - schema = new OpenApiSchema - { - UnresolvedReference = true, - Reference = new OpenApiReference - { - Type = ReferenceType.Schema, - Id = NavigationProperty.ToEntityType().FullName() - } - }; - } - + { + OpenApiSchema schema = null; + + if (Context.Settings.EnableDerivedTypesReferencesForRequestBody) + { + schema = EdmModelHelper.GetDerivedTypesReferenceSchema(NavigationProperty.ToEntityType(), Context.Model); + } + operation.RequestBody = new OpenApiRequestBody { Required = true, Description = "New navigation property", - Content = new Dictionary - { - { - Constants.ApplicationJsonMediaType, new OpenApiMediaType - { - Schema = schema - } - } - } + Content = GetContent(schema, _insertRestriction?.RequestContentTypes) }; base.SetRequestBody(operation); @@ -94,7 +72,7 @@ protected override void SetRequestBody(OpenApiOperation operation) /// protected override void SetResponses(OpenApiOperation operation) - { + { OpenApiSchema schema = null; if (Context.Settings.EnableDerivedTypesReferencesForResponses) @@ -102,19 +80,6 @@ protected override void SetResponses(OpenApiOperation operation) schema = EdmModelHelper.GetDerivedTypesReferenceSchema(NavigationProperty.ToEntityType(), Context.Model); } - if (schema == null) - { - schema = new OpenApiSchema - { - UnresolvedReference = true, - Reference = new OpenApiReference - { - Type = ReferenceType.Schema, - Id = NavigationProperty.ToEntityType().FullName() - } - }; - } - operation.Responses = new OpenApiResponses { { @@ -122,24 +87,15 @@ protected override void SetResponses(OpenApiOperation operation) new OpenApiResponse { Description = "Created navigation property.", - Content = new Dictionary - { - { - Constants.ApplicationJsonMediaType, - new OpenApiMediaType - { - Schema = schema - } - } - } + Content = GetContent(schema, _insertRestriction?.ResponseContentTypes) } } }; operation.AddErrorResponses(Context.Settings, false); base.SetResponses(operation); - } - + } + protected override void SetSecurity(OpenApiOperation operation) { if (_insertRestriction == null) diff --git a/src/Microsoft.OpenApi.OData.Reader/Operation/NavigationPropertyUpdateOperationHandler.cs b/src/Microsoft.OpenApi.OData.Reader/Operation/NavigationPropertyUpdateOperationHandler.cs index f01c95c4..0ae4b500 100644 --- a/src/Microsoft.OpenApi.OData.Reader/Operation/NavigationPropertyUpdateOperationHandler.cs +++ b/src/Microsoft.OpenApi.OData.Reader/Operation/NavigationPropertyUpdateOperationHandler.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------ +// ------------------------------------------------------------ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. // ------------------------------------------------------------ @@ -50,20 +50,18 @@ protected override void SetBasicInfo(OpenApiOperation operation) /// protected override void SetRequestBody(OpenApiOperation operation) - { + { + OpenApiSchema schema = null; + if (Context.Settings.EnableDerivedTypesReferencesForRequestBody) + { + schema = EdmModelHelper.GetDerivedTypesReferenceSchema(NavigationProperty.ToEntityType(), Context.Model); + } + operation.RequestBody = new OpenApiRequestBody { Required = true, Description = "New navigation property values", - Content = new Dictionary - { - { - Constants.ApplicationJsonMediaType, new OpenApiMediaType - { - Schema = GetOpenApiSchema() - } - } - } + Content = GetContent(schema, _updateRestriction?.RequestContentTypes) }; base.SetRequestBody(operation); @@ -103,23 +101,5 @@ protected override void AppendCustomParameters(OpenApiOperation operation) AppendCustomParameters(operation, _updateRestriction.CustomQueryOptions, ParameterLocation.Query); } } - - private OpenApiSchema GetOpenApiSchema() - { - if (Context.Settings.EnableDerivedTypesReferencesForRequestBody) - { - return EdmModelHelper.GetDerivedTypesReferenceSchema(NavigationProperty.ToEntityType(), Context.Model); - } - - return new OpenApiSchema - { - UnresolvedReference = true, - Reference = new OpenApiReference - { - Type = ReferenceType.Schema, - Id = NavigationProperty.ToEntityType().FullName() - } - }; - } } } diff --git a/src/Microsoft.OpenApi.OData.Reader/Vocabulary/Capabilities/InsertRestrictionsType.cs b/src/Microsoft.OpenApi.OData.Reader/Vocabulary/Capabilities/InsertRestrictionsType.cs index ba6eb06e..020f49f5 100644 --- a/src/Microsoft.OpenApi.OData.Reader/Vocabulary/Capabilities/InsertRestrictionsType.cs +++ b/src/Microsoft.OpenApi.OData.Reader/Vocabulary/Capabilities/InsertRestrictionsType.cs @@ -78,6 +78,18 @@ internal class InsertRestrictionsType : IRecord /// True/false. public bool IsInsertable => Insertable == null || Insertable.Value == true; + /// + /// Lists the media types acceptable for the request content + /// + /// This is not an official OASIS standard property. + public IList RequestContentTypes { get; private set; } + + /// + /// Lists the media types acceptable for the response content + /// + /// This is not an official OASIS standard property. + public IList ResponseContentTypes { get; private set; } + /// /// Test the input navigation property do not allow deep insert. /// @@ -127,6 +139,12 @@ public void Initialize(IEdmRecordExpression record) // LongDescription LongDescription = record.GetString("LongDescription"); + + // RequestContentTypes + RequestContentTypes = record.GetCollection("RequestContentTypes"); + + // ResponseContentTypes + ResponseContentTypes = record.GetCollection("ResponseContentTypes"); } } } diff --git a/src/Microsoft.OpenApi.OData.Reader/Vocabulary/Capabilities/UpdateRestrictionsType.cs b/src/Microsoft.OpenApi.OData.Reader/Vocabulary/Capabilities/UpdateRestrictionsType.cs index c4af3128..753a012e 100644 --- a/src/Microsoft.OpenApi.OData.Reader/Vocabulary/Capabilities/UpdateRestrictionsType.cs +++ b/src/Microsoft.OpenApi.OData.Reader/Vocabulary/Capabilities/UpdateRestrictionsType.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------ +// ------------------------------------------------------------ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. // ------------------------------------------------------------ @@ -126,7 +126,19 @@ public bool IsNonUpdatableNavigationProperty(string navigationPropertyPath) /// /// Tests whether the update method for the entity has been explicitly specified as PUT /// - public bool IsUpdateMethodPut => UpdateMethod.HasValue && UpdateMethod.Value == HttpMethod.PUT; + public bool IsUpdateMethodPut => UpdateMethod.HasValue && UpdateMethod.Value == HttpMethod.PUT; + + /// + /// Lists the media types acceptable for the request content + /// + /// This is not an official OASIS standard property. + public IList RequestContentTypes { get; private set; } + + /// + /// Lists the media types acceptable for the response content + /// + /// This is not an official OASIS standard property. + public IList ResponseContentTypes { get; private set; } /// /// Init the . @@ -155,9 +167,9 @@ public void Initialize(IEdmRecordExpression record) TypecastSegmentSupported = record.GetBoolean("TypecastSegmentSupported"); // NonUpdatableNavigationProperties - NonUpdatableNavigationProperties = record.GetCollectionPropertyPath("NonUpdatableNavigationProperties"); - - // MaxLevels + NonUpdatableNavigationProperties = record.GetCollectionPropertyPath("NonUpdatableNavigationProperties"); + + // MaxLevels MaxLevels = record.GetInteger("MaxLevels"); // Permissions @@ -177,6 +189,12 @@ public void Initialize(IEdmRecordExpression record) // LongDescription LongDescription = record.GetString("LongDescription"); + + // RequestContentTypes + RequestContentTypes = record.GetCollection("RequestContentTypes"); + + // ResponseContentTypes + ResponseContentTypes = record.GetCollection("ResponseContentTypes"); } } } diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/EntityPutOperationHandlerTests.cs b/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/EntityPutOperationHandlerTests.cs index 511d1681..fb5f11c2 100644 --- a/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/EntityPutOperationHandlerTests.cs +++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/EntityPutOperationHandlerTests.cs @@ -177,5 +177,24 @@ public void CreateEntityPutReturnsSecurityForUpdateRestrictions(bool enableAnnot Assert.Empty(putOperation.Security); } } + + [Fact] + public void CreateEntityPutOperationReturnsCorrectOperationWithAnnotatedRequestBodyContent() + { + IEdmModel model = EdmModelHelper.GraphBetaModel; + OpenApiConvertSettings settings = new(); + ODataContext context = new(model, settings); + IEdmEntitySet entitySet = model.EntityContainer.FindEntitySet("directoryObjects"); + Assert.NotNull(entitySet); + + ODataPath path = new(new ODataNavigationSourceSegment(entitySet), new ODataKeySegment(entitySet.EntityType())); + + // Act + var operation = _operationHandler.CreateOperation(context, path); + + // Assert + Assert.NotNull(operation.RequestBody); + Assert.Equal("multipart/form-data", operation.RequestBody.Content.First().Key); + } } } diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/EntitySetPostOperationHandlerTests.cs b/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/EntitySetPostOperationHandlerTests.cs index 82f24d26..61ace688 100644 --- a/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/EntitySetPostOperationHandlerTests.cs +++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/EntitySetPostOperationHandlerTests.cs @@ -11,6 +11,7 @@ using Microsoft.OpenApi.OData.Edm; using Microsoft.OpenApi.OData.Tests; using Microsoft.OpenApi.OData.Vocabulary.Core; +using System.Linq; using System.Xml.Linq; using Xunit; @@ -79,26 +80,22 @@ private void VerifyEntitySetPostOperation(string annotation, bool enableOperatio if (!string.IsNullOrEmpty(annotation)) { // RequestBody - Assert.Equal(2, post.RequestBody.Content.Keys.Count); + Assert.Single(post.RequestBody.Content.Keys); Assert.True(post.RequestBody.Content.ContainsKey("application/todo")); - Assert.True(post.RequestBody.Content.ContainsKey(Constants.ApplicationJsonMediaType)); // Response - Assert.Equal(2, post.Responses[statusCode].Content.Keys.Count); + Assert.Single(post.Responses[statusCode].Content.Keys); Assert.True(post.Responses[statusCode].Content.ContainsKey("application/todo")); - Assert.True(post.Responses[statusCode].Content.ContainsKey(Constants.ApplicationJsonMediaType)); } else { // RequestBody - Assert.Equal(2, post.RequestBody.Content.Keys.Count); + Assert.Single(post.RequestBody.Content.Keys); Assert.True(post.RequestBody.Content.ContainsKey(Constants.ApplicationOctetStreamMediaType)); - Assert.True(post.RequestBody.Content.ContainsKey(Constants.ApplicationJsonMediaType)); // Response - Assert.Equal(2, post.Responses[statusCode].Content.Keys.Count); + Assert.Single(post.Responses[statusCode].Content.Keys); Assert.True(post.Responses[statusCode].Content.ContainsKey(Constants.ApplicationOctetStreamMediaType)); - Assert.True(post.Responses[statusCode].Content.ContainsKey(Constants.ApplicationJsonMediaType)); } } else @@ -229,6 +226,27 @@ public void CreateEntitySetPostReturnsSecurityForInsertRestrictions(bool enableA { Assert.Empty(post.Security); } + } + + [Fact] + public void CreateEntitySetPostOperationReturnsCorrectOperationWithAnnotatedRequestBodyAndResponseContent() + { + IEdmModel model = OData.Tests.EdmModelHelper.GraphBetaModel; + OpenApiConvertSettings settings = new(); + ODataContext context = new(model, settings); + IEdmEntitySet entitySet = model.EntityContainer.FindEntitySet("directoryObjects"); + Assert.NotNull(entitySet); + + ODataPath path = new(new ODataNavigationSourceSegment(entitySet)); + + // Act + var operation = _operationHandler.CreateOperation(context, path); + + // Assert + Assert.NotNull(operation.RequestBody); + Assert.Equal("multipart/form-data", operation.RequestBody.Content.First().Key); + Assert.NotNull(operation.Responses); + Assert.Equal("multipart/form-data", operation.Responses.First().Value.Content.First().Key); } internal static IEdmModel GetEdmModel(string annotation, bool hasStream = false) diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/NavigationPropertyPatchOperationHandlerTests.cs b/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/NavigationPropertyPatchOperationHandlerTests.cs index 3adb2e6f..78fc292c 100644 --- a/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/NavigationPropertyPatchOperationHandlerTests.cs +++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/NavigationPropertyPatchOperationHandlerTests.cs @@ -77,6 +77,27 @@ public void CreateNavigationPatchOperationReturnsCorrectOperation(bool enableOpe { Assert.Null(operation.OperationId); } + } + + [Fact] + public void CreateNavigationPatchOperationReturnsCorrectOperationWithAnnotatedRequestBodyContent() + { + IEdmModel model = EdmModelHelper.GraphBetaModel; + OpenApiConvertSettings settings = new(); + ODataContext context = new(model, settings); + IEdmSingleton sTon = model.EntityContainer.FindSingleton("identity"); + Assert.NotNull(sTon); + IEdmEntityType entity = model.SchemaElements.OfType().First(c => c.Name == "identityContainer"); + IEdmNavigationProperty navProperty = entity.DeclaredNavigationProperties().First(c => c.Name == "apiConnectors"); + + ODataPath path = new(new ODataNavigationSourceSegment(sTon), new ODataNavigationPropertySegment(navProperty)); + + // Act + var operation = _operationHandler.CreateOperation(context, path); + + // Assert + Assert.NotNull(operation.RequestBody); + Assert.Equal("application/xhtml+xml", operation.RequestBody.Content.First().Key); } [Theory] diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/NavigationPropertyPostOperationHandlerTests.cs b/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/NavigationPropertyPostOperationHandlerTests.cs index 34824c6b..28b3fb39 100644 --- a/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/NavigationPropertyPostOperationHandlerTests.cs +++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/NavigationPropertyPostOperationHandlerTests.cs @@ -70,6 +70,27 @@ public void CreateNavigationPostOperationReturnsCorrectOperation(bool enableOper } } + [Fact] + public void CreateNavigationPostOperationReturnsCorrectOperationWithAnnotatedRequestBodyContent() + { + IEdmModel model = EdmModelHelper.GraphBetaModel; + OpenApiConvertSettings settings = new(); + ODataContext context = new(model, settings); + IEdmSingleton sTon = model.EntityContainer.FindSingleton("identity"); + Assert.NotNull(sTon); + IEdmEntityType entity = model.SchemaElements.OfType().First(c => c.Name == "identityContainer"); + IEdmNavigationProperty navProperty = entity.DeclaredNavigationProperties().First(c => c.Name == "apiConnectors"); + + ODataPath path = new(new ODataNavigationSourceSegment(sTon), new ODataNavigationPropertySegment(navProperty)); + + // Act + var operation = _operationHandler.CreateOperation(context, path); + + // Assert + Assert.NotNull(operation.RequestBody); + Assert.Equal("application/xhtml+xml", operation.RequestBody.Content.First().Key); + } + [Theory] [InlineData(true)] [InlineData(false)] diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/Resources/Graph.Beta.OData.xml b/test/Microsoft.OpenAPI.OData.Reader.Tests/Resources/Graph.Beta.OData.xml index 9c941dd4..f6af4ef0 100644 --- a/test/Microsoft.OpenAPI.OData.Reader.Tests/Resources/Graph.Beta.OData.xml +++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/Resources/Graph.Beta.OData.xml @@ -112784,6 +112784,26 @@ + + + + + + application/xhtml+xml + + + + + + + + + application/xhtml+xml + + + + + @@ -112831,6 +112851,29 @@ + + + + + multipart/form-data + + + + + + + + + multipart/form-data + + + + + multipart/form-data + + + +