Skip to content

Commit

Permalink
Fixes potential duplicate operation ids of navigation property paths …
Browse files Browse the repository at this point in the history
…with overloaded composable functions (#597)

* Fixes generation of navigation property op ids with overloaded composable fxns

* Adds validation unit test

* Updates release note

* chore: adds string comparison

Co-authored-by: Andrew Omondi <andrueastman@users.noreply.github.com>

---------

Co-authored-by: Vincent Biret <vibiret@microsoft.com>
Co-authored-by: Andrew Omondi <andrueastman@users.noreply.github.com>
  • Loading branch information
3 people authored Oct 17, 2024
1 parent 59a8d35 commit a464bed
Show file tree
Hide file tree
Showing 9 changed files with 122 additions and 25 deletions.
60 changes: 46 additions & 14 deletions src/Microsoft.OpenApi.OData.Reader/Common/EdmModelHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,38 +93,40 @@ internal static bool NavigationRestrictionsAllowsNavigability(
/// Generates the operation id from a navigation property path.
/// </summary>
/// <param name="path">The target <see cref="ODataPath"/>.</param>
/// <param name="context">The OData context.</param>
/// <param name="prefix">Optional: Identifier indicating whether it is a collection-valued non-indexed or single-valued navigation property.</param>
/// <returns>The operation id generated from the given navigation property path.</returns>
internal static string GenerateNavigationPropertyPathOperationId(ODataPath path, string prefix = null)
internal static string GenerateNavigationPropertyPathOperationId(ODataPath path, ODataContext context, string prefix = null)
{
IList<string> items = RetrieveNavigationPropertyPathsOperationIdSegments(path);
IList<string> items = RetrieveNavigationPropertyPathsOperationIdSegments(path, context);

if (!items.Any())
return null;

int lastItemIndex = items.Count - 1;
int lastItemIndex = items[^1].StartsWith('-') ? items.Count - 2 : items.Count - 1;

if (!string.IsNullOrEmpty(prefix))
{
items[lastItemIndex] = prefix + Utils.UpperFirstChar(items.Last());
items[lastItemIndex] = prefix + Utils.UpperFirstChar(items[lastItemIndex]);
}
else
{
items[lastItemIndex] = Utils.UpperFirstChar(items.Last());
items[lastItemIndex] = Utils.UpperFirstChar(items[lastItemIndex]);
}

return string.Join(".", items);
return GenerateNavigationPropertyPathOperationId(items);
}

/// <summary>
/// Generates the operation id from a complex property path.
/// </summary>
/// <param name="path">The target <see cref="ODataPath"/>.</param>
/// <param name="context">The OData context.</param>
/// <param name="prefix">Optional: Identifier indicating whether it is a collection-valued or single-valued complex property.</param>
/// <returns>The operation id generated from the given complex property path.</returns>
internal static string GenerateComplexPropertyPathOperationId(ODataPath path, string prefix = null)
internal static string GenerateComplexPropertyPathOperationId(ODataPath path, ODataContext context, string prefix = null)
{
IList<string> items = RetrieveNavigationPropertyPathsOperationIdSegments(path);
IList<string> items = RetrieveNavigationPropertyPathsOperationIdSegments(path, context);

if (!items.Any())
return null;
Expand All @@ -141,15 +143,29 @@ internal static string GenerateComplexPropertyPathOperationId(ODataPath path, st
items.Add(Utils.UpperFirstChar(lastSegment?.Identifier));
}

return string.Join(".", items);
return GenerateNavigationPropertyPathOperationId(items);
}

/// <summary>
/// Generates a navigation property operation id from a list of string values.
/// </summary>
/// <param name="items">The list of string values.</param>
/// <returns>The generated navigation property operation id.</returns>
private static string GenerateNavigationPropertyPathOperationId(IList<string> items)
{
if (!items.Any())
return null;

return string.Join(".", items).Replace(".-", "-", StringComparison.OrdinalIgnoreCase); // Format any hashed value appropriately (this will be the last value)
}

/// <summary>
/// Retrieves the segments of an operation id generated from a navigation property path.
/// </summary>
/// <param name="path">The target <see cref="ODataPath"/>.</param>
/// <param name="context">The OData context.</param>
/// <returns>The segments of an operation id generated from the given navigation property path.</returns>
internal static IList<string> RetrieveNavigationPropertyPathsOperationIdSegments(ODataPath path)
internal static IList<string> RetrieveNavigationPropertyPathsOperationIdSegments(ODataPath path, ODataContext context)
{
Utils.CheckArgumentNull(path, nameof(path));

Expand All @@ -173,6 +189,8 @@ s is ODataOperationSegment ||
Utils.CheckArgumentNull(segments, nameof(segments));

string previousTypeCastSegmentId = null;
string pathHash = string.Empty;

foreach (var segment in segments)
{
if (segment is ODataNavigationPropertySegment navPropSegment)
Expand All @@ -192,6 +210,14 @@ s is ODataOperationSegment ||
else if (segment is ODataOperationSegment operationSegment)
{
// Navigation property generated via composable function
if (operationSegment.Operation is IEdmFunction function && context.Model.IsOperationOverload(function))
{
// Hash the segment to avoid duplicate operationIds
pathHash = string.IsNullOrEmpty(pathHash)
? operationSegment.GetPathHash(context.Settings)
: (pathHash + operationSegment.GetPathHash(context.Settings)).GetHashSHA256()[..4];
}

items.Add(operationSegment.Identifier);
}
else if (segment is ODataKeySegment keySegment && keySegment.IsAlternateKey)
Expand All @@ -208,6 +234,11 @@ s is ODataOperationSegment ||
}
}

if (!string.IsNullOrEmpty(pathHash))
{
items.Add("-" + pathHash);
}

return items;
}

Expand Down Expand Up @@ -320,9 +351,10 @@ internal static string GenerateComplexPropertyPathTagName(ODataPath path, ODataC
/// Generates the operation id prefix from an OData type cast path.
/// </summary>
/// <param name="path">The target <see cref="ODataPath"/>.</param>
/// <param name="context">The OData context.</param>
/// <param name="includeListOrGetPrefix">Optional: Whether to include the List or Get prefix to the generated operation id.</param>
/// <returns>The operation id prefix generated from the OData type cast path.</returns>
internal static string GenerateODataTypeCastPathOperationIdPrefix(ODataPath path, bool includeListOrGetPrefix = true)
internal static string GenerateODataTypeCastPathOperationIdPrefix(ODataPath path, ODataContext context, bool includeListOrGetPrefix = true)
{
// Get the segment before the last OData type cast segment
ODataTypeCastSegment typeCastSegment = path.Segments.OfType<ODataTypeCastSegment>()?.Last();
Expand Down Expand Up @@ -352,7 +384,7 @@ internal static string GenerateODataTypeCastPathOperationIdPrefix(ODataPath path
if (secondLastSegment is ODataComplexPropertySegment complexSegment)
{
string listOrGet = includeListOrGetPrefix ? (complexSegment.Property.Type.IsCollection() ? "List" : "Get") : null;
operationId = GenerateComplexPropertyPathOperationId(path, listOrGet);
operationId = GenerateComplexPropertyPathOperationId(path, context, listOrGet);
}
else if (secondLastSegment is ODataNavigationPropertySegment navPropSegment)
{
Expand All @@ -362,13 +394,13 @@ internal static string GenerateODataTypeCastPathOperationIdPrefix(ODataPath path
prefix = navPropSegment?.NavigationProperty.TargetMultiplicity() == EdmMultiplicity.Many ? "List" : "Get";
}

operationId = GenerateNavigationPropertyPathOperationId(path, prefix);
operationId = GenerateNavigationPropertyPathOperationId(path, context, prefix);
}
else if (secondLastSegment is ODataKeySegment keySegment)
{
if (isIndexedCollValuedNavProp)
{
operationId = GenerateNavigationPropertyPathOperationId(path, "Get");
operationId = GenerateNavigationPropertyPathOperationId(path, context, "Get");
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<TargetFrameworks>net8.0</TargetFrameworks>
<PackageId>Microsoft.OpenApi.OData</PackageId>
<SignAssembly>true</SignAssembly>
<Version>2.0.0-preview.5</Version>
<Version>2.0.0-preview.6</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 @@ -28,9 +28,10 @@
- Adds nullable to double schema conversions #581
- Updates tag names for actions/functions operations #585
- Creates unique operation ids for paths with composable overloaded functions #580
- Further fixes for double/decimal/float schema conversions #581
- Replaced integer types by number types
- Further fixes for double/decimal/float schema conversions #581
- Replaced integer types by number types
- Further fix for generating unique operation ids for paths with composable overloaded functions where all functions in path are overloaded #594
- Further fix for generating unique operation ids for navigation property paths with composable overloaded functions #596
</PackageReleaseNotes>
<AssemblyName>Microsoft.OpenApi.OData.Reader</AssemblyName>
<AssemblyOriginatorKeyFile>..\..\tool\Microsoft.OpenApi.OData.snk</AssemblyOriginatorKeyFile>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ protected override void SetBasicInfo(OpenApiOperation operation)
if (Context.Settings.EnableOperationId)
{
string prefix = ComplexPropertySegment.Property.Type.IsCollection() ? "List" : "Get";
operation.OperationId = EdmModelHelper.GenerateComplexPropertyPathOperationId(Path, prefix);
operation.OperationId = EdmModelHelper.GenerateComplexPropertyPathOperationId(Path, Context, prefix);
}

// Summary and Description
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ protected override void SetBasicInfo(OpenApiOperation operation)
// OperationId
if (Context.Settings.EnableOperationId)
{
operation.OperationId = EdmModelHelper.GenerateComplexPropertyPathOperationId(Path, "Set");
operation.OperationId = EdmModelHelper.GenerateComplexPropertyPathOperationId(Path, Context, "Set");
}

// Summary and Description
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ protected override void SetBasicInfo(OpenApiOperation operation)
// OperationId
if (Context.Settings.EnableOperationId)
{
operation.OperationId = EdmModelHelper.GenerateComplexPropertyPathOperationId(Path, "Update");
operation.OperationId = EdmModelHelper.GenerateComplexPropertyPathOperationId(Path, Context, "Update");
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,17 +128,17 @@ protected override void SetBasicInfo(OpenApiOperation operation)
}
else if (SecondLastSegment is ODataNavigationPropertySegment)
{
var navPropOpId = string.Join(".", EdmModelHelper.RetrieveNavigationPropertyPathsOperationIdSegments(Path));
var navPropOpId = string.Join(".", EdmModelHelper.RetrieveNavigationPropertyPathsOperationIdSegments(Path, Context));
operation.OperationId = $"{navPropOpId}.GetCount-{Path.GetPathHash(Context.Settings)}";
}
else if (SecondLastSegment is ODataTypeCastSegment odataTypeCastSegment)
{
IEdmNamedElement targetStructuredType = odataTypeCastSegment.StructuredType as IEdmNamedElement;
operation.OperationId = $"{EdmModelHelper.GenerateODataTypeCastPathOperationIdPrefix(Path, false)}.GetCount.As{Utils.UpperFirstChar(targetStructuredType.Name)}-{Path.GetPathHash(Context.Settings)}";
operation.OperationId = $"{EdmModelHelper.GenerateODataTypeCastPathOperationIdPrefix(Path, Context, false)}.GetCount.As{Utils.UpperFirstChar(targetStructuredType.Name)}-{Path.GetPathHash(Context.Settings)}";
}
else if (SecondLastSegment is ODataComplexPropertySegment)
{
operation.OperationId = $"{EdmModelHelper.GenerateComplexPropertyPathOperationId(Path)}.GetCount-{Path.GetPathHash(Context.Settings)}";
operation.OperationId = $"{EdmModelHelper.GenerateComplexPropertyPathOperationId(Path, Context)}.GetCount-{Path.GetPathHash(Context.Settings)}";
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ protected override void SetExtensions(OpenApiOperation operation)

internal string GetOperationId(string prefix = null)
{
return EdmModelHelper.GenerateNavigationPropertyPathOperationId(Path, prefix);
return EdmModelHelper.GenerateNavigationPropertyPathOperationId(Path, Context, prefix);
}

/// <inheritdoc/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ protected override void SetBasicInfo(OpenApiOperation operation)

// OperationId
if (Context.Settings.EnableOperationId)
operation.OperationId = EdmModelHelper.GenerateODataTypeCastPathOperationIdPrefix(Path) + $".As{Utils.UpperFirstChar(TargetSchemaElement.Name)}";
operation.OperationId = EdmModelHelper.GenerateODataTypeCastPathOperationIdPrefix(Path, Context) + $".As{Utils.UpperFirstChar(TargetSchemaElement.Name)}";

base.SetBasicInfo(operation);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,70 @@ public void CreateNavigationGetOperationViaComposableFunctionReturnsCorrectOpera
Assert.Contains(operation.Parameters, x => x.Name == "path");
}

[Fact]
public void CreateNavigationGetOperationViaOverloadedComposableFunctionReturnsCorrectOperation()
{
// Arrange
IEdmModel model = EdmModelHelper.GraphBetaModel;
ODataContext context = new(model, new OpenApiConvertSettings()
{
EnableOperationId = true
});

IEdmEntitySet drives = model.EntityContainer.FindEntitySet("drives");
IEdmEntityType drive = model.SchemaElements.OfType<IEdmEntityType>().First(c => c.Name == "drive");
IEdmNavigationProperty items = drive.DeclaredNavigationProperties().First(c => c.Name == "items");
IEdmEntityType driveItem = model.SchemaElements.OfType<IEdmEntityType>().First(c => c.Name == "driveItem");
IEdmNavigationProperty workbook = driveItem.DeclaredNavigationProperties().First(c => c.Name == "workbook");
IEdmEntityType workbookEntity = model.SchemaElements.OfType<IEdmEntityType>().First(c => c.Name == "workbook");
IEdmNavigationProperty worksheets = workbookEntity.DeclaredNavigationProperties().First(c => c.Name == "worksheets");
IEdmEntityType workbookWorksheet = model.SchemaElements.OfType<IEdmEntityType>().First(c => c.Name == "workbookWorksheet");
IEdmOperation usedRangeWithParams = model.SchemaElements.OfType<IEdmOperation>().First(f => f.Name == "usedRange" && f.Parameters.Any(x => x.Name.Equals("valuesOnly")));
IEdmOperation usedRange = model.SchemaElements.OfType<IEdmOperation>().First(f => f.Name == "usedRange" && f.Parameters.Count() == 1);
IEdmEntityType workbookRange = model.SchemaElements.OfType<IEdmEntityType>().First(c => c.Name == "workbookRange");
IEdmNavigationProperty format = workbookRange.DeclaredNavigationProperties().First(c => c.Name == "format");


ODataPath path1 = new(new ODataNavigationSourceSegment(drives),
new ODataKeySegment(drive),
new ODataNavigationPropertySegment(items),
new ODataKeySegment(driveItem),
new ODataNavigationPropertySegment(workbook),
new ODataNavigationPropertySegment(worksheets),
new ODataKeySegment(workbookWorksheet),
new ODataOperationSegment(usedRangeWithParams),
new ODataNavigationPropertySegment(format));

ODataPath path2 = new(new ODataNavigationSourceSegment(drives),
new ODataKeySegment(drive),
new ODataNavigationPropertySegment(items),
new ODataKeySegment(driveItem),
new ODataNavigationPropertySegment(workbook),
new ODataNavigationPropertySegment(worksheets),
new ODataKeySegment(workbookWorksheet),
new ODataOperationSegment(usedRange),
new ODataNavigationPropertySegment(format));

// Act
var operation1 = _operationHandler.CreateOperation(context, path1);
var operation2 = _operationHandler.CreateOperation(context, path2);

// Assert
Assert.NotNull(operation1);
Assert.NotNull(operation2);

Assert.Equal("drives.items.workbook.worksheets.usedRange.GetFormat-206d", operation1.OperationId);
Assert.Equal("drives.items.workbook.worksheets.usedRange.GetFormat-ec2c", operation2.OperationId);

Assert.NotNull(operation1.Parameters);
Assert.Equal(6, operation1.Parameters.Count);
Assert.Contains(operation1.Parameters, x => x.Name == "valuesOnly");

Assert.NotNull(operation2.Parameters);
Assert.Equal(5, operation2.Parameters.Count);
Assert.DoesNotContain(operation2.Parameters, x => x.Name == "valuesOnly");
}

[Theory]
[InlineData(true)]
[InlineData(false)]
Expand Down

0 comments on commit a464bed

Please sign in to comment.