diff --git a/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPath.cs b/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPath.cs index 14555997..254d1325 100644 --- a/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPath.cs +++ b/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPath.cs @@ -313,7 +313,7 @@ private ODataPathKind CalcPathType() { return ODataPathKind.OperationImport; } - else if (Segments.Any(c => c.Kind == ODataSegmentKind.Operation)) + else if (Segments.Last().Kind == ODataSegmentKind.Operation) { return ODataPathKind.Operation; } diff --git a/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPathProvider.cs b/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPathProvider.cs index f2a7a25e..7858d858 100644 --- a/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPathProvider.cs +++ b/src/Microsoft.OpenApi.OData.Reader/Edm/ODataPathProvider.cs @@ -771,7 +771,8 @@ bool filter(IEdmStructuredType x) => /// private void RetrieveBoundOperationPaths(OpenApiConvertSettings convertSettings) { - foreach (var edmOperation in _model.GetAllElements().OfType().Where(e => e.IsBound)) + var edmOperations = _model.GetAllElements().OfType().Where(x => x.IsBound).ToArray(); + foreach (var edmOperation in edmOperations) { if (!CanFilter(edmOperation)) { @@ -791,20 +792,8 @@ private void RetrieveBoundOperationPaths(OpenApiConvertSettings convertSettings) continue; } - var firstEntityType = bindingType.AsEntity().EntityDefinition(); - - bool filter(IEdmNavigationSource z) => - z.EntityType() != firstEntityType && - z.EntityType().FindAllBaseTypes().Contains(firstEntityType); + var allEntitiesForOperation = GetAllEntitiesForOperation(bindingType); - var allEntitiesForOperation = new IEdmEntityType[] { firstEntityType } - .Union(_model.EntityContainer.EntitySets() - .Where(filter).Select(x => x.EntityType())) //Search all EntitySets - .Union(_model.EntityContainer.Singletons() - .Where(filter).Select(x => x.EntityType())) //Search all singletons - .Distinct() - .ToList(); - foreach (var bindingEntityType in allEntitiesForOperation) { // 1. Search for corresponding navigation source path @@ -820,7 +809,81 @@ bool filter(IEdmNavigationSource z) => AppendBoundOperationOnDerivedNavigationPropertyPath(edmOperation, isCollection, bindingEntityType, convertSettings); } } + + // all operations appended to properties + // append bound operations to functions + foreach (var edmOperation in edmOperations) + { + if (!CanFilter(edmOperation)) + { + continue; + } + + IEdmOperationParameter bindingParameter = edmOperation.Parameters.First(); + IEdmTypeReference bindingType = bindingParameter.Type; + + bool isCollection = bindingType.IsCollection(); + if (isCollection) + { + bindingType = bindingType.AsCollection().ElementType(); + } + if (!bindingType.IsEntity()) + { + continue; + } + + var allEntitiesForOperation = GetAllEntitiesForOperation(bindingType); + + foreach (var bindingEntityType in allEntitiesForOperation) + { + AppendBoundOperationOnOperationPath(edmOperation, isCollection, bindingEntityType); + } + } + + // append navigation properties to functions with return type + var functionPaths = _allOperationPaths.Where(x => x.LastSegment is ODataOperationSegment operationSegment + && operationSegment.Operation is IEdmFunction edmFunction + && edmFunction.IsComposable + && edmFunction.ReturnType != null + && edmFunction.ReturnType.Definition is IEdmEntityType returnBindingEntityType); + + foreach( var functionPath in functionPaths) + { + if (functionPath.LastSegment is not ODataOperationSegment operationSegment + || operationSegment.Operation is not IEdmFunction edmFunction + || !edmFunction.IsComposable + || edmFunction.ReturnType == null + || edmFunction.ReturnType.Definition is not IEdmEntityType returnBindingEntityType) + { + continue; + } + + foreach (var navProperty in returnBindingEntityType.NavigationProperties()) + { + ODataPath newNavigationPath = functionPath.Clone(); + newNavigationPath.Push(new ODataNavigationPropertySegment(navProperty)); + AppendPath(newNavigationPath); + } + } } + + private List GetAllEntitiesForOperation(IEdmTypeReference bindingType) + { + var firstEntityType = bindingType.AsEntity().EntityDefinition(); + + bool filter(IEdmNavigationSource z) => + z.EntityType() != firstEntityType && + z.EntityType().FindAllBaseTypes().Contains(firstEntityType); + + return new IEdmEntityType[] { firstEntityType } + .Union(_model.EntityContainer.EntitySets() + .Where(filter).Select(x => x.EntityType())) //Search all EntitySets + .Union(_model.EntityContainer.Singletons() + .Where(filter).Select(x => x.EntityType())) //Search all singletons + .Distinct() + .ToList(); + } + private static readonly HashSet _oDataPathKindsToSkipForOperationsWhenSingle = new() { ODataPathKind.EntitySet, ODataPathKind.MediaEntity, @@ -1056,5 +1119,32 @@ private void AppendBoundOperationOnDerivedNavigationPropertyPath( } } } + + private void AppendBoundOperationOnOperationPath(IEdmOperation edmOperation, bool isCollection, IEdmEntityType bindingEntityType) + { + bool isEscapedFunction = _model.IsUrlEscapeFunction(edmOperation); + + // only composable functions + var paths = _allOperationPaths.Where(x => x.LastSegment is ODataOperationSegment operationSegment + && operationSegment.Operation is IEdmFunction edmFunction + && edmFunction.IsComposable).ToList(); + + foreach (var path in paths) + { + if (path.LastSegment is not ODataOperationSegment operationSegment + || (path.Segments.Count > 1 && path.Segments[path.Segments.Count - 2] is ODataOperationSegment) + || operationSegment.Operation is not IEdmFunction edmFunction || !edmFunction.IsComposable + || edmFunction.ReturnType == null || !edmFunction.ReturnType.Definition.Equals(bindingEntityType) + || isCollection + || !EdmModelHelper.IsOperationAllowed(_model, edmOperation, operationSegment.Operation, true)) + { + continue; + } + + ODataPath newOperationPath = path.Clone(); + newOperationPath.Push(new ODataOperationSegment(edmOperation, isEscapedFunction, _model)); + AppendPath(newOperationPath); + } + } } } 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 5a376e6a..256d446e 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.5.0-preview9 + 1.5.0-preview10 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 @@ -27,6 +27,7 @@ - Adds support for `x-ms-enum-flags` extension for flagged enums - Use containment together with RequiresExplicitBinding annotation to check whether to append bound operations to navigation properties #430 - Adds schema to content types of stream properties that have a collection of acceptable media types #435 +- Adds support for composable functions #431 - Retrieves complex properties of derived types #437 - Updates operationIds of navigation property paths with OData type cast segments #442 - Generate navigation property paths defined in nested complex properties #446 diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/Common/EdmModelHelper.cs b/test/Microsoft.OpenAPI.OData.Reader.Tests/Common/EdmModelHelper.cs index eec5e4b9..bc00e640 100644 --- a/test/Microsoft.OpenAPI.OData.Reader.Tests/Common/EdmModelHelper.cs +++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/Common/EdmModelHelper.cs @@ -43,6 +43,8 @@ public class EdmModelHelper public static IEdmModel GraphBetaModel { get; } + public static IEdmModel ComposableFunctionsModel { get; } + static EdmModelHelper() { MultipleInheritanceEdmModel = CreateMultipleInheritanceEdmModel(); @@ -53,6 +55,7 @@ static EdmModelHelper() GraphBetaModel = LoadEdmModel("Graph.Beta.OData.xml"); MultipleSchemasEdmModel = LoadEdmModel("Multiple.Schema.OData.xml"); InheritanceEdmModelAcrossReferences = CreateInheritanceEdmModelAcrossReferences(); + ComposableFunctionsModel = LoadEdmModel("ComposableFunctions.OData.xml"); } private static IEdmModel LoadEdmModel(string source) diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/Edm/ODataPathProviderTests.cs b/test/Microsoft.OpenAPI.OData.Reader.Tests/Edm/ODataPathProviderTests.cs index cc668c13..27c3a8ea 100644 --- a/test/Microsoft.OpenAPI.OData.Reader.Tests/Edm/ODataPathProviderTests.cs +++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/Edm/ODataPathProviderTests.cs @@ -52,7 +52,7 @@ public void GetPathsForGraphBetaModelReturnsAllPaths() // Assert Assert.NotNull(paths); - Assert.Equal(18288, paths.Count()); + Assert.Equal(16967, paths.Count()); AssertGraphBetaModelPaths(paths); } @@ -113,7 +113,7 @@ public void GetPathsForGraphBetaModelWithDerivedTypesConstraintReturnsAllPaths() // Assert Assert.NotNull(paths); - Assert.Equal(18939, paths.Count()); + Assert.Equal(17618, paths.Count()); } [Theory] @@ -159,6 +159,28 @@ public void UseCountRestrictionsAnnotationsToAppendDollarCountSegmentsToNavigati } } + [Fact] + public void GetPathsForComposableFunctionsReturnsAllPaths() + { + // Arrange + IEdmModel model = EdmModelHelper.ComposableFunctionsModel; + ODataPathProvider provider = new ODataPathProvider(); + var settings = new OpenApiConvertSettings + { + RequireDerivedTypesConstraintForBoundOperations = true, + AppendBoundOperationsOnDerivedTypeCastSegments = true + }; + + // Act + var paths = provider.GetPaths(model, settings); + + // Assert + Assert.NotNull(paths); + Assert.Equal(38, paths.Count()); + Assert.Equal(20, paths.Where(p => p.LastSegment is ODataOperationSegment).Count()); + Assert.Equal(12, paths.Where(p => p.Segments.Count > 1 && p.LastSegment is ODataNavigationPropertySegment && p.Segments[p.Segments.Count - 2] is ODataOperationSegment).Count()); + } + [Fact] public void GetPathsDoesntReturnPathsForCountWhenDisabled() { diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/Microsoft.OpenAPI.OData.Reader.Tests.csproj b/test/Microsoft.OpenAPI.OData.Reader.Tests/Microsoft.OpenAPI.OData.Reader.Tests.csproj index e57bc2d3..a2998909 100644 --- a/test/Microsoft.OpenAPI.OData.Reader.Tests/Microsoft.OpenAPI.OData.Reader.Tests.csproj +++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/Microsoft.OpenAPI.OData.Reader.Tests.csproj @@ -17,6 +17,7 @@ + @@ -34,6 +35,7 @@ + diff --git a/test/Microsoft.OpenAPI.OData.Reader.Tests/Resources/ComposableFunctions.OData.xml b/test/Microsoft.OpenAPI.OData.Reader.Tests/Resources/ComposableFunctions.OData.xml new file mode 100644 index 00000000..27504bda --- /dev/null +++ b/test/Microsoft.OpenAPI.OData.Reader.Tests/Resources/ComposableFunctions.OData.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file 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 461a2895..ebf03e5f 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 @@ -1,4 +1,5 @@  + @@ -53499,15 +53500,18 @@ - + microsoft.graph.user microsoft.graph.group microsoft.graph.application + microsoft.graph.servicePrincipal + microsoft.graph.administrativeUnit + microsoft.graph.device - + microsoft.graph.restore @@ -81960,7 +81964,6 @@ -