Skip to content

Commit

Permalink
Merge pull request #431 from MartinM85/mm/composable-functions
Browse files Browse the repository at this point in the history
Support for composable functions
  • Loading branch information
baywet authored Nov 28, 2023
2 parents 6272df9 + a943b5e commit f97b4e3
Show file tree
Hide file tree
Showing 8 changed files with 184 additions and 21 deletions.
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();

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

0 comments on commit f97b4e3

Please sign in to comment.