diff --git a/src/Microsoft.OpenApi.OData.Reader/Common/Constants.cs b/src/Microsoft.OpenApi.OData.Reader/Common/Constants.cs index 69d2e5bc..2bb9dd77 100644 --- a/src/Microsoft.OpenApi.OData.Reader/Common/Constants.cs +++ b/src/Microsoft.OpenApi.OData.Reader/Common/Constants.cs @@ -194,5 +194,25 @@ internal static class Constants /// count segment identifier /// public const string CountSegmentIdentifier = "count"; + + /// + /// content string + /// + public const string Content = "content"; + + /// + /// Success string + /// + public const string Success = "Success"; + + /// + /// Created string + /// + public const string Created = "Created"; + + /// + /// error string + /// + public const string Error = "error"; } } diff --git a/src/Microsoft.OpenApi.OData.Reader/Common/OpenApiOperationExtensions.cs b/src/Microsoft.OpenApi.OData.Reader/Common/OpenApiOperationExtensions.cs index 36150489..3eb56130 100644 --- a/src/Microsoft.OpenApi.OData.Reader/Common/OpenApiOperationExtensions.cs +++ b/src/Microsoft.OpenApi.OData.Reader/Common/OpenApiOperationExtensions.cs @@ -20,48 +20,49 @@ public static class OpenApiOperationExtensions /// /// The operation. /// The settings. - /// Whether to add a 204 no content response. + /// Optional: Whether to add a 204 no content response. /// Optional: The OpenAPI schema of the response. public static void AddErrorResponses(this OpenApiOperation operation, OpenApiConvertSettings settings, bool addNoContent = false, OpenApiSchema schema = null) { - if (operation == null) { - throw Error.ArgumentNull(nameof(operation)); - } - if(settings == null) { - throw Error.ArgumentNull(nameof(settings)); - } - - if(operation.Responses == null) + Utils.CheckArgumentNull(operation, nameof(operation)); + Utils.CheckArgumentNull(settings, nameof(settings)); + + if (operation.Responses == null) { operation.Responses = new(); } if (addNoContent) - { - if (settings.UseSuccessStatusCodeRange && schema != null) + { + if (settings.UseSuccessStatusCodeRange) { - OpenApiResponse response = new() + OpenApiResponse response = null; + if (schema != null) { - Content = new Dictionary + response = new() { + Description = Constants.Success, + Content = new Dictionary { - Constants.ApplicationJsonMediaType, - new OpenApiMediaType { - Schema = schema + Constants.ApplicationJsonMediaType, + new OpenApiMediaType + { + Schema = schema + } } } - } - }; - operation.Responses.Add(Constants.StatusCodeClass2XX, response); + }; + } + operation.Responses.Add(Constants.StatusCodeClass2XX, response ?? Constants.StatusCodeClass2XX.GetResponse()); } else { operation.Responses.Add(Constants.StatusCode204, Constants.StatusCode204.GetResponse()); } - } + } - if(settings.ErrorResponsesAsDefault) + if (settings.ErrorResponsesAsDefault) { operation.Responses.Add(Constants.StatusCodeDefault, Constants.StatusCodeDefault.GetResponse()); } diff --git a/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPathProvider.cs b/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPathProvider.cs index 90f8f240..0c9b8276 100644 --- a/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPathProvider.cs +++ b/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPathProvider.cs @@ -385,7 +385,7 @@ private void RetrieveMediaEntityStreamPaths(IEdmEntityType entityType, ODataPath currentPath.Pop(); } - if (sp.Name.Equals("content", StringComparison.OrdinalIgnoreCase)) + if (sp.Name.Equals(Constants.Content, StringComparison.OrdinalIgnoreCase)) { createValuePath = false; } diff --git a/src/Microsoft.OpenApi.OData.Reader/Generator/OpenApiResponseGenerator.cs b/src/Microsoft.OpenApi.OData.Reader/Generator/OpenApiResponseGenerator.cs index 46ec5596..e11a1a02 100644 --- a/src/Microsoft.OpenApi.OData.Reader/Generator/OpenApiResponseGenerator.cs +++ b/src/Microsoft.OpenApi.OData.Reader/Generator/OpenApiResponseGenerator.cs @@ -27,19 +27,21 @@ internal static class OpenApiResponseGenerator Reference = new OpenApiReference { Type = ReferenceType.Response, - Id = "error" + Id = Constants.Error } } }, - { Constants.StatusCode204, new OpenApiResponse { Description = "Success"} }, + { Constants.StatusCode204, new OpenApiResponse { Description = Constants.Success} }, + { Constants.StatusCode201, new OpenApiResponse { Description = Constants.Created} }, + { Constants.StatusCodeClass2XX, new OpenApiResponse { Description = Constants.Success} }, { Constants.StatusCodeClass4XX, new OpenApiResponse { UnresolvedReference = true, Reference = new OpenApiReference { Type = ReferenceType.Response, - Id = "error" + Id = Constants.Error } } }, @@ -49,7 +51,7 @@ internal static class OpenApiResponseGenerator Reference = new OpenApiReference { Type = ReferenceType.Response, - Id = "error" + Id = Constants.Error } } } 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 b194e40c..89bd34d7 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.3.0-preview2 + 1.3.0-preview3 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 @@ -25,6 +25,7 @@ - Skips adding a $count path if a similar count() function path exists #347 - Checks whether path exists before adding it to the paths dictionary #343 - Strips namespace prefix from operation segments and aliases type cast segments #348 +- Return response status code 2XX for PUT operations of stream properties when UseSuccessStatusCodeRange is enabled #310 Microsoft.OpenApi.OData.Reader ..\..\tool\Microsoft.OpenApi.OData.snk diff --git a/src/Microsoft.OpenApi.OData.Reader/Operation/MediaEntityGetOperationHandler.cs b/src/Microsoft.OpenApi.OData.Reader/Operation/MediaEntityGetOperationHandler.cs index f04af78c..ccfe15bd 100644 --- a/src/Microsoft.OpenApi.OData.Reader/Operation/MediaEntityGetOperationHandler.cs +++ b/src/Microsoft.OpenApi.OData.Reader/Operation/MediaEntityGetOperationHandler.cs @@ -34,23 +34,23 @@ protected override void SetBasicInfo(OpenApiOperation operation) // Description if (LastSegmentIsStreamPropertySegment) { - IEdmVocabularyAnnotatable annotatable = GetAnnotatableElement(); + (_, var property) = GetStreamElements(); string description; - if (annotatable is IEdmNavigationProperty) + if (property is IEdmNavigationProperty) { - ReadRestrictionsType readRestriction = Context.Model.GetRecord(annotatable, CapabilitiesConstants.NavigationRestrictions)? + ReadRestrictionsType readRestriction = Context.Model.GetRecord(property, CapabilitiesConstants.NavigationRestrictions)? .RestrictedProperties?.FirstOrDefault()?.ReadRestrictions; description = LastSegmentIsKeySegment ? readRestriction?.ReadByKeyRestrictions?.Description : readRestriction?.Description - ?? Context.Model.GetDescriptionAnnotation(annotatable); + ?? Context.Model.GetDescriptionAnnotation(property); } else { // Structural property - description = Context.Model.GetDescriptionAnnotation(annotatable); + description = Context.Model.GetDescriptionAnnotation(property); } operation.Description = description; diff --git a/src/Microsoft.OpenApi.OData.Reader/Operation/MediaEntityOperationalHandler.cs b/src/Microsoft.OpenApi.OData.Reader/Operation/MediaEntityOperationalHandler.cs index 14043905..e53ad674 100644 --- a/src/Microsoft.OpenApi.OData.Reader/Operation/MediaEntityOperationalHandler.cs +++ b/src/Microsoft.OpenApi.OData.Reader/Operation/MediaEntityOperationalHandler.cs @@ -145,11 +145,11 @@ protected IDictionary GetContentDescription() }; // Fetch the respective AcceptableMediaTypes - IEdmVocabularyAnnotatable annotatableElement = GetAnnotatableElement(); + (_, var property) = GetStreamElements(); IEnumerable mediaTypes = null; - if (annotatableElement != null) + if (property != null) { - mediaTypes = Context.Model.GetCollection(annotatableElement, + mediaTypes = Context.Model.GetCollection(property, CoreConstants.AcceptableMediaTypes); } @@ -173,13 +173,13 @@ protected IDictionary GetContentDescription() } /// - /// Gets the annotatable stream property from the path segments. + /// Gets the stream property and entity type declaring the stream property. /// - /// The annotatable stream property. - protected IEdmVocabularyAnnotatable GetAnnotatableElement() + /// The stream property and entity type declaring the stream property. + protected (IEdmEntityType entityType, IEdmProperty property) GetStreamElements() { // Only ODataStreamPropertySegment is annotatable - if (!LastSegmentIsStreamPropertySegment) return null; + if (!LastSegmentIsStreamPropertySegment) return (null, null); // Retrieve the entity type of the segment before the stream property segment var entityType = Path.Segments.ElementAtOrDefault(Path.Segments.Count - 2).EntityType; @@ -192,7 +192,7 @@ protected IEdmVocabularyAnnotatable GetAnnotatableElement() property = GetNavigationProperty(entityType, lastSegmentProp.Identifier); } - return property; + return (entityType, property); } private IEdmStructuralProperty GetStructuralProperty(IEdmEntityType entityType, string identifier) diff --git a/src/Microsoft.OpenApi.OData.Reader/Operation/MediaEntityPutOperationHandler.cs b/src/Microsoft.OpenApi.OData.Reader/Operation/MediaEntityPutOperationHandler.cs index 9430a468..3a46fe73 100644 --- a/src/Microsoft.OpenApi.OData.Reader/Operation/MediaEntityPutOperationHandler.cs +++ b/src/Microsoft.OpenApi.OData.Reader/Operation/MediaEntityPutOperationHandler.cs @@ -3,6 +3,7 @@ // Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. // ------------------------------------------------------------ +using System; using System.Linq; using Microsoft.OData.Edm; using Microsoft.OData.Edm.Vocabularies; @@ -34,20 +35,20 @@ protected override void SetBasicInfo(OpenApiOperation operation) // Description if (LastSegmentIsStreamPropertySegment) { - IEdmVocabularyAnnotatable annotatable = GetAnnotatableElement(); + (_, var property) = GetStreamElements(); string description; - if (annotatable is IEdmNavigationProperty) + if (property is IEdmNavigationProperty) { - UpdateRestrictionsType updateRestriction = Context.Model.GetRecord(annotatable, CapabilitiesConstants.NavigationRestrictions)? + UpdateRestrictionsType updateRestriction = Context.Model.GetRecord(property, CapabilitiesConstants.NavigationRestrictions)? .RestrictedProperties?.FirstOrDefault()?.UpdateRestrictions; - description = updateRestriction?.Description ?? Context.Model.GetDescriptionAnnotation(annotatable); + description = updateRestriction?.Description ?? Context.Model.GetDescriptionAnnotation(property); } else { // Structural property - description = Context.Model.GetDescriptionAnnotation(annotatable); + description = Context.Model.GetDescriptionAnnotation(property); } operation.Description = description; @@ -77,7 +78,28 @@ protected override void SetRequestBody(OpenApiOperation operation) /// protected override void SetResponses(OpenApiOperation operation) { - operation.AddErrorResponses(Context.Settings, true); + if (LastSegmentIsStreamPropertySegment && Path.LastSegment.Identifier.Equals(Constants.Content, StringComparison.OrdinalIgnoreCase)) + { + // Get the entity type declaring this stream property. + (var entityType, _) = GetStreamElements(); + + OpenApiSchema schema = new() + { + UnresolvedReference = true, + Reference = new OpenApiReference + { + Type = ReferenceType.Schema, + Id = entityType.FullName() + } + }; + + operation.AddErrorResponses(Context.Settings, addNoContent: true, schema: schema); + } + else + { + operation.AddErrorResponses(Context.Settings, true); + } + base.SetResponses(operation); } diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/MediaEntityGetOperationHandlerTests.cs b/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/MediaEntityGetOperationHandlerTests.cs index b67921bd..85f7fc52 100644 --- a/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/MediaEntityGetOperationHandlerTests.cs +++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/MediaEntityGetOperationHandlerTests.cs @@ -135,6 +135,7 @@ public static IEdmModel GetEdmModel(string annotation) + diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/MediaEntityPutOperationHandlerTests.cs b/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/MediaEntityPutOperationHandlerTests.cs index 8fdc3a93..7564e4b0 100644 --- a/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/MediaEntityPutOperationHandlerTests.cs +++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/Operation/MediaEntityPutOperationHandlerTests.cs @@ -17,9 +17,11 @@ public class MediaEntityPutOperationHandlerTests private readonly MediaEntityPutOperationHandler _operationalHandler = new MediaEntityPutOperationHandler(); [Theory] - [InlineData(true)] - [InlineData(false)] - public void CreateMediaEntityPutOperationReturnsCorrectOperation(bool enableOperationId) + [InlineData(true, false)] + [InlineData(true, true)] + [InlineData(false, false)] + [InlineData(false, true)] + public void CreateMediaEntityPutOperationReturnsCorrectOperation(bool enableOperationId, bool useSuccessStatusCodeRange) { // Arrange string qualifiedName = CoreConstants.AcceptableMediaTypes; @@ -33,17 +35,18 @@ public void CreateMediaEntityPutOperationReturnsCorrectOperation(bool enableOper "; // Assert - VerifyMediaEntityPutOperation("", enableOperationId); - VerifyMediaEntityPutOperation(annotation, enableOperationId); + VerifyMediaEntityPutOperation("", enableOperationId, useSuccessStatusCodeRange); + VerifyMediaEntityPutOperation(annotation, enableOperationId, useSuccessStatusCodeRange); } - private void VerifyMediaEntityPutOperation(string annotation, bool enableOperationId) + private void VerifyMediaEntityPutOperation(string annotation, bool enableOperationId, bool useSuccessStatusCodeRange) { // Arrange IEdmModel model = MediaEntityGetOperationHandlerTests.GetEdmModel(annotation); OpenApiConvertSettings settings = new OpenApiConvertSettings { - EnableOperationId = enableOperationId + EnableOperationId = enableOperationId, + UseSuccessStatusCodeRange = useSuccessStatusCodeRange }; ODataContext context = new ODataContext(model, settings); @@ -63,13 +66,20 @@ private void VerifyMediaEntityPutOperation(string annotation, bool enableOperati new ODataNavigationPropertySegment(navProperty), new ODataStreamContentSegment()); + IEdmStructuralProperty sp2 = todo.StructuralProperties().First(c => c.Name == "Content"); + ODataPath path3 = new(new ODataNavigationSourceSegment(todos), + new ODataKeySegment(todos.EntityType()), + new ODataStreamPropertySegment(sp2.Name)); + // Act var putOperation = _operationalHandler.CreateOperation(context, path); var putOperation2 = _operationalHandler.CreateOperation(context, path2); + var putOperation3 = _operationalHandler.CreateOperation(context, path3); // Assert Assert.NotNull(putOperation); Assert.NotNull(putOperation2); + Assert.NotNull(putOperation3); Assert.Equal("Update Logo for Todo in Todos", putOperation.Summary); Assert.Equal("Update media content for the navigation property photo in me", putOperation2.Summary); Assert.NotNull(putOperation.Tags); @@ -82,10 +92,28 @@ private void VerifyMediaEntityPutOperation(string annotation, bool enableOperati Assert.NotNull(putOperation.Responses); Assert.NotNull(putOperation2.Responses); + Assert.NotNull(putOperation3.Responses); + Assert.Equal(2, putOperation.Responses.Count); Assert.Equal(2, putOperation2.Responses.Count); - Assert.Equal(new[] { "204", "default" }, putOperation.Responses.Select(r => r.Key)); - Assert.Equal(new[] { "204", "default" }, putOperation2.Responses.Select(r => r.Key)); + Assert.Equal(2, putOperation3.Responses.Count); + + var statusCode = (useSuccessStatusCodeRange) ? Constants.StatusCodeClass2XX : Constants.StatusCode204; + Assert.Equal(new[] { statusCode, "default" }, putOperation.Responses.Select(r => r.Key)); + Assert.Equal(new[] { statusCode, "default" }, putOperation2.Responses.Select(r => r.Key)); + Assert.Equal(new[] { statusCode, "default" }, putOperation3.Responses.Select(r => r.Key)); + + // Test only for stream properties of identifier 'content' + if (useSuccessStatusCodeRange) + { + var referenceId = putOperation3.Responses[statusCode]?.Content[Constants.ApplicationJsonMediaType]?.Schema?.Reference.Id; + Assert.NotNull(referenceId); + Assert.Equal("microsoft.graph.Todo", referenceId); + } + else + { + Assert.Empty(putOperation3.Responses[statusCode].Content); + } if (!string.IsNullOrEmpty(annotation)) {