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

type cast segments for derived types #149

Merged
merged 28 commits into from
Dec 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
e7c15a2
- typo fixes
baywet Nov 24, 2021
16b495b
- draft implementation of odata cast segments
baywet Nov 24, 2021
f1c0abe
- code linting
baywet Nov 25, 2021
8eed01f
- bump language version for test project
baywet Nov 25, 2021
4b187d9
- adds unit test for Cast operation handler & cast path item handler
baywet Nov 25, 2021
1b11a4e
- adds support for entity set down cast
baywet Nov 25, 2021
ca5bd3b
- adds support for casting after key segment
baywet Nov 26, 2021
11d2116
- adds support for single navigation property cast
baywet Nov 26, 2021
da1afbe
- adds support for singleton downcast
baywet Nov 26, 2021
299bfc9
- adds a unit test for the id under navigation property case
baywet Dec 2, 2021
386c327
- adds cast segments for all cases
baywet Dec 3, 2021
9b41326
- refactored cast segments addition to avoid duplication
baywet Dec 3, 2021
9cfbc3d
- adds derived type annotation reading
baywet Dec 3, 2021
3b7c0cf
- adds a setting to require or not the derived type constraint
baywet Dec 3, 2021
0125bc1
- fixes a bug where cast would be missing for single valued properties
baywet Dec 3, 2021
7649358
- adds integration test data for odata cast
baywet Dec 3, 2021
916edbd
- refactors tests for reusability
baywet Dec 3, 2021
a740942
- adds unit tests for odata type cast path provider
baywet Dec 3, 2021
f6c36d1
- removes parameters quotes in sample files following rebase
baywet Dec 7, 2021
0e584c1
- adds count segments under typecast segments
baywet Dec 7, 2021
39bc798
- adds support for down cast navigation properties path items
baywet Dec 8, 2021
b1be8b6
- fixes a bug where single bound operations would be added under type…
baywet Dec 9, 2021
68ef136
- fixes a bug where type cast segments would have operations from par…
baywet Dec 9, 2021
fe92390
- fixes a bug where count or type cast operations would have duplicat…
baywet Dec 9, 2021
1b63951
- fixes tabspacing issues
baywet Dec 9, 2021
b680407
Update src/Microsoft.OpenApi.OData.Reader/Edm/ODataPath.cs
baywet Dec 9, 2021
33bf925
Update src/Microsoft.OpenApi.OData.Reader/Edm/ODataPathProvider.cs
baywet Dec 9, 2021
306e946
- adds missing using following PR feedback
baywet Dec 9, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 21 additions & 16 deletions src/Microsoft.OpenApi.OData.Reader/Edm/ODataPath.cs
Original file line number Diff line number Diff line change
Expand Up @@ -289,13 +289,15 @@ private ODataPathKind CalcPathType()
{
return ODataPathKind.Metadata;
}

if (Segments.Last().Kind == ODataSegmentKind.DollarCount)
else if (Segments.Last().Kind == ODataSegmentKind.DollarCount)
{
return ODataPathKind.DollarCount;
}

if (Segments.Any(c => c.Kind == ODataSegmentKind.StreamProperty || c.Kind == ODataSegmentKind.StreamContent))
else if (Segments.Last().Kind == ODataSegmentKind.TypeCast)
{
return ODataPathKind.TypeCast;
}
else if (Segments.Any(c => c.Kind == ODataSegmentKind.StreamProperty || c.Kind == ODataSegmentKind.StreamContent))
{
return ODataPathKind.MediaEntity;
}
Expand All @@ -315,20 +317,15 @@ private ODataPathKind CalcPathType()
{
return ODataPathKind.NavigationProperty;
}

if (Segments.Count == 1)
else if (Segments.Count == 1 && Segments[0] is ODataNavigationSourceSegment segment)
{
ODataNavigationSourceSegment segment = Segments[0] as ODataNavigationSourceSegment;
if (segment != null)
if (segment.NavigationSource is IEdmSingleton)
{
if (segment.NavigationSource is IEdmSingleton)
{
return ODataPathKind.Singleton;
}
else
{
return ODataPathKind.EntitySet;
}
return ODataPathKind.Singleton;
}
else
{
return ODataPathKind.EntitySet;
}
}
else if (Segments.Count == 2 && Segments.Last().Kind == ODataSegmentKind.Key)
Expand All @@ -338,5 +335,13 @@ private ODataPathKind CalcPathType()

return ODataPathKind.Unknown;
}

/// <summary>
/// Provides a suffix for the operation id based on the operation path.
/// </summary>
/// <param name="settings">The settings.</param>
///<returns>The suffix.</returns>
public string GetPathHash(OpenApiConvertSettings settings) =>
LastSegment.GetPathHash(settings, this);
}
}
9 changes: 7 additions & 2 deletions src/Microsoft.OpenApi.OData.Reader/Edm/ODataPathKind.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,14 @@ public enum ODataPathKind
/// </summary>
DollarCount,

/// <summary>
/// Represents a type cast path, for example: ~/groups/{id}/members/microsoft.graph.user
/// </summary>
TypeCast,

/// <summary>
/// Represents an un-supported/unknown path.
/// </summary>
Unknown
}
Unknown,
}
}
134 changes: 109 additions & 25 deletions src/Microsoft.OpenApi.OData.Reader/Edm/ODataPathProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Linq;
using Microsoft.OData.Edm;
using Microsoft.OData.Edm.Vocabularies;
using Microsoft.OpenApi.OData.Common;
using Microsoft.OpenApi.OData.Vocabulary.Capabilities;

namespace Microsoft.OpenApi.OData.Edm
Expand Down Expand Up @@ -127,6 +128,7 @@ private void AppendPath(ODataPath path)
ODataPathKind kind = path.Kind;
switch(kind)
{
case ODataPathKind.TypeCast:
case ODataPathKind.DollarCount:
case ODataPathKind.Entity:
case ODataPathKind.EntitySet:
Expand Down Expand Up @@ -186,11 +188,19 @@ private void RetrieveNavigationSourcePaths(IEdmNavigationSource navigationSource
if (entitySet != null)
{
count = _model.GetRecord<CountRestrictionsType>(entitySet, CapabilitiesConstants.CountRestrictions);
if(count?.Countable ?? true)
if(count?.Countable ?? true) // ~/entitySet/$count
CreateCountPath(path, convertSettings);

CreateTypeCastPaths(path, convertSettings, entityType, entitySet, true); // ~/entitySet/subType

path.Push(new ODataKeySegment(entityType));
AppendPath(path.Clone());

CreateTypeCastPaths(path, convertSettings, entityType, entitySet, false); // ~/entitySet/{id}/subType
}
else if (navigationSource is IEdmSingleton singleton)
{ // ~/singleton/subType
CreateTypeCastPaths(path, convertSettings, entityType, singleton, false);
}

// media entity
Expand Down Expand Up @@ -285,13 +295,20 @@ private void RetrieveNavigationPropertyPaths(IEdmNavigationProperty navigationPr
IEdmEntityType navEntityType = navigationProperty.ToEntityType();
var targetsMany = navigationProperty.TargetMultiplicity() == EdmMultiplicity.Many;
var propertyPath = navigationProperty.GetPartnerPath()?.Path;
var propertyPathIsEmpty = string.IsNullOrEmpty(propertyPath);

if (targetsMany && (string.IsNullOrEmpty(propertyPath) ||
(count?.IsNonCountableNavigationProperty(propertyPath) ?? true)))
if (targetsMany)
{
// ~/entityset/{key}/collection-valued-Nav/$count
CreateCountPath(currentPath, convertSettings);
if(propertyPathIsEmpty ||
(count?.IsNonCountableNavigationProperty(propertyPath) ?? true))
{
// ~/entityset/{key}/collection-valued-Nav/$count
CreateCountPath(currentPath, convertSettings);
}
}
// ~/entityset/{key}/collection-valued-Nav/subtype
// ~/entityset/{key}/single-valued-Nav/subtype
CreateTypeCastPaths(currentPath, convertSettings, navigationProperty.DeclaringType, navigationProperty, targetsMany);

if (!navigationProperty.ContainsTarget)
{
Expand All @@ -305,6 +322,8 @@ private void RetrieveNavigationPropertyPaths(IEdmNavigationProperty navigationPr
// Collection-valued: DELETE ~/entityset/{key}/collection-valued-Nav/{key}/$ref
currentPath.Push(new ODataKeySegment(navEntityType));
CreateRefPath(currentPath);

CreateTypeCastPaths(currentPath, convertSettings, navigationProperty.DeclaringType, navigationProperty, false); // ~/entityset/{key}/collection-valued-Nav/{id}/subtype
}

// Get possible stream paths for the navigation entity type
Expand All @@ -317,6 +336,8 @@ private void RetrieveNavigationPropertyPaths(IEdmNavigationProperty navigationPr
{
currentPath.Push(new ODataKeySegment(navEntityType));
AppendPath(currentPath.Clone());

CreateTypeCastPaths(currentPath, convertSettings, navigationProperty.DeclaringType, navigationProperty, false); // ~/entityset/{key}/collection-valued-Nav/{id}/subtype
}

// Get possible stream paths for the navigation entity type
Expand Down Expand Up @@ -393,6 +414,60 @@ private void CreateCountPath(ODataPath currentPath, OpenApiConvertSettings conve
AppendPath(countPath);
}

/// <summary>
/// Create OData type cast paths.
/// </summary>
/// <param name="currentPath">The current OData path.</param>
/// <param name="convertSettings">The settings for the current conversion.</param>
/// <param name="structuredType">The type that is being inherited from to which this method will add downcast path segments.</param>
/// <param name="annotable">The annotable navigation source to read cast annotations from.</param>
/// <param name="targetsMany">Whether the annotable navigation source targets many entities.</param>
private void CreateTypeCastPaths(ODataPath currentPath, OpenApiConvertSettings convertSettings, IEdmStructuredType structuredType, IEdmVocabularyAnnotatable annotable, bool targetsMany)
{
if(currentPath == null) throw Error.ArgumentNull(nameof(currentPath));
if(convertSettings == null) throw new ArgumentNullException(nameof(convertSettings));
if(structuredType == null) throw new ArgumentNullException(nameof(structuredType));
if(annotable == null) throw new ArgumentNullException(nameof(annotable));
if(!convertSettings.EnableODataTypeCast) return;

var annotedTypeNames = GetDerivedTypeConstaintTypeNames(annotable);

if(!annotedTypeNames.Any() && convertSettings.RequireDerivedTypesConstraintForODataTypeCastSegments) return; // we don't want to generate any downcast path item if there is no type cast annotation.
baywet marked this conversation as resolved.
Show resolved Hide resolved

var annotedTypeNamesSet = new HashSet<string>(annotedTypeNames, StringComparer.OrdinalIgnoreCase);

bool filter(IEdmStructuredType x) =>
convertSettings.RequireDerivedTypesConstraintForODataTypeCastSegments && annotedTypeNames.Contains(x.FullTypeName()) ||
!convertSettings.RequireDerivedTypesConstraintForODataTypeCastSegments && (
!annotedTypeNames.Any() ||
annotedTypeNames.Contains(x.FullTypeName())
);

var targetTypes = _model
.FindAllDerivedTypes(structuredType)
.Where(x => x.TypeKind == EdmTypeKind.Entity && filter(x))
.OfType<IEdmEntityType>()
.ToArray();

foreach(var targetType in targetTypes)
{
var castPath = currentPath.Clone();
castPath.Push(new ODataTypeCastSegment(targetType));
AppendPath(castPath);
if(targetsMany)
{
CreateCountPath(castPath, convertSettings);
}
else
{
foreach(var declaredNavigationProperty in targetType.DeclaredNavigationProperties())
{
RetrieveNavigationPropertyPaths(declaredNavigationProperty, null, castPath, convertSettings);
}
}
}
}

/// <summary>
/// Retrieve all bounding <see cref="IEdmOperation"/>.
/// </summary>
Expand All @@ -419,26 +494,19 @@ private void RetrieveBoundOperationPaths(OpenApiConvertSettings convertSettings)
}

var firstEntityType = bindingType.AsEntity().EntityDefinition();
var allEntitiesForOperation= new List<IEdmEntityType>(){ firstEntityType };

System.Func<IEdmNavigationSource, bool> filter = (z) =>
bool filter(IEdmNavigationSource z) =>
z.EntityType() != firstEntityType &&
z.EntityType().FindAllBaseTypes().Contains(firstEntityType);

//Search all EntitySets
allEntitiesForOperation.AddRange(
_model.EntityContainer.EntitySets()
.Where(filter).Select(x => x.EntityType())
);

//Search all singletons
allEntitiesForOperation.AddRange(
_model.EntityContainer.Singletons()
.Where(filter).Select(x => x.EntityType())
);

allEntitiesForOperation = allEntitiesForOperation.Distinct().ToList();

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 Down Expand Up @@ -468,7 +536,7 @@ private void RetrieveBoundOperationPaths(OpenApiConvertSettings convertSettings)
}
}
}
private static readonly HashSet<ODataPathKind> _oDataPathKindsToSkipForOperations = new HashSet<ODataPathKind>() {
private static readonly HashSet<ODataPathKind> _oDataPathKindsToSkipForOperationsWhenSingle = new() {
ODataPathKind.EntitySet,
ODataPathKind.MediaEntity,
ODataPathKind.DollarCount
Expand All @@ -483,8 +551,22 @@ private bool AppendBoundOperationOnNavigationSourcePath(IEdmOperation edmOperati

foreach (var subPath in value)
{
if ((isCollection && subPath.Kind == ODataPathKind.EntitySet) ||
(!isCollection && !_oDataPathKindsToSkipForOperations.Contains(subPath.Kind)))
var lastPathSegment = subPath.LastOrDefault();
var secondLastPathSegment = subPath.Count > 1 ? subPath.ElementAt(subPath.Count - 2) : null;
if (subPath.Kind == ODataPathKind.TypeCast &&
!isCollection &&
secondLastPathSegment != null &&
secondLastPathSegment is not ODataKeySegment &&
(secondLastPathSegment is not ODataNavigationSourceSegment navSource || navSource.NavigationSource is not IEdmSingleton) &&
(secondLastPathSegment is not ODataNavigationPropertySegment navProp || navProp.NavigationProperty.Type.IsCollection()))
{// we don't want to add operations bound to single elements on type cast segments under collections, only under the key segment, singletons and nav props bound to singles.
continue;
}
else if ((lastPathSegment is not ODataTypeCastSegment castSegment ||
castSegment.EntityType == bindingEntityType ||
bindingEntityType.InheritsFrom(castSegment.EntityType)) && // we don't want to add operations from the parent types under type cast segments because they already are present without the cast
((isCollection && subPath.Kind == ODataPathKind.EntitySet) ||
(!isCollection && !_oDataPathKindsToSkipForOperationsWhenSingle.Contains(subPath.Kind))))
{
ODataPath newPath = subPath.Clone();
newPath.Push(new ODataOperationSegment(edmOperation, isEscapedFunction));
Expand Down Expand Up @@ -611,9 +693,11 @@ private bool HasUnsatisfiedDerivedTypeConstraint(
OpenApiConvertSettings convertSettings)
{
return convertSettings.RequireDerivedTypesConstraintForBoundOperations &&
!(_model.GetCollection(annotatable, "Org.OData.Validation.V1.DerivedTypeConstraint") ?? Enumerable.Empty<string>())
!GetDerivedTypeConstaintTypeNames(annotatable)
.Any(c => c.Equals(baseType.FullName(), StringComparison.OrdinalIgnoreCase));
}
private IEnumerable<string> GetDerivedTypeConstaintTypeNames(IEdmVocabularyAnnotatable annotatable) =>
_model.GetCollection(annotatable, "Org.OData.Validation.V1.DerivedTypeConstraint") ?? Enumerable.Empty<string>();

private bool AppendBoundOperationOnDerivedNavigationPropertyPath(
IEdmOperation edmOperation,
Expand Down
13 changes: 13 additions & 0 deletions src/Microsoft.OpenApi.OData.Reader/Edm/ODataSegment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.OData.Edm;
using Microsoft.OpenApi.Models;
using Microsoft.OpenApi.OData.Common;

namespace Microsoft.OpenApi.OData.Edm
{
Expand Down Expand Up @@ -100,6 +102,17 @@ public string GetPathItemName(OpenApiConvertSettings settings)
{
return GetPathItemName(settings, new HashSet<string>());
}
/// <summary>
/// Profides a suffix for the operation id based on the operation path.
/// </summary>
/// <param name="path">Path to use to deduplicate.</param>
/// <param name="settings">The settings.</param>
///<returns>The suffix.</returns>
public string GetPathHash(OpenApiConvertSettings settings, ODataPath path = default)
baywet marked this conversation as resolved.
Show resolved Hide resolved
{
var suffix = string.Join("/", path?.Segments.Select(x => x.Identifier).Distinct() ?? Enumerable.Empty<string>());
return (GetPathItemName(settings) + suffix).GetHashSHA256().Substring(0, 4);
}

/// <summary>
/// Gets the path item name for this segment.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public class ODataStreamPropertySegment : ODataSegment
{
private readonly string _streamPropertyName;
/// <summary>
/// Initializes a new instance of <see cref="ODataTypeCastSegment"/> class.
/// Initializes a new instance of <see cref="ODataStreamPropertySegment"/> class.
/// </summary>
/// <param name="streamPropertyName">The name of the stream property.</param>
public ODataStreamPropertySegment(string streamPropertyName)
Expand Down
12 changes: 12 additions & 0 deletions src/Microsoft.OpenApi.OData.Reader/OpenApiConvertSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,16 @@ public string PathPrefix
/// Gets/sets a value indicating whether or not single quotes surrounding string parameters in url templates should be added.
/// </summary>
public bool AddSingleQuotesForStringParameters { get; set; } = false;

/// <summary>
/// Gets/sets a value indicating whether or not to include the OData type cast segments.
/// </summary>
public bool EnableODataTypeCast { get; set; } = true;
baywet marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// Gets/sets a value indicating whether or not to require a derived types constraint to include the OData type cast segments.
/// </summary>
public bool RequireDerivedTypesConstraintForODataTypeCastSegments { get; set; } = true;
baywet marked this conversation as resolved.
Show resolved Hide resolved

internal OpenApiConvertSettings Clone()
{
Expand Down Expand Up @@ -225,6 +235,8 @@ internal OpenApiConvertSettings Clone()
PathProvider = this.PathProvider,
EnableDollarCountPath = this.EnableDollarCountPath,
AddSingleQuotesForStringParameters = this.AddSingleQuotesForStringParameters,
EnableODataTypeCast = this.EnableODataTypeCast,
RequireDerivedTypesConstraintForODataTypeCastSegments = this.RequireDerivedTypesConstraintForODataTypeCastSegments,
};

return newSettings;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ protected override void SetBasicInfo(OpenApiOperation operation)
// OperationId
if (Context.Settings.EnableOperationId)
{
operation.OperationId = $"Get.Count.{LastSecondSegment.Identifier}";
operation.OperationId = $"Get.Count.{LastSecondSegment.Identifier}-{Path.GetPathHash(Context.Settings)}";
}

base.SetBasicInfo(operation);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,9 @@ protected override void SetBasicInfo(OpenApiOperation operation)
}
else
{
ODataOperationImportSegment operationImportSegment = Path.LastSegment as ODataOperationImportSegment;
string pathItemName = operationImportSegment.GetPathItemName(Context.Settings, new HashSet<string>());
if (Context.Model.IsOperationImportOverload(EdmOperationImport))
{
string hash = pathItemName.GetHashSHA256();
operation.OperationId = "FunctionImport." + EdmOperationImport.Name + "-" + hash.Substring(0, 4);
operation.OperationId = "FunctionImport." + EdmOperationImport.Name + "-" + Path.LastSegment.GetPathHash(Context.Settings);
}
else
{
Expand Down
Loading