Skip to content
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

Support for composable functions #431

Merged
merged 11 commits into from
Nov 28, 2023
2 changes: 1 addition & 1 deletion src/Microsoft.OpenApi.OData.Reader/Edm/ODataPath.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
118 changes: 104 additions & 14 deletions src/Microsoft.OpenApi.OData.Reader/Edm/ODataPathProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -771,7 +771,8 @@ bool filter(IEdmStructuredType x) =>
/// </summary>
private void RetrieveBoundOperationPaths(OpenApiConvertSettings convertSettings)
{
foreach (var edmOperation in _model.GetAllElements().OfType<IEdmOperation>().Where(e => e.IsBound))
var edmOperations = _model.GetAllElements().OfType<IEdmOperation>().Where(x => x.IsBound).ToArray();
foreach (var edmOperation in edmOperations)
{
if (!CanFilter(edmOperation))
{
Expand All @@ -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
Expand All @@ -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<IEdmEntityType> 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<ODataPathKind> _oDataPathKindsToSkipForOperationsWhenSingle = new() {
ODataPathKind.EntitySet,
ODataPathKind.MediaEntity,
Expand Down Expand Up @@ -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();
baywet marked this conversation as resolved.
Show resolved Hide resolved

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);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<TargetFrameworks>netstandard2.0</TargetFrameworks>
<PackageId>Microsoft.OpenApi.OData</PackageId>
<SignAssembly>true</SignAssembly>
<Version>1.5.0-preview9</Version>
<Version>1.5.0-preview10</Version>
<Description>This package contains the codes you need to convert OData CSDL to Open API Document of Model.</Description>
<Copyright>© Microsoft Corporation. All rights reserved.</Copyright>
<PackageTags>Microsoft OpenApi OData EDM</PackageTags>
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ public class EdmModelHelper

public static IEdmModel GraphBetaModel { get; }

public static IEdmModel ComposableFunctionsModel { get; }

static EdmModelHelper()
{
MultipleInheritanceEdmModel = CreateMultipleInheritanceEdmModel();
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public void GetPathsForGraphBetaModelReturnsAllPaths()

// Assert
Assert.NotNull(paths);
Assert.Equal(18288, paths.Count());
Assert.Equal(16967, paths.Count());
AssertGraphBetaModelPaths(paths);
}

Expand Down Expand Up @@ -113,7 +113,7 @@ public void GetPathsForGraphBetaModelWithDerivedTypesConstraintReturnsAllPaths()

// Assert
Assert.NotNull(paths);
Assert.Equal(18939, paths.Count());
Assert.Equal(17618, paths.Count());
}

[Theory]
Expand Down Expand Up @@ -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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
<None Remove="Resources\Basic.OpenApi.V2.json" />
<None Remove="Resources\Basic.OpenApi.yaml" />
<None Remove="Resources\Basic.OpenApi.V2.yaml" />
<None Remove="Resources\ComposableFunctions.OData.xml" />
<None Remove="Resources\Empty.OpenApi.json" />
<None Remove="Resources\Empty.OpenApi.V2.json" />
<None Remove="Resources\Empty.OpenApi.yaml" />
Expand All @@ -34,6 +35,7 @@
</ItemGroup>

<ItemGroup>
<EmbeddedResource Include="Resources\ComposableFunctions.OData.xml" />
<EmbeddedResource Include="Resources\Multiple.Schema.OData.xml" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx">
<edmx:DataServices>
<Schema Namespace="composable.functions" xmlns="http://docs.oasis-open.org/odata/ns/edm">
<EntityType Name="arrayItem">
<Property Name="index" Type="Edm.Int64" Nullable="false" />
<Property Name="value" Type="Edm.String" />
</EntityType>
<EntityType Name="array">
<Key>
<PropertyRef Name="id" />
</Key>
<Property Name="id" Type="Edm.Int64" Nullable="false" />
<Property Name="size" Type="Edm.Int64"/>
<NavigationProperty Name="items" Type="Collection(composable.functions.arrayItem)"/>
</EntityType>
<Action Name="clean" IsBound="true">
<Parameter Name="bindingParameter" Type="composable.functions.array"/>
</Action>
<Function Name="range" IsBound="true" IsComposable="true">
<Parameter Name="bindingParameter" Type="composable.functions.array"/>
<ReturnType Type="composable.functions.array"/>
</Function>
<Function Name="filter" IsBound="true" IsComposable="true">
<Parameter Name="bindingParameter" Type="composable.functions.array"/>
<ReturnType Type="composable.functions.array"/>
</Function>
<Function Name="sort" IsBound="true" IsComposable="true">
<Parameter Name="bindingParameter" Type="composable.functions.array"/>
<ReturnType Type="composable.functions.array"/>
</Function>
<Function Name="itemAt" IsBound="true">
<Parameter Name="bindingParameter" Type="composable.functions.array"/>
<Parameter Name="index" Type="Edm.Int32"/>
<ReturnType Type="composable.functions.arrayItem"/>
</Function>
<EntityContainer Name="ComposableFunctions">
<EntitySet Name="arrays" EntityType="composable.functions.array"/>
</EntityContainer>
</Schema>
</edmx:DataServices>
</edmx:Edmx>
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type='text/xsl' href='.\transforms\csdl\preprocess_csdl.xsl'?>
<edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx">
<edmx:DataServices>
<Schema Namespace="microsoft.graph.identityGovernance" xmlns="http://docs.oasis-open.org/odata/ns/edm">
Expand Down Expand Up @@ -53499,15 +53500,18 @@
<NavigationProperty Name="customSecurityAttributeDefinitions" Type="Collection(graph.customSecurityAttributeDefinition)" ContainsTarget="true">
<Annotation Term="Org.OData.Core.V1.Description" String="Schema of a custom security attributes (key-value pairs)." />
</NavigationProperty>
<NavigationProperty Name="deletedItems" Type="Collection(graph.directoryObject)" ContainsTarget="true">
<NavigationProperty Name="deletedItems" Type="Collection(graph.directoryObject)">
<Annotation Term="Org.OData.Validation.V1.DerivedTypeConstraint">
<Collection>
<String>microsoft.graph.user</String>
<String>microsoft.graph.group</String>
<String>microsoft.graph.application</String>
<String>microsoft.graph.servicePrincipal</String>
<String>microsoft.graph.administrativeUnit</String>
<String>microsoft.graph.device</String>
</Collection>
</Annotation>
<Annotation Term="Org.OData.Core.V1.ExplicitOperationBindings">
<Annotation Term="Org.OData.Core.V1.ExplicitOperationBindings">
<Collection>
<String>microsoft.graph.restore</String>
</Collection>
Expand Down Expand Up @@ -81960,7 +81964,6 @@
<Annotation Term="Org.OData.Core.V1.Description" String="The list of printers registered in the tenant." />
</NavigationProperty>
<NavigationProperty Name="printerShares" Type="Collection(graph.printerShare)" ContainsTarget="true" />
<NavigationProperty Name="reports" Type="graph.reportRoot" ContainsTarget="true" />
<NavigationProperty Name="services" Type="Collection(graph.printService)" ContainsTarget="true">
<Annotation Term="Org.OData.Core.V1.Description" String="The list of available Universal Print service endpoints." />
</NavigationProperty>
Expand Down
Loading